breakaway 1.4.0-main.51 → 1.4.0-main.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakaway",
3
- "version": "1.4.0-main.51",
3
+ "version": "1.4.0-main.53",
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",
@@ -25,7 +25,7 @@ The title is the work ID and a plain sentence (`BRK-12: Sort the inbox by age`),
25
25
 
26
26
  ## Direction
27
27
 
28
- `AGENTS.md`'s "What breakaway is, and isn't" are settled: free and self-hosted, an install keeps its data, people merge and deploy, and the claims in `brand/README.md` stay true. An idea that breaks one needs the owner's decision first. Look at the board for the horizons (`tasks list --json`) and for tasks the work overlaps. For a technical change with real choices, settle it in a spec; for anything people will use, shape the flow and its empty, error, and first-run states in the spec too. Specs go in `docs/specs/<ID>-<slug>.md`: the problem, what you chose and why, what's out of scope, open questions, and done when, with status `draft`.
28
+ `AGENTS.md`'s "What breakaway is, and isn't" are settled: free and self-hosted, an install keeps its data, people merge and deploy, and the claims in `brand/README.md` stay true. An idea that breaks one needs the owner's decision first. Look at the board for the horizons (`tasks list --json`) and for tasks the work overlaps. For a technical change with real choices, settle it in a spec; for anything people will use, shape the flow and its empty, error, and first-run states in the spec too. Specs go in `docs/specs/<ID>-<slug>.md`: the problem, what you chose and why, what's out of scope, open questions, done when, and how to check it, with status `draft`.
29
29
 
30
30
  ## Dependency updates
31
31
 
package/prompts/core.md CHANGED
@@ -33,8 +33,8 @@ The idea is the task's description, in the owner's own words. Never rewrite it:
33
33
 
34
34
  1. **Understand it.** If the payload has an `Attachments: <n>` line (or `show` lists images), look at the images first: `tasks attachments <the task> --save <a folder in your scratch space, not the repository>`, then `Read` each file. Each image's caption is what the owner wants you to notice. The line is context, like the owner's note. Read the idea and what the repository's **Direction** lists (its principles, settled decisions, and what it isn't doing), and use the skills it names for shaping. Look at the board (`tasks list --json`) for tasks that already cover part of it, tasks it overlaps with, and tasks it must wait for. Search the code for what already exists.
35
35
  2. **Check it fits.** If the idea breaks a principle or a settled decision, or is something the repository isn't doing, don't turn it into agent work. Write the spec so it says so plainly, and ask the owner the question with a decision (see "Asking for a decision" below).
36
- 3. **Check in, then write the spec.** Check in on the peloton (step 4 above) with the spec and the areas the idea touches, before you write anything. Write the spec where the repository's **Direction** says specs go (as `<IDEA-ID>-<slug>.md`), from its template, with status `draft`. Say what the idea became, what you chose and why, what's out of scope, and any questions you couldn't settle. Describe in words what the images show where the spec needs them, since the images stay on the board; don't copy them into the repository unless the owner asks, and never repeat what the repository's **Never share** lists. Keep it as short as the idea allows. A small idea gets a short spec.
37
- 4. **Make the tasks.** One task per piece that one agent can finish in one pull request, with `tasks add "<title>" --project <area> --horizon <now|next|later> --tag agent|owner|decide --depends <IDs> --brief "<what and why>" --done-when "<what has to be true to call it done>"`. They go in your checkout's repository; a piece that belongs in another repository is a task there (`--repo <slug>` on `add`), named in the spec. Fill in every field on purpose:
36
+ 3. **Check in, then write the spec.** Check in on the peloton (step 4 above) with the spec and the areas the idea touches, before you write anything. Write the spec where the repository's **Direction** says specs go (as `<IDEA-ID>-<slug>.md`), from its template, with status `draft`. Say what the idea became, what you chose and why, what's out of scope, and any questions you couldn't settle, and end it with **How to check it**: a few steps someone who isn't technical can follow, once the work is built, to see the result is what they asked for (what to open, what to do, what they should see). Describe in words what the images show where the spec needs them, since the images stay on the board; don't copy them into the repository unless the owner asks, and never repeat what the repository's **Never share** lists. Keep it as short as the idea allows. A small idea gets a short spec.
37
+ 4. **Make the tasks.** One task per piece that one agent can finish in one pull request, with `tasks add "<title>" --project <area> --horizon <now|next|later> --tag agent|owner|decide --depends <IDs> --brief "<what and why>" --done-when "<what has to be true to call it done>"`, a done when the owner can check in a few minutes. They go in your checkout's repository; a piece that belongs in another repository is a task there (`--repo <slug>` on `add`), named in the spec. Fill in every field on purpose:
38
38
  - the area that fits the rest of the board (look at neighbouring tasks), and a priority only when the idea says it matters;
39
39
  - the horizon: if the idea has a tag `horizon-now`, `horizon-next`, or `horizon-later`, that's the owner's choice, so give every task exactly that horizon, and never change the tag. Only with `horizon-auto` do you choose, task by task, from the neighbouring tasks and the repository's horizons;
40
40
  - `--tag agent` for work an agent can do in the repository, `--tag owner` for production, dashboards, accounts, and sign-offs, and `--tag decide` when the owner has to choose first;
@@ -42,7 +42,7 @@ The idea is the task's description, in the owner's own words. Never rewrite it:
42
42
  - `--spec <path>` on the main task;
43
43
  - one feature tag on every task, not a release tag: if the idea's tasks belong together, add the feature first (`tasks features add <slug> --title "<name>"`, with no release: aiming it at one is the owner's) and give each task `--tag <slug>`; if they join a feature already on the board (`tasks features`), use its slug.
44
44
  Never set `--autostart`, never start an agent or a chase on a task or feature you made. Whether a task starts by itself is the owner's choice, made on the board, and the idea's own setting is not yours to copy.
45
- 5. **Hand over.** On the idea, `comment` the IDs you made and what each waits for, and `modify` nothing else about it (not its description). Open the pull request as the repository's **Pull requests** says: the title is `<IDEA-ID>: Shape <the idea in a few words>`, the description lists the new tasks and their blockers, and it ends with "Closes <IDEA-ID>." Then `modify <IDEA-ID> --pr <number>` and keep watching the pull request as in step 8.
45
+ 5. **Hand over.** On the idea, `comment` the IDs you made and what each waits for, and `modify` nothing else about it (not its description). Open the pull request as the repository's **Pull requests** says: the title is `<IDEA-ID>: Shape <the idea in a few words>`, the description lists the new tasks and their blockers, repeats the spec's **How to check it** under a `## How to check it` heading, and ends with "Closes <IDEA-ID>." Then `modify <IDEA-ID> --pr <number>` and keep watching the pull request as in step 8.
46
46
 
47
47
  The pull request holds only the spec. The tasks already exist on the board, waiting for it to merge.
48
48
 
@@ -58,7 +58,7 @@ The owner kicked off a new project from the board: the task is its `IDEA-`, tagg
58
58
 
59
59
  Write every `prompt` in everyday words, one idea per question, with options rather than open text where they work and your recommendation first, marked "(recommended)". A technical term goes only in `help`, explained in a line. Don't ask what the pitch already answers. Then `comment` what you asked and why, `release <the task>`, and stop: the owner answers on the board, and their **Send answers and carry on** starts the next run.
60
60
  3. **Ask once more, only if you must.** If something important is still open after the first round's answers, ask a second round about only that, at most 6 questions, the same way, and stop. Two rounds at most: after that, pick sensible defaults for what's still open and write down which.
61
- 4. **Plan it.** Once the answers settle it, check in on the peloton (step 4 above) with the spec, `AGENTS.md`, and the prompt, then shape the idea as "Shaping an idea" above says, with three additions, all in one pull request in this repository: `AGENTS.md` says how to build, test, and check the chosen stack; the repository's agent prompt replaces its default sections (**Building**, **Checks**, **Pull requests**, **Direction**, and the rest) with what this project needs; and the spec opens with **In short**, a few plain sentences someone who isn't technical can check against what they asked for, naming the stack Pick for me chose or the one you recommended. The pull request's description starts with the same sentences under an `## In short` heading: the kickoff's page on the board quotes them. Every task you add depends on the IDEA and carries one feature named for the first version, `<the repository's slug>-v1` (add it with `features add`, without a release); the first task sets up the stack, so building stays tasks. The pull request closes the IDEA; `modify <the task> --pr <number>` and watch it as in step 8.
61
+ 4. **Plan it.** Once the answers settle it, check in on the peloton (step 4 above) with the spec, `AGENTS.md`, and the prompt, then shape the idea as "Shaping an idea" above says, with three additions, all in one pull request in this repository: `AGENTS.md` says how to build, test, and check the chosen stack; the repository's agent prompt replaces its default sections (**Building**, **Checks**, **Pull requests**, **Direction**, and the rest) with what this project needs; and the spec opens with **In short**, a few plain sentences someone who isn't technical can check against what they asked for, naming the stack Pick for me chose or the one you recommended. The pull request's description starts with the same sentences under an `## In short` heading (the kickoff's page on the board quotes them), followed by the spec's **How to check it**, which says how the owner will see the first version is what they asked for. The first task's done when is one the owner can check in a few minutes. Every task you add depends on the IDEA and carries one feature named for the first version, `<the repository's slug>-v1` (add it with `features add`, without a release); the first task sets up the stack, so building stays tasks. The pull request closes the IDEA; `modify <the task> --pr <number>` and watch it as in step 8.
62
62
 
63
63
  Never start an agent, a chase, or a deploy for the project, and never set `--autostart`: the owner merges the plan and starts the building from the board.
64
64
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Before github-connect trades its code: whether the board already has a GitHub App (CLI-2). Each code comes
3
+ * from a new App on GitHub, so a second run on a board that has one would overwrite the keys of the App that works
4
+ * with a second App's. Read from the board's Connections report (`GET /api/connections`, its `github.app`
5
+ * row). Pure, so it's tested without a board.
6
+ */
7
+
8
+ /**
9
+ * `ok` false stops the command before the code is traded (it still works, within its hour); `note` is what
10
+ * a run that goes ahead says first.
11
+ *
12
+ * @param {{ connections?: { id: string, state: string, detail?: string, at?: string | null, link?: string | null }[] } | null | undefined} report
13
+ * @param {{ replace: boolean }} options
14
+ * @returns {{ ok: boolean, message?: string, note?: string }}
15
+ */
16
+ export function appInPlace(report, { replace }) {
17
+ const row = Array.isArray(report?.connections) ? report.connections.find((c) => c.id === 'github.app') : undefined;
18
+ if (!row)
19
+ return replace
20
+ ? {
21
+ ok: true,
22
+ note: "Couldn't ask the board whether it already has a GitHub App; connecting a new one (--replace).",
23
+ }
24
+ : {
25
+ ok: false,
26
+ message:
27
+ "couldn't ask the board whether it already has a GitHub App, so nothing was changed; the code still works within its hour. Check the board answers (npx breakaway connections), then run this again; or add --replace to connect a new App whatever is there.",
28
+ };
29
+ if (row.state === 'off') return { ok: true };
30
+ const what = `${row.detail}${row.link ? `, ${row.link}` : ''}`;
31
+ const deleteOld = `Once the new one works, delete the old one on GitHub (Settings → Developer settings → GitHub Apps)${row.link ? `: ${row.link}` : ''}.`;
32
+ if (replace) return { ok: true, note: `Replacing the board's GitHub App (${what}). ${deleteOld}` };
33
+ // Checked, and GitHub refused it: it doesn't work, so a new one takes its place.
34
+ if (row.state === 'attention' && row.at)
35
+ return {
36
+ ok: true,
37
+ note: `The board's GitHub App doesn't work (${row.detail}); connecting the new one in its place. ${deleteOld}`,
38
+ };
39
+ const kept =
40
+ "Nothing was changed: this code's App would replace that one. If you made the new App by mistake, delete it on GitHub (Settings → Developer settings → GitHub Apps). To switch the board to it, run npx breakaway github-connect <code> --replace within the code's hour, then delete the old one.";
41
+ if (row.state === 'working')
42
+ return {
43
+ ok: false,
44
+ message: `the board already has a working GitHub App: ${what}. ${kept}`,
45
+ };
46
+ return {
47
+ ok: false,
48
+ message: `the board already has a GitHub App's keys (${what}); press Check now on its Connections view to see whether it works. ${kept}`,
49
+ };
50
+ }
package/scripts/tasks.mjs CHANGED
@@ -42,6 +42,7 @@ import { looksLikeSecret } from '../src/ping.js';
42
42
  import { promptPathOf } from '../src/repos.js';
43
43
  import { hookFailure, sessionProxy, routeThroughSessionProxy } from './tasks/proxy.js';
44
44
  import { githubFromRemote, inRepo, pickRepo } from './tasks/repo.js';
45
+ import { appInPlace } from './tasks/github-connect.js';
45
46
  import { checkInstall } from './tasks/install-check.js';
46
47
  import { NO_TERMINAL, ask as askIn } from './tasks/ask.js';
47
48
  import {
@@ -296,10 +297,11 @@ Setup (owner)
296
297
  setup connect this machine's Taskwarrior (writes taskrc in this machine's folder for the board, with every repository's report and context)
297
298
  rotate-sync new client ID and sync secret; the server re-encrypts its history
298
299
  rotate-token new API token; every browser is signed out
299
- github-connect <code> store the GitHub App's keys (the board's GitHub view gives the code)
300
+ github-connect <code> store the GitHub App's keys (the board's GitHub view gives the code); refuses when the board
301
+ already has an App, unless --replace
300
302
  agents-connect store the agent routine's URL and token (docs/tasks.md#cloud-agents-from-the-board)
301
303
  --repo <slug> connects another repository's routine; --replace drops ones this machine doesn't hold
302
- init-secrets once, for a brand-new board
304
+ init-secrets once, for a brand-new board; --force starts again, keeping the old file as a .bak
303
305
 
304
306
  Install repository (no board needed: the files and steps that deploy a board from its own repository)
305
307
  install init [dir] write an install repository into <dir> (default: here): its config, breakaway.json, the Deploy and
@@ -1727,6 +1729,12 @@ const commands = {
1727
1729
  const code = need(args[0], 'code');
1728
1730
  // Before the code is traded: it works once, and the keys it gives have to go into the board's own secrets.
1729
1731
  await ensureBoardInstall('github-connect');
1732
+ // Each code comes from a new App: on a board that has one, it would replace the working App's keys (CLI-2).
1733
+ const inPlace = appInPlace(await call('GET', 'connections', undefined, { soft: true }), {
1734
+ replace: Boolean(opts.replace),
1735
+ });
1736
+ if (!inPlace.ok) fail(inPlace.message);
1737
+ if (inPlace.note) console.log(inPlace.note);
1730
1738
  const res = await fetch(`https://api.github.com/app-manifests/${enc(code)}/conversions`, {
1731
1739
  method: 'POST',
1732
1740
  headers: {
@@ -1775,6 +1783,9 @@ const commands = {
1775
1783
  const install = installConfig();
1776
1784
  // A new install has no address until it's deployed (CLD-139).
1777
1785
  const known = BOARD.from !== 'default' || install.url !== null;
1786
+ // --force never loses the only copy of the sync secret: the old file is kept the way rotations keep it (CLI-2).
1787
+ const kept = backupEnvFile();
1788
+ if (kept) console.log(`Kept the old one as ${kept} (0600).`);
1778
1789
  writePrivate(
1779
1790
  ENV_FILE,
1780
1791
  envFile({
@@ -2226,9 +2237,18 @@ function writePrivate(path, content) {
2226
2237
  chmodSync(path, 0o600);
2227
2238
  }
2228
2239
 
2240
+ /** Moves tasks.env aside as tasks.env.<date>.bak (0600), and says where; null when there's none. */
2241
+ function backupEnvFile() {
2242
+ if (!existsSync(ENV_FILE)) return null;
2243
+ const backup = `${ENV_FILE}.${new Date().toISOString().replace(/[:.]/gu, '-')}.bak`;
2244
+ renameSync(ENV_FILE, backup);
2245
+ chmodSync(backup, 0o600);
2246
+ return backup;
2247
+ }
2248
+
2229
2249
  /** tasks.env.next becomes tasks.env; the old one is kept as tasks.env.<date>.bak. */
2230
2250
  function promoteEnvFile() {
2231
- if (existsSync(ENV_FILE)) renameSync(ENV_FILE, `${ENV_FILE}.${new Date().toISOString().replace(/[:.]/gu, '-')}.bak`);
2251
+ backupEnvFile();
2232
2252
  renameSync(`${ENV_FILE}.next`, ENV_FILE);
2233
2253
  }
2234
2254