breakaway 1.4.0-main.8 → 1.4.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/.agents/skills/pipeline/SKILL.md +127 -0
- package/.agents/skills/tasks/SKILL.md +2 -0
- package/README.md +9 -3
- package/package.json +10 -5
- package/prompts/add-repository.md +6 -5
- package/prompts/breakaway.md +2 -2
- package/prompts/core.md +22 -5
- package/prompts/install.md +91 -28
- package/scripts/board-files.mjs +20 -0
- package/scripts/install/cli.js +17 -2
- package/scripts/install/lib.js +26 -1
- package/scripts/lib/package-release.js +19 -0
- package/scripts/package-release.mjs +10 -2
- package/scripts/tasks/cli.js +178 -15
- package/scripts/tasks/github-connect.js +50 -0
- package/scripts/tasks/init.js +3 -567
- package/scripts/tasks/install-check.js +95 -0
- package/scripts/tasks/keep.js +17 -0
- package/scripts/tasks/pipeline.js +165 -1
- package/scripts/tasks/platform.js +55 -0
- package/scripts/tasks.mjs +304 -35
- package/src/cli-version.js +5 -6
- package/src/init.js +568 -0
- package/src/install.js +6 -1
- package/src/packages.js +56 -0
- package/src/prompt.js +3 -1
- package/src/repos.js +26 -9
- package/src/session-report.js +92 -0
- package/template/.github/workflows/deploy.yml +46 -14
- package/template/README.md +2 -0
- package/template/pipeline/ci.yml +40 -0
- package/template/pipeline/release.yml +71 -0
package/prompts/install.md
CHANGED
|
@@ -2,30 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
You're helping someone set up their own breakaway board: a task board for them and their coding agents, on their own Cloudflare account. It's theirs: they own it, run it, and decide. You do the typing. This file is the whole job. Read it to the end before you start, then work through it one step at a time.
|
|
4
4
|
|
|
5
|
-
The owner started you with something like: "Set up a breakaway board for me. Read https://leavethepack.dev/install.md and follow it." The full guide behind these steps is at https://leavethepack.dev/docs/quickstart/, if a step needs more detail.
|
|
5
|
+
The owner started you with something like: "Set up a breakaway board for me. Read https://leavethepack.dev/install.md and follow it." The full guide behind these steps is at https://leavethepack.dev/docs/quickstart/, if a step needs more detail. The install is done when one real task is closed by a merged pull request, not when the board is deployed.
|
|
6
6
|
|
|
7
7
|
## How you work
|
|
8
8
|
|
|
9
|
-
- **They decide, you do the typing.** Before anything that changes something outside this folder (a GitHub repository or its settings, anything on Cloudflare, a deploy), say in one line what it does and wait for a yes.
|
|
9
|
+
- **They decide, you do the typing.** Before anything that changes something outside this folder (a GitHub repository or its settings, anything on Cloudflare, a deploy, a task on the board), say in one line what it does and wait for a yes.
|
|
10
|
+
- **Look before you make.** Never make anything twice: a second install repository, Worker, `tasks.env`, GitHub App, or routine. When something exists but doesn't match what you expect, stop and ask.
|
|
10
11
|
- **Never ask for a secret in the chat, and never print one.** A token or key goes straight from where it is to where it's needed: a command reads it from a file without showing it, or the owner types it into their own terminal, where nothing reaches you. If one lands in the conversation anyway, say so, and have them make a new one.
|
|
11
|
-
- **Some steps are theirs.** Signing in to Cloudflare, GitHub, and claude.ai, making a token, anything in a browser, and the commands that ask for a secret.
|
|
12
|
-
- **Each step ends with a check.**
|
|
12
|
+
- **Some steps are theirs.** Signing in to Cloudflare, GitHub, and claude.ai, making a token, anything in a browser, and the commands that ask for a secret. For each, name the page (with its link), what to press or type, and what the page shows when it worked. Then wait until they say it's done.
|
|
13
|
+
- **Each step ends with a check, read one of three ways.** **Verified**: a command or the board tested it. **Not verified yet**: it's set up, but nothing has used it; say what will verify it. **Failed**: say what failed and the fix. Never call a step done when it's only Not verified yet. Once the board is up, `npx breakaway connections` lists everything it leans on: a row that reads Working or Verified is Verified, Not verified yet is the same, and Needs attention is Failed, with the row's fix.
|
|
14
|
+
- **POSIX shell only.** Every command here runs in a POSIX shell on macOS, Linux, or Windows through WSL.
|
|
13
15
|
- **Keep it short.** One step at a time: what you're doing and why, in a sentence, then do it.
|
|
14
16
|
|
|
15
|
-
## 0.
|
|
17
|
+
## 0. The system, and what's there
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
**The system.** Run `uname -s`. `Darwin` is macOS and `Linux` is Linux, or WSL when `grep -qi microsoft /proc/version` succeeds: carry on. Anything else (`MINGW…`, `MSYS…`, `CYGWIN…`, or no `uname`) is Windows itself, which the board doesn't support. Say so plainly, and that the fix is WSL: they open PowerShell as administrator, run `wsl --install`, restart, and open **Ubuntu** from the Start menu (https://learn.microsoft.com/windows/wsl/install). Inside it they install Node and Claude Code, make a folder, open Claude Code there, and paste the same line. Everything from here happens inside WSL; stop until then.
|
|
20
|
+
|
|
21
|
+
**The tools.** Check, and help install what's missing:
|
|
18
22
|
|
|
19
23
|
- Node 20 or later: `node --version`.
|
|
20
|
-
- Git, and the GitHub CLI signed in: `gh auth status`. If it isn't, they run `gh auth login` themselves.
|
|
21
|
-
- Wrangler signed in to their Cloudflare account: `npx wrangler whoami`. If it isn't, they run `npx wrangler login` themselves. Note the account ID
|
|
24
|
+
- Git, and the GitHub CLI signed in: `gh auth status`. If it isn't, they run `gh auth login` themselves and pick GitHub.com and the browser; it ends with "Logged in as <login>".
|
|
25
|
+
- Wrangler signed in to their Cloudflare account: `npx wrangler whoami`. If it isn't, they run `npx wrangler login` themselves: a Cloudflare page opens, they press **Allow**, and the terminal says "Successfully logged in". Note the account ID `whoami` shows: it isn't a secret, and step 1 needs it.
|
|
26
|
+
|
|
27
|
+
**What's there.** Before asking anything, look, in this order, and say what you found in one short list:
|
|
28
|
+
|
|
29
|
+
1. `~/.config/breakaway/tasks.env`: `test -f ~/.config/breakaway/tasks.env`, and which board it's for, `sed -n 's/^BREAKAWAY_URL=//p' ~/.config/breakaway/tasks.env`. Read nothing else from it.
|
|
30
|
+
2. The install repository: `breakaway.config.json` in this folder, or in the folder they name. If it's there, its `worker` and `url`, and `git remote get-url origin`.
|
|
31
|
+
3. The Worker: `npx wrangler deployments list --name <worker>`.
|
|
32
|
+
4. The board: `npx breakaway health`.
|
|
33
|
+
5. Its repositories: `npx breakaway repos`.
|
|
34
|
+
6. Its connections: `npx breakaway connections --json`.
|
|
35
|
+
|
|
36
|
+
Then carry on from the first step below that isn't done, and never repeat one that is:
|
|
37
|
+
|
|
38
|
+
| Step | Done when |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| 1. The install repository | `breakaway.config.json` is on the GitHub repository's `main`, and its `production` environment has both Cloudflare secrets |
|
|
41
|
+
| 2. The board's secrets | `tasks.env` exists |
|
|
42
|
+
| 3. Deploy | `health` says the board is healthy, and `tasks.env` has its address |
|
|
43
|
+
| 4. The repository their agents work on | `repos` lists it |
|
|
44
|
+
| 5. GitHub | the GitHub rows on Connections are Verified, and the board's files are on the repository's default branch |
|
|
45
|
+
| 6. Agents from the board | its **Agent routine** row is connected (or they have no routines) |
|
|
46
|
+
| 7. The first task | a task is closed by a merged pull request |
|
|
22
47
|
|
|
23
|
-
|
|
48
|
+
Stop and ask, and make nothing, when what's there doesn't fit:
|
|
49
|
+
|
|
50
|
+
- a `tasks.env` for another address, or a `breakaway.config.json` whose `worker` isn't the Worker they mean;
|
|
51
|
+
- a Worker that exists while `tasks.env` doesn't: never run `init-secrets` for it, since new secrets lock them out of that board. Their copy may be in their password manager; otherwise https://leavethepack.dev/docs/operations/ says how to recover each value;
|
|
52
|
+
- `health` answering 401: the token in `tasks.env` isn't the one on the Worker. `npx wrangler secret list --name <worker>` shows only names: if the three secrets are already there, they're another `tasks.env`'s, so never put new ones over them.
|
|
53
|
+
|
|
54
|
+
Then ask what's still open, in one message, and nothing the look already answered:
|
|
24
55
|
|
|
25
56
|
1. What to call the board, and the GitHub `owner/name` for its install repository, the private repository that deploys it (for example `<their login>/my-board`).
|
|
26
57
|
2. Where the board should answer: an address on a domain in their Cloudflare account (like `tasks.example.com`), or nothing, for a `workers.dev` address.
|
|
27
58
|
3. The repository their agents will work on (`owner/name`), a short name for it, and its areas, each with a work-ID prefix (like `app:APP` or `docs:DOC`).
|
|
28
|
-
4. Whether they have a Claude plan with routines, to start agents from the board. Without one, the board works the same, and they start agents themselves.
|
|
59
|
+
4. Whether they have a Claude plan with routines, to start agents from the board. Without one, the board works the same, but they give up starting agents from the board, live output on a task, starting by itself when ready, chase, and scheduled routines; they start agents themselves in Claude Code.
|
|
29
60
|
5. `stable` (recommended: each new release comes as a pull request they merge) or `main` (the board follows every change to breakaway).
|
|
30
61
|
|
|
31
62
|
## 1. The install repository
|
|
@@ -48,7 +79,7 @@ gh secret set CLOUDFLARE_ACCOUNT_ID --env production --repo <owner>/<name> --bod
|
|
|
48
79
|
gh api -X PUT repos/<owner>/<name>/actions/permissions/workflow -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true
|
|
49
80
|
```
|
|
50
81
|
|
|
51
|
-
|
|
82
|
+
**Theirs: the Cloudflare API token.** At https://dash.cloudflare.com/profile/api-tokens, they press **Create Token**, then **Use template** beside **Edit Cloudflare Workers**. Under **Account Resources** they pick their account; under **Zone Resources**, their domain's zone for an address on their own domain, or all zones otherwise. **Continue to summary**, then **Create Token**: the page shows the token once. Then they run this in their own terminal, not through you; it asks for the token without showing it, and says "Set Actions secret CLOUDFLARE_API_TOKEN":
|
|
52
83
|
|
|
53
84
|
```sh
|
|
54
85
|
gh secret set CLOUDFLARE_API_TOKEN --env production --repo <owner>/<name>
|
|
@@ -60,15 +91,17 @@ That token can run `wrangler deploy`, so Deploy can apply a new address, cron tr
|
|
|
60
91
|
gh variable set BREAKAWAY_DEPLOY_CHANGES --body true --repo <owner>/<name>
|
|
61
92
|
```
|
|
62
93
|
|
|
63
|
-
**Check:** `gh api repos/<owner>/<name>/environments/production/secrets --jq '.secrets[].name'` lists both secrets, and the workflows are on `main`.
|
|
94
|
+
**Check:** `gh api repos/<owner>/<name>/environments/production/secrets --jq '.secrets[].name'` lists both secrets, and the workflows are on `main`. That's Verified; whether the token works is Not verified yet, until step 3's dry run.
|
|
64
95
|
|
|
65
96
|
## 2. The board's secrets
|
|
66
97
|
|
|
98
|
+
Only when step 0 found no `tasks.env` and no Worker:
|
|
99
|
+
|
|
67
100
|
```sh
|
|
68
101
|
npx breakaway init-secrets
|
|
69
102
|
```
|
|
70
103
|
|
|
71
|
-
It writes `~/.config/breakaway/tasks.env` (only they can read it) and prints which value goes where, never the values.
|
|
104
|
+
It writes `~/.config/breakaway/tasks.env` (only they can read it), lists what to keep, and prints which value goes where, never the values. Never run it with `--force` on an install that has a board.
|
|
72
105
|
|
|
73
106
|
**Check:** the file exists. The secrets go on the Worker in step 3, once it exists.
|
|
74
107
|
|
|
@@ -82,9 +115,9 @@ gh run list --repo <owner>/<name> --workflow deploy.yml --limit 1 # it takes a
|
|
|
82
115
|
gh run watch <run ID> --repo <owner>/<name> --exit-status
|
|
83
116
|
```
|
|
84
117
|
|
|
85
|
-
When the dry run passes, run it again without `-f dry-run=true` and watch it the same way. It stops with a message when something needs their hands: read the run's log (`gh run view <run ID> --repo <owner>/<name> --log-failed`) and say what it asks for.
|
|
118
|
+
When the dry run passes, run it again without `-f dry-run=true` and watch it the same way. On an address on their own domain, this first deploy can wait up to five minutes for its certificate, and can end with a notice that the board is waiting for its secrets: that's expected, they go on next. It stops with a message when something needs their hands, and it refuses to make a second, empty board when the install already has one: read the run's log (`gh run view <run ID> --repo <owner>/<name> --log-failed`) and say what it asks for.
|
|
86
119
|
|
|
87
|
-
Then put the three secrets on the Worker. Each command reads its value from `tasks.env` and pipes it to Wrangler, so it never shows:
|
|
120
|
+
Then put the three secrets on the Worker, unless `npx wrangler secret list --name <worker-name>` already lists them (step 0 says what then). Each command reads its value from `tasks.env` and pipes it to Wrangler, so it never shows:
|
|
88
121
|
|
|
89
122
|
```sh
|
|
90
123
|
v() { sed -n "s/^$1=//p" ~/.config/breakaway/tasks.env; }
|
|
@@ -93,9 +126,9 @@ v BREAKAWAY_CLIENT_ID | npx wrangler secret put TASKS_CLIENT_ID --name <worker-n
|
|
|
93
126
|
v BREAKAWAY_SYNC_KEY | npx wrangler secret put TASKS_SYNC_KEY --name <worker-name>
|
|
94
127
|
```
|
|
95
128
|
|
|
96
|
-
The board's address is the one they chose, or the `workers.dev` one the run's log shows. Add it to `tasks.env` as `BREAKAWAY_URL=<address
|
|
129
|
+
The board's address is the one they chose, or the `workers.dev` one the run's log shows. Add it to `tasks.env` as `BREAKAWAY_URL=<address>` if it isn't there, and to the install repository as the variable `BREAKAWAY_URL` (`gh variable set BREAKAWAY_URL --repo <owner>/<name> --body <address>`), so later deploys check it.
|
|
97
130
|
|
|
98
|
-
**Check:** `npx breakaway health` says the board is healthy. Then they open the address in their browser and sign in with `BREAKAWAY_TOKEN` from `tasks.env`, copying it from the file themselves.
|
|
131
|
+
**Check:** `npx breakaway health` says the board is healthy. Then they open the address in their browser and sign in with `BREAKAWAY_TOKEN` from `tasks.env`, copying it from the file themselves. The board says it has no repository yet and points to **Set up the board** on Connections, which ticks the steps below as they work.
|
|
99
132
|
|
|
100
133
|
## 4. The repository their agents work on
|
|
101
134
|
|
|
@@ -111,24 +144,54 @@ The first repository is the board's default. A prefix belongs to one repository
|
|
|
111
144
|
|
|
112
145
|
The board reads GitHub through a private GitHub App made for it.
|
|
113
146
|
|
|
114
|
-
1. **Theirs
|
|
115
|
-
2. **Theirs
|
|
147
|
+
1. **Theirs: make the App.** On the board, they open **GitHub** in the sidebar and press **Create the App on GitHub**. GitHub shows a **Create GitHub App** page with what the App may read; they keep the name and press **Create GitHub App**, and land back on the board, which shows a `npx breakaway github-connect <code>` command. They run it in their own terminal, in this folder, within the hour. It stores the App's keys and deploys a new version of the Worker. If it refuses because the board already has a working App, stop: that's the App to keep, and `--replace` is only for one Connections says has failed.
|
|
148
|
+
2. **Theirs: install it.** The command prints the install link. On GitHub they pick **Only select repositories**, choose the repository from step 4 (and the install repository too if they chose `main`, so the board can start its Deploy workflow), and press **Install**. Within a few seconds the board's GitHub view fills in.
|
|
116
149
|
3. **Yours, after a yes:** turn on auto-merge for the repository: `gh api -X PATCH repos/<owner/name> -F allow_auto_merge=true`.
|
|
117
|
-
4. **Yours:** add the board's files to the repository with `npx breakaway repos init <slug>`. It writes the agent prompt from sections you can fill in well: read the repository's README, `AGENTS.md`, and package scripts first, then pass `--building`, `--checks`, and `--pull-requests` from what you found. Ask them for `--direction` (what matters most right now) and `--never-share` (what must never leave the repository). It opens a pull request on the repository. They review and merge it.
|
|
150
|
+
4. **Yours:** add the board's files to the repository with `npx breakaway repos init <slug>`. It writes the agent prompt from sections you can fill in well: read the repository's README, `AGENTS.md`, and package scripts first, then pass `--building`, `--checks`, and `--pull-requests` from what you found. Ask them for `--direction` (what matters most right now) and `--never-share` (what must never leave the repository). It opens a pull request on the repository. They review and merge it on GitHub.
|
|
118
151
|
|
|
119
|
-
**Check:** `npx breakaway connections` shows the GitHub rows
|
|
152
|
+
**Check:** `npx breakaway connections` shows the GitHub rows Verified: the App, installed, permissions, auto-merge, and sync. The webhook stays Not verified yet until GitHub sends one, which the merge in item 4 does.
|
|
120
153
|
|
|
121
154
|
## 6. Agents from the board
|
|
122
155
|
|
|
123
|
-
Only with a Claude plan that has routines.
|
|
156
|
+
Only with a Claude plan that has routines. Without one, say again what they give up (step 0, question 4) and go to step 7.
|
|
157
|
+
|
|
158
|
+
**Theirs: the routine.** At https://claude.ai/code/routines, they press **New routine**, and go down this list, one line at a time, saying each one is done:
|
|
159
|
+
|
|
160
|
+
- [ ] **Repository:** the one from step 4.
|
|
161
|
+
- [ ] **Instructions:** the stub, from the board's **Agents** view (**Copy stub**), pasted as it is.
|
|
162
|
+
- [ ] **Cloud environment, network access:** **Custom**, with the board's host under **Allowed domains**, and **Also include default list of common package managers** ticked.
|
|
163
|
+
- [ ] **Cloud environment, API credential:** **Add credential**, type **Bearer**, the board's host as the allowed website, and `BREAKAWAY_TOKEN` from `tasks.env` as the value, which they copy from the file themselves. Not as an environment variable: the credential keeps the token out of the session.
|
|
164
|
+
- [ ] **Cloud environment, environment variable:** `BREAKAWAY_AGENT=claude-cloud`.
|
|
165
|
+
- [ ] **Trigger:** **Add trigger**, **API**. It shows a URL and a token, the token once.
|
|
166
|
+
|
|
167
|
+
**Theirs: connect it.** On the board's Connections, the **Agent routine** row's form takes the trigger's URL and token; or they run `npx breakaway agents-connect` in their own terminal and paste them when it asks.
|
|
168
|
+
|
|
169
|
+
**Check:** `npx breakaway connections` shows **Agent routine** connected and **Not verified yet**: the board can't read claude.ai, so nothing proves the routine works until an agent it starts claims a task. Step 7 does that. Don't start an agent just to test it.
|
|
170
|
+
|
|
171
|
+
## 7. The first task
|
|
172
|
+
|
|
173
|
+
The install is done when one real task is closed by its merged pull request.
|
|
174
|
+
|
|
175
|
+
1. **Propose one.** Read the repository's README and its open issues (`gh issue list --repo <owner/name> --limit 20`), and propose one small, useful task: a title, a sentence on why, and a done when they can check in a few minutes. After a yes, add it from a checkout of the repository (`gh repo clone <owner/name>` next to this folder, if there's none):
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
npx breakaway add "<title>" --project <area> --tag agent --horizon now --brief "<what and why>" --done-when "<what they can check>"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
2. **With routines:** they open **Set up the board** on Connections; its last step opens the **Add a repository** wizard's agent step, where they press **Start an agent on <ID>** (if it names another task, they press **Start an agent** on <ID>'s own page instead). The step ticks as it goes: started, live output, pull request. If the start fails, the step says why and the fix, and **Try again**. The **Agent routine** row reads **Verified by <ID>** once the agent claims the task.
|
|
182
|
+
3. **Without routines:** they open Claude Code in the checkout and say "Work on <ID> from the board". The board's files tell it how to claim, report, and open the pull request.
|
|
183
|
+
4. **Theirs: review and merge.** The pull request's title starts with the work ID and its description says `Closes <ID>.` They check it against the done when, and merge it on GitHub or on the board's pull request page. Merging stays theirs; the agent never merges.
|
|
184
|
+
|
|
185
|
+
**Check:** `npx breakaway show <ID>` says it's completed, merged in the pull request. With routines, the wizard's last check, merged, ticks too.
|
|
124
186
|
|
|
125
|
-
|
|
126
|
-
2. **Theirs:** run `npx breakaway agents-connect` in their own terminal and paste the trigger's URL and token when it asks.
|
|
187
|
+
## Before you stop
|
|
127
188
|
|
|
128
|
-
|
|
189
|
+
Run `npx breakaway connections` and go through anything that's Failed, with the fix each row gives. Taskwarrior is optional: if they use it, `npx breakaway setup` connects it.
|
|
129
190
|
|
|
130
|
-
|
|
191
|
+
Ask them to confirm they've put these in their password manager, whole, since the board can't give the values back:
|
|
131
192
|
|
|
132
|
-
|
|
193
|
+
- `~/.config/breakaway/tasks.env`: the token, and the only copy of the sync secret.
|
|
194
|
+
- `~/.config/breakaway/tasks-routines.json`, when there is one: other repositories' routines.
|
|
195
|
+
- `~/.config/breakaway/github-app.json`, only if `github-connect` wrote it: the App's keys, until they're stored.
|
|
133
196
|
|
|
134
|
-
|
|
197
|
+
Tell them how to get back in: open Claude Code in this same folder and paste the same line; it looks first and carries on where it stopped. Tell them where things are: the board in their browser, `npx breakaway help` for the CLI, and the docs at https://leavethepack.dev/docs/. Then stop. From here on, the board is theirs.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Writes src/board-files.json (BRK-132): the board's files repos init reads (boardSources in src/init.js), by path,
|
|
4
|
+
* so the Worker renders an empty repository's first commit from the same files as the CLI. It's generated, never
|
|
5
|
+
* committed (BRK-148): `pnpm install`, `build`, `typecheck`, `test`, and `deploy` run this first, and the release
|
|
6
|
+
* workflow's build puts it in the bundle.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { boardSources } from '../src/init.js';
|
|
10
|
+
|
|
11
|
+
const ROOT = new URL('../', import.meta.url);
|
|
12
|
+
const read = (path) => readFileSync(new URL(path, ROOT), 'utf8');
|
|
13
|
+
|
|
14
|
+
export const boardFiles = () => Object.fromEntries(boardSources(read).map((path) => [path, read(path)]));
|
|
15
|
+
|
|
16
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
17
|
+
writeFileSync(new URL('src/board-files.json', ROOT), `${JSON.stringify(boardFiles(), null, 2)}\n`);
|
|
18
|
+
// On stderr: npm pack and npm publish run it (prepare), and their --json goes to stdout.
|
|
19
|
+
console.error('Wrote src/board-files.json.');
|
|
20
|
+
}
|
package/scripts/install/cli.js
CHANGED
|
@@ -13,9 +13,10 @@ import {
|
|
|
13
13
|
bumpBody,
|
|
14
14
|
deployPlan,
|
|
15
15
|
deployTarget,
|
|
16
|
-
isHealthy,
|
|
17
16
|
latestReleases,
|
|
17
|
+
newWorkerStop,
|
|
18
18
|
parseState,
|
|
19
|
+
pingHealth,
|
|
19
20
|
previousVersionId,
|
|
20
21
|
shapeOf,
|
|
21
22
|
updatePlan,
|
|
@@ -152,6 +153,15 @@ export async function runStep(step, opts, io) {
|
|
|
152
153
|
`Couldn't list the Worker's deployments, so the deploy stopped before changing anything. Check that CLOUDFLARE_ACCOUNT_ID is your account's ID and that CLOUDFLARE_API_TOKEN can read Workers on it. Wrangler said:\n${output.trim()}`,
|
|
153
154
|
);
|
|
154
155
|
}
|
|
156
|
+
// A Worker that doesn't exist on an install that already has a board is a changed or mistyped name, not a first
|
|
157
|
+
// deploy (BRK-141): a dispatch has no earlier config for `check` to compare with.
|
|
158
|
+
const stop = newWorkerStop({
|
|
159
|
+
worker: configIn(join(dir, 'breakaway.config.json')).worker,
|
|
160
|
+
variable: opts.variable || null,
|
|
161
|
+
running: opts.running || null,
|
|
162
|
+
at: opts.at || null,
|
|
163
|
+
});
|
|
164
|
+
if (stop) throw new Stop(stop, 2);
|
|
155
165
|
io.out('first=true');
|
|
156
166
|
return 0;
|
|
157
167
|
}
|
|
@@ -163,7 +173,12 @@ export async function runStep(step, opts, io) {
|
|
|
163
173
|
} catch {
|
|
164
174
|
ping = null;
|
|
165
175
|
}
|
|
166
|
-
|
|
176
|
+
// A first deploy answers before its secrets are on (the install puts them on the Worker it made), so there it
|
|
177
|
+
// passes and says so; any later deploy needs them (BRK-141).
|
|
178
|
+
const health = pingHealth(ping, opts.version);
|
|
179
|
+
const first = opts.first === true || opts.first === 'true';
|
|
180
|
+
io.out(`health=${health}`);
|
|
181
|
+
return health === 'healthy' || (first && health === 'secrets') ? 0 : 1;
|
|
167
182
|
}
|
|
168
183
|
if (step === 'update') {
|
|
169
184
|
const state = stateIn(dir);
|
package/scripts/install/lib.js
CHANGED
|
@@ -253,9 +253,34 @@ export function workerMissing(output) {
|
|
|
253
253
|
return /\[code: 10007\]|worker does not exist on your account/iu.test(String(output ?? ''));
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* The message that stops a deploy about to make a new Worker on an install that already has a board (BRK-141), or null
|
|
258
|
+
* when making one is a first deploy. A board is there when the repository variable BREAKAWAY_URL is set (`variable`: the
|
|
259
|
+
* install sets it once the board answers) or the address the deploy checked (`at`) answered /api/ping with a release
|
|
260
|
+
* (`running`). A new Worker then means the name changed or is mistyped, and deploying it would open a second, empty board.
|
|
261
|
+
* @param {{ worker: string, variable?: string | null, running?: string | null, at?: string | null }} options
|
|
262
|
+
*/
|
|
263
|
+
export function newWorkerStop({ worker, variable = null, running = null, at = null }) {
|
|
264
|
+
if (!variable && !running) return null;
|
|
265
|
+
const seen = running
|
|
266
|
+
? `${at || variable || 'its address'} answers as breakaway ${running}`
|
|
267
|
+
: `the repository variable BREAKAWAY_URL is set (${variable})`;
|
|
268
|
+
return `There is no Worker named ${worker} on this Cloudflare account, but this install already has a board: ${seen}. Deploying would make a new Worker with an empty board, so nothing was deployed. Put "worker" in breakaway.config.json back to the name the board runs as (Workers & Pages on Cloudflare lists it), then run Deploy again. If that board is gone and you mean to start an empty one, delete the repository variable BREAKAWAY_URL, then run Deploy again.`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* What `/api/ping`'s answer says about the new release: `healthy` when it runs and its secrets load (BRK-96), `secrets`
|
|
273
|
+
* when it runs but a bound secret can't be read yet, and `down` for anything else.
|
|
274
|
+
* @returns {'healthy' | 'secrets' | 'down'}
|
|
275
|
+
*/
|
|
276
|
+
export function pingHealth(ping, version) {
|
|
277
|
+
if (ping?.ok !== true || ping.release !== version) return 'down';
|
|
278
|
+
return ping.secrets?.ok === true ? 'healthy' : 'secrets';
|
|
279
|
+
}
|
|
280
|
+
|
|
256
281
|
/** Whether `/api/ping`'s answer says the new release is running and its secrets load (BRK-96). */
|
|
257
282
|
export function isHealthy(ping, version) {
|
|
258
|
-
return
|
|
283
|
+
return pingHealth(ping, version) === 'healthy';
|
|
259
284
|
}
|
|
260
285
|
|
|
261
286
|
/**
|
|
@@ -56,6 +56,25 @@ export function nextPrerelease(current, tags, prefix = 'v') {
|
|
|
56
56
|
return { version, base, tag: `${prefix}${version}` };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The version a stable release's pull request sets package.json to (BRK-118, WEB-39): the next minor or major after
|
|
61
|
+
* the stable, or null when there is nothing to set. A patch is null, since the pre-releases count patches by
|
|
62
|
+
* themselves, and so is a package.json already at or past the choice (main moved on before an older pre-release was
|
|
63
|
+
* released).
|
|
64
|
+
* @param {string} stable the version just released, like 1.3.0
|
|
65
|
+
* @param {string} next patch, minor, or major
|
|
66
|
+
* @param {string} current package.json's version on the default branch
|
|
67
|
+
* @returns {string | null}
|
|
68
|
+
*/
|
|
69
|
+
export function nextVersion(stable, next, current) {
|
|
70
|
+
const [maj, min] = parts(stable);
|
|
71
|
+
parts(current);
|
|
72
|
+
if (next === 'patch') return null;
|
|
73
|
+
if (next !== 'minor' && next !== 'major') throw new Error(`next is patch, minor, or major, not "${next}".`);
|
|
74
|
+
const version = next === 'major' ? `${maj + 1}.0.0` : `${maj}.${min + 1}.0`;
|
|
75
|
+
return compareVersions(current, version) >= 0 ? null : version;
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
/** The pre-release among `tags` (the tags on one commit), if one was already staged from it. */
|
|
60
79
|
export function prereleaseAmong(tags, prefix = 'v') {
|
|
61
80
|
const prerelease = prereleasePattern(prefix);
|
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* exists=true when that commit already has a pre-release (a second run for one merge stages nothing)
|
|
8
8
|
* node scripts/package-release.mjs stable <pre-release tag> --prefix <tag prefix>
|
|
9
9
|
* the stable version and tag it becomes, and the commit it was staged from; stops if that stable exists
|
|
10
|
+
* node scripts/package-release.mjs next <stable> <patch|minor|major> --dir <path>
|
|
11
|
+
* the version to set <path>/package.json to after the stable, empty for none (WEB-39)
|
|
10
12
|
* Reads the tags with git, so run it in a checkout with its tags (fetch-depth: 0). Copied by `repos init`.
|
|
11
13
|
*/
|
|
12
14
|
import { execFileSync } from 'node:child_process';
|
|
13
15
|
import { readFileSync } from 'node:fs';
|
|
14
16
|
import { join } from 'node:path';
|
|
15
17
|
import { parseArgs } from 'node:util';
|
|
16
|
-
import { nextPrerelease, prereleaseAmong, stableOf } from './lib/package-release.js';
|
|
18
|
+
import { nextPrerelease, nextVersion, prereleaseAmong, stableOf } from './lib/package-release.js';
|
|
17
19
|
|
|
18
20
|
const { positionals, values: o } = parseArgs({
|
|
19
21
|
allowPositionals: true,
|
|
@@ -41,9 +43,15 @@ try {
|
|
|
41
43
|
throw new Error(`${o.prefix}${version} is already released. Pick a newer pre-release.`);
|
|
42
44
|
const [commit] = git('rev-list', '-n', '1', `refs/tags/${tag}`);
|
|
43
45
|
console.log(`version=${version}\ntag=${o.prefix}${version}\ncommit=${commit}`);
|
|
46
|
+
} else if (command === 'next' && tag) {
|
|
47
|
+
const current = JSON.parse(readFileSync(join(o.dir, 'package.json'), 'utf8')).version;
|
|
48
|
+
const version = nextVersion(tag, positionals[2] ?? 'patch', current);
|
|
49
|
+
if (!version && positionals[2] && positionals[2] !== 'patch')
|
|
50
|
+
console.error(`package.json already says ${current}, at or past the next ${positionals[2]} after ${tag}.`);
|
|
51
|
+
console.log(`version=${version ?? ''}`);
|
|
44
52
|
} else {
|
|
45
53
|
throw new Error(
|
|
46
|
-
'Usage: package-release.mjs prerelease --dir <path> --prefix <p> [--sha <commit>] | stable <tag> --prefix <p>',
|
|
54
|
+
'Usage: package-release.mjs prerelease --dir <path> --prefix <p> [--sha <commit>] | stable <tag> --prefix <p> | next <stable> <patch|minor|major> --dir <path>',
|
|
47
55
|
);
|
|
48
56
|
}
|
|
49
57
|
} catch (error) {
|
package/scripts/tasks/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ export const SUBCOMMANDS = {
|
|
|
14
14
|
horizon: ['close'],
|
|
15
15
|
hook: ['session', 'wait'],
|
|
16
16
|
peloton: ['checkin', 'step', 'reply'],
|
|
17
|
+
specs: ['list', 'show'],
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
/** Commands that take nothing after their name, so a word there is a mistake (an old copy's missing subcommand, say). */
|
|
@@ -48,17 +49,18 @@ export function unknownSubcommand(command, first) {
|
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* What a CLI says about where it runs from, or null to say nothing (BRK-7). The CLI ships on npm, so `packaged` (run
|
|
51
|
-
* through npx) has nothing to say. In a checkout of the board's own repository, `
|
|
52
|
-
*
|
|
53
|
-
* works while the API stays compatible, and on each run it says
|
|
52
|
+
* through npx) has nothing to say. In a checkout of the board's own repository, `behind` (the board's `release`, its
|
|
53
|
+
* X-Tasks-Release header, isn't in this checkout's history: releaseBehind) means pull (BRK-148). Anywhere else the
|
|
54
|
+
* CLI is an old copy that `repos init` used to commit: it works while the API stays compatible, and on each run it says
|
|
55
|
+
* how to switch. `own` and `board` are the frozen CLI number (src/cli-version.js) such a copy carries and the board sends.
|
|
54
56
|
*/
|
|
55
|
-
export function staleCliWarning({ own, board, boardCheckout, slug, packaged = false }) {
|
|
57
|
+
export function staleCliWarning({ own, board, boardCheckout, slug, packaged = false, release = null, behind = false }) {
|
|
56
58
|
if (packaged) return null;
|
|
57
59
|
const theirs = Number(board);
|
|
58
60
|
const older = Number.isInteger(theirs) && theirs > own;
|
|
59
61
|
if (boardCheckout) {
|
|
60
|
-
if (!
|
|
61
|
-
return `this checkout
|
|
62
|
+
if (!release || !behind) return null;
|
|
63
|
+
return `this checkout is behind the board's release (v${release}), so a command may be missing or behave differently: pull the default branch to update it.`;
|
|
62
64
|
}
|
|
63
65
|
const newer = older ? `, older than the board's (${theirs}), so a command may be missing or behave differently` : '';
|
|
64
66
|
return `this checkout carries a copy of the board's CLI (version ${own}${newer}). The CLI is on npm now: run it as npx ${CLI_PACKAGE} <command> instead of node scripts/tasks.mjs, and remove the copy with npx ${CLI_PACKAGE} repos init ${slug || '<slug>'} --update, which opens a pull request here.`;
|
|
@@ -76,6 +78,22 @@ export function githubRequest(repo, { sync = false } = {}) {
|
|
|
76
78
|
return ['GET', repo ? `github?repo=${encodeURIComponent(repo)}` : 'github', undefined];
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Whether the checkout `git` runs in is behind the board's release `release` (BRK-148): it has the release's tag
|
|
83
|
+
* (`v1.4.0-main.9`, which the release workflow pushes) and that commit isn't in HEAD's history. Without the tag (tags
|
|
84
|
+
* not fetched yet), or in a `shallow` clone, whose cut history can hide an ancestor, it can't tell, and says no.
|
|
85
|
+
* `git(args)` runs git and returns its exit code.
|
|
86
|
+
* @param {string | null} release
|
|
87
|
+
* @param {(args: string[]) => number | null} git
|
|
88
|
+
* @param {{ shallow?: boolean }} [options]
|
|
89
|
+
*/
|
|
90
|
+
export function releaseBehind(release, git, { shallow = false } = {}) {
|
|
91
|
+
if (shallow || !release || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(release)) return false;
|
|
92
|
+
const tag = `refs/tags/v${release}`;
|
|
93
|
+
if (git(['rev-parse', '-q', '--verify', `${tag}^{commit}`]) !== 0) return false;
|
|
94
|
+
return git(['merge-base', '--is-ancestor', tag, 'HEAD']) === 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
/** What `github fix` accepts for --problem: the same three the pull request page offers. */
|
|
80
98
|
export const FIX_PROBLEMS = ['conflicts', 'failing', 'review'];
|
|
81
99
|
|
|
@@ -104,6 +122,30 @@ export function pullAgentRequest(action, number, { repo = null, problem, note, f
|
|
|
104
122
|
return { request: ['POST', `github/pulls/${n}/${action}`, body] };
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
/**
|
|
126
|
+
* `npx breakaway github release <pre-release> [--next patch|minor|major]` (BRK-103, WEB-39): the owner releases a
|
|
127
|
+
* package's pre-release as its stable, as Release on the GitHub page does, with what the default branch works toward
|
|
128
|
+
* next. The board starts the repository's release.yml stable job, and npm waits for the owner's 2FA; it refuses an
|
|
129
|
+
* agent, so the request always says who asks, and refuses a pre-release whose stable is already out (409).
|
|
130
|
+
* @param {string | undefined} version the pre-release, like 1.4.0-main.5 (or its tag)
|
|
131
|
+
* @param {{ repo?: string | null, by?: string, next?: string | null }} [options]
|
|
132
|
+
* @returns {{ error?: string, request?: [string, string, Record<string, string>] }}
|
|
133
|
+
*/
|
|
134
|
+
export function packageReleaseRequest(version, { repo = null, by, next = null } = {}) {
|
|
135
|
+
const v = String(version ?? '').trim();
|
|
136
|
+
if (!/^(?:\S+@|v)?\d+\.\d+\.\d+-main\.\d+$/u.test(v))
|
|
137
|
+
return { error: 'say which pre-release: npx breakaway github release <version>, like 1.4.0-main.5' };
|
|
138
|
+
if (next !== null && next !== undefined && !['patch', 'minor', 'major'].includes(String(next)))
|
|
139
|
+
return { error: '--next is patch, minor, or major' };
|
|
140
|
+
return {
|
|
141
|
+
request: [
|
|
142
|
+
'POST',
|
|
143
|
+
'github/release',
|
|
144
|
+
{ version: v, ...(next ? { next: String(next) } : {}), ...(repo ? { repo } : {}), ...(by ? { by } : {}) },
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
107
149
|
export const REVIEW_VERDICTS = ['ready', 'follow-up', 'changes'];
|
|
108
150
|
|
|
109
151
|
/**
|
|
@@ -148,17 +190,27 @@ export function forceFields(force, by) {
|
|
|
148
190
|
* With `decision` (`agents new --decision <ID> ["<note>"]`, BRK-110) the board writes the prompt from that answered
|
|
149
191
|
* decision, in the decision's repository, and the text is the owner's note under it. With `next`
|
|
150
192
|
* (`agents new --next minor|major ["<note>"]`, BRK-100) it writes the prompt that sets the repository's next version.
|
|
193
|
+
* With `spec` (`agents new --spec <path> "<what should change>"`, BRK-121) it writes the prompt that refines that spec
|
|
194
|
+
* and the tasks that link it, and the text, required, is what should change.
|
|
151
195
|
* @param {string} prompt
|
|
152
|
-
* @param {{ repo?: string | null, force?: boolean, by?: string, decision?: string | null, next?: string | null }} [options]
|
|
196
|
+
* @param {{ repo?: string | null, force?: boolean, by?: string, decision?: string | null, next?: string | null, spec?: string | null }} [options]
|
|
153
197
|
*/
|
|
154
|
-
export function generalAgentRequest(
|
|
198
|
+
export function generalAgentRequest(
|
|
199
|
+
prompt,
|
|
200
|
+
{ repo = null, force = false, by, decision = null, next = null, spec = null } = {},
|
|
201
|
+
) {
|
|
155
202
|
const text = String(prompt ?? '').trim();
|
|
156
|
-
if (decision
|
|
203
|
+
if ([decision, next, spec].filter(Boolean).length > 1)
|
|
204
|
+
return { error: 'start one from --decision, --spec, or --next: only one of them' };
|
|
157
205
|
if (next && !['minor', 'major'].includes(next))
|
|
158
206
|
return { error: 'patches count by themselves: --next minor or --next major' };
|
|
207
|
+
if (spec && !text)
|
|
208
|
+
return {
|
|
209
|
+
error: 'say what should change in the spec: npx breakaway agents new --spec <path> "<what should change>"',
|
|
210
|
+
};
|
|
159
211
|
if (!text && !decision && !next)
|
|
160
212
|
return { error: 'say what the agent should do: npx breakaway agents new "Tidy the docs" [--image <file>]' };
|
|
161
|
-
const board = decision ? { decision } : next ? { next } : null;
|
|
213
|
+
const board = decision ? { decision } : next ? { next } : spec ? { spec: specPath(spec) } : null;
|
|
162
214
|
const body = {
|
|
163
215
|
...(board ? { ...board, ...(text ? { note: text } : {}) } : { prompt: text }),
|
|
164
216
|
...(repo ? { repo } : {}),
|
|
@@ -170,18 +222,54 @@ export function generalAgentRequest(prompt, { repo = null, force = false, by, de
|
|
|
170
222
|
|
|
171
223
|
/**
|
|
172
224
|
* What the CLI says about a general agent's answer: the task and that it started, or why it waits (and whether Force
|
|
173
|
-
* start could skip that), or, from a decision
|
|
225
|
+
* start could skip that), or, from a decision, for the next version (`next`), or on a spec (`spec`), the open one
|
|
226
|
+
* that already has it.
|
|
174
227
|
* @param {{ task: { wid?: string, short?: string }, run?: { url?: string, agent?: string } | null, waiting?: string | null, forceable?: boolean, already?: string | null }} answer
|
|
175
|
-
* @param {{ next?: string | null }} [options]
|
|
228
|
+
* @param {{ next?: string | null, spec?: unknown }} [options]
|
|
176
229
|
*/
|
|
177
|
-
export function generalAgentSummary({ task, run, waiting, forceable, already }, { next = null } = {}) {
|
|
230
|
+
export function generalAgentSummary({ task, run, waiting, forceable, already }, { next = null, spec = null } = {}) {
|
|
178
231
|
const id = task.wid ?? task.short;
|
|
179
|
-
if (!run && already)
|
|
180
|
-
|
|
232
|
+
if (!run && already) {
|
|
233
|
+
const what = next ? 'prepares the next version' : spec ? 'refines this spec' : 'refines from these answers';
|
|
234
|
+
return `${id} already ${what}: ${already}.`;
|
|
235
|
+
}
|
|
181
236
|
if (run) return `Started ${run.agent ? `${run.agent} ` : 'an agent '}on ${id}${run.url ? `: ${run.url}` : ''}`;
|
|
182
237
|
return `Saved ${id}, waiting to start: ${waiting ?? 'no room yet'}.${forceable ? ` Start it now past the board's limits: npx breakaway agents start ${id} --force` : ''}`;
|
|
183
238
|
}
|
|
184
239
|
|
|
240
|
+
/** A spec's path as the board reads it: no leading `./`, no doubled or trailing slashes. */
|
|
241
|
+
const specPath = (path) =>
|
|
242
|
+
String(path ?? '')
|
|
243
|
+
.trim()
|
|
244
|
+
.replace(/^(\.\/)+/u, '')
|
|
245
|
+
.split('/')
|
|
246
|
+
.filter((part) => part && part !== '.')
|
|
247
|
+
.join('/');
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The request behind `npx breakaway specs [list]` (BRK-121): the specs of the checkout's repository, or the one `--repo`
|
|
251
|
+
* names; without either, the board answers with its default repository's.
|
|
252
|
+
* @param {string | null} repo
|
|
253
|
+
* @returns {[string, string, undefined]}
|
|
254
|
+
*/
|
|
255
|
+
export function specsRequest(repo) {
|
|
256
|
+
return ['GET', repo ? `specs?repo=${encodeURIComponent(repo)}` : 'specs', undefined];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The request behind `npx breakaway specs show <path>` (BRK-121): one spec, by its path in the repository. The board
|
|
261
|
+
* refuses a path outside the specs directory; one that climbs out with `..` is refused here first.
|
|
262
|
+
* @param {string | undefined} path
|
|
263
|
+
* @param {string | null} repo
|
|
264
|
+
*/
|
|
265
|
+
export function specRequest(path, repo) {
|
|
266
|
+
const clean = specPath(path);
|
|
267
|
+
if (!clean) return { error: 'say which spec: npx breakaway specs show <path>, like docs/specs/BRK-1-thing.md' };
|
|
268
|
+
if (clean.split('/').includes('..')) return { error: `${clean.slice(0, 200)} climbs out of the repository` };
|
|
269
|
+
const query = repo ? `?repo=${encodeURIComponent(repo)}` : '';
|
|
270
|
+
return { request: ['GET', `specs/${clean.split('/').map(encodeURIComponent).join('/')}${query}`, undefined] };
|
|
271
|
+
}
|
|
272
|
+
|
|
185
273
|
/**
|
|
186
274
|
* What the CLI says about an answer to those requests: which task and agent took the pull request, or who already has it.
|
|
187
275
|
* @param {'fix' | 'review'} action
|
|
@@ -409,3 +497,78 @@ export function chaseSummary(slug, { dryRun, chase, started = [], wouldStart = [
|
|
|
409
497
|
}
|
|
410
498
|
return [first, '', ...chaseLines(chase, slug)].join('\n');
|
|
411
499
|
}
|
|
500
|
+
|
|
501
|
+
/** A spec's tasks in a few words: "3 tasks, 2 open", or "no tasks". */
|
|
502
|
+
const specTaskCount = (tasks = []) => {
|
|
503
|
+
if (!tasks.length) return 'no tasks';
|
|
504
|
+
const open = tasks.filter((t) => t.status === 'pending').length;
|
|
505
|
+
return `${plural(tasks.length, 'task')}, ${open} open`;
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* What `npx breakaway specs` prints: the repository's specs newest first, each with its work ID, status, title, and its
|
|
510
|
+
* tasks' count; with none, where specs go and how to point the board at another directory.
|
|
511
|
+
* @param {{ slug: string, dir: string, missing?: boolean, readme?: { path: string } | null, specs: any[] }} answer
|
|
512
|
+
*/
|
|
513
|
+
export function specListLines({ slug, dir, missing, readme, specs }) {
|
|
514
|
+
if (!specs.length)
|
|
515
|
+
return [
|
|
516
|
+
`No specs in ${dir} yet${missing ? `: ${slug} has no ${dir} on its default branch` : ''}.`,
|
|
517
|
+
'A spec is a Markdown file in that directory, merged like any change.',
|
|
518
|
+
`If ${slug} keeps its specs somewhere else, the owner sets it with npx breakaway repos modify ${slug} --specs <dir>.`,
|
|
519
|
+
];
|
|
520
|
+
const intro = readme ? ` (its introduction is ${readme.path})` : '';
|
|
521
|
+
const out = [`${slug}: ${plural(specs.length, 'spec')} in ${dir}${intro}`, ''];
|
|
522
|
+
for (const s of specs) {
|
|
523
|
+
const extra = s.tooLarge ? ', over 1 MB: read it on GitHub' : '';
|
|
524
|
+
out.push(
|
|
525
|
+
` ${(s.wid ?? '').padEnd(9)} ${(s.status ?? '-').padEnd(10)} ${s.title} (${specTaskCount(s.tasks)}${extra})`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
out.push('', `Read one: npx breakaway specs show <path>, like ${specs[0].path}`);
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* What `npx breakaway specs show <path>` prints: the spec's title and path, status, the commit that last changed it,
|
|
534
|
+
* its GitHub link, its Markdown (or, over 1 MB, a pointer to GitHub), and the tasks that link it.
|
|
535
|
+
* @param {any} spec
|
|
536
|
+
*/
|
|
537
|
+
export function specLines(spec) {
|
|
538
|
+
const out = [`${spec.title} (${spec.path})`, ''];
|
|
539
|
+
const row = (k, v) => v && out.push(` ${k.padEnd(11)} ${v}`);
|
|
540
|
+
row('Status', spec.status);
|
|
541
|
+
const c = spec.commit;
|
|
542
|
+
if (c)
|
|
543
|
+
row(
|
|
544
|
+
'Changed',
|
|
545
|
+
`${c.date ? `${String(c.date).slice(0, 16).replace('T', ' ')} ` : ''}in ${String(c.sha).slice(0, 7)}${c.message ? `: ${c.message}` : ''}`,
|
|
546
|
+
);
|
|
547
|
+
row('GitHub', spec.url);
|
|
548
|
+
out.push('');
|
|
549
|
+
if (spec.tooLarge || spec.text === null || spec.text === undefined)
|
|
550
|
+
out.push('Over 1 MB, too large to show here: read it on GitHub.');
|
|
551
|
+
else out.push(String(spec.text).replace(/\s+$/u, ''));
|
|
552
|
+
const tasks = spec.tasks ?? [];
|
|
553
|
+
out.push('');
|
|
554
|
+
if (tasks.length) {
|
|
555
|
+
out.push(` Tasks (${tasks.length}, ${tasks.filter((t) => t.status === 'pending').length} open)`);
|
|
556
|
+
for (const t of tasks) out.push(` ${idOf(t).padEnd(9)} ${t.status.padEnd(9)} ${t.description}`);
|
|
557
|
+
} else out.push(` No task links it yet: npx breakaway modify <ref> --spec ${spec.path}`);
|
|
558
|
+
out.push('', `Refine it: npx breakaway agents new --spec ${spec.path} "<what should change>"`);
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* What's left by hand once `repos remove` took a repository off the board (CLI-4): the board can't delete its routine
|
|
564
|
+
* on claude.ai, connected or not, nor the repository on GitHub. `repos remove` ends with these, and `--json` carries
|
|
565
|
+
* them as `byHand`.
|
|
566
|
+
* @param {string} github the repository, as owner/name
|
|
567
|
+
* @returns {string[]}
|
|
568
|
+
*/
|
|
569
|
+
export function removedRepoByHand(github) {
|
|
570
|
+
return [
|
|
571
|
+
`Delete its routine on claude.ai, with its API trigger: open claude.ai/code/routines, then the routine for ${github}. The board can't delete it, whether or not it was connected.`,
|
|
572
|
+
`Made ${github} only for a rehearsal? Delete it on GitHub too (Settings, then Danger zone; or gh auth refresh -h github.com -s delete_repo once, then gh repo delete ${github}), and your local clone.`,
|
|
573
|
+
];
|
|
574
|
+
}
|