breakaway 1.4.0-main.34 → 1.4.0-main.36

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.
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: pipeline
3
+ description: Use when a task asks to move a repository's CI/CD to breakaway's deploy flow or release flow (Move to breakaway's deploy flow, Deploy with breakaway, Release with breakaway), to write or change .github/breakaway-pipeline.json, to run npx breakaway pipeline init or pipeline check, or to map an old deploy or npm publish workflow onto Deploy, Promote, Roll back, and Release.
4
+ ---
5
+
6
+ # Moving a repository to the deploy flow
7
+
8
+ A move is one task and one pull request: you read how the repository checks, deploys, and publishes today, write `.github/breakaway-pipeline.json`, render the workflows from it with `npx breakaway pipeline init`, and account for every step of the old setup, so nothing is lost and nothing runs twice. The design is the [move's spec](../../../docs/specs/IDEA-27-move-ci-cd-to-the-deploy-flow.md) (sections 2, 2b, and 3), and the [docs](../../../docs/tasks.md#moving-a-repository-to-the-deploy-flow) say what the flows do.
9
+
10
+ It is an ordinary build: claim, check in on the peloton, and hand over the way the repository's prompt and the `tasks` skill say. This skill is how to do the move itself.
11
+
12
+ **You never** deploy, publish, run or re-run a workflow, touch Cloudflare, npm, or the repository's GitHub settings (environments, secrets, variables, the App's permissions), or set the repository's pipeline on the board (`repos modify --pipeline` and **Turn on deploys** are the owner's). You write files in a pull request; the owner does the rest.
13
+
14
+ ## 1. Read what's there
15
+
16
+ Read all of it before you decide anything:
17
+
18
+ - **Workflows**, `.github/workflows/*.yml`: each one's `name:` line, its triggers (`push`, `pull_request`, `workflow_run`, `release`, `workflow_dispatch`, `schedule`), and every job and step. Note which steps check (lint, test, build, typecheck), which deploy (`wrangler deploy`, `wrangler versions upload`, `cloudflare/wrangler-action`), which publish (`npm publish`, or pnpm's or yarn's, `JS-DevTools/npm-publish`, a `release` event), and which do something else (a migration, a cache purge, a notification, a manual approval through an `environment` with reviewers).
19
+ - **Wrangler config**, `wrangler.jsonc`, `wrangler.json`, or `wrangler.toml`: the Worker's `name`, its `env` blocks (each one's name, and the Worker name it deploys as), and its bindings, above all D1 databases and their `migrations_dir`.
20
+ - **Migrations**: the folder and how they're applied today (`wrangler d1 migrations apply`, a script).
21
+ - **`package.json`**: `name`, `version`, `private`, `scripts` (`build`, `test`, `deploy`, `release`, `prepublishOnly`), `publishConfig`, and `workspaces`; and the lockfile, which says the install command (`npm ci` for `package-lock.json`, or pnpm's or yarn's frozen install for theirs).
22
+ - **Versioning tools**: `.changeset/`, `release-please-config.json`, `.releaserc*` or a `release` key in `package.json` (semantic-release), and `lerna.json`.
23
+ - **Anything already at the flow's paths**: `.github/breakaway-pipeline.json`, `.github/deploy-paths.json`, and `deploy.yml`, `promote.yml`, `rollback.yml`, or `release.yml` in `.github/workflows/`.
24
+ - **Scripts the workflows run**, in `scripts/` or `package.json`: what each does, so you know whether `beforeDeploy` can run it.
25
+
26
+ ## 2. Decide what moves
27
+
28
+ | The repository today | What you do |
29
+ | --- | --- |
30
+ | Deploys a Worker with `wrangler`, from Actions, Workers Builds, or by hand | Move it: `workers` in the config. |
31
+ | Has checks but deploys by hand | Move it: the deploy is new, and the checks stay as they are. |
32
+ | Publishes a package to npm, from Actions or by hand | Move it: `package` in the config. |
33
+ | Deploys a Worker and publishes a package | Both, in one config and one pull request. |
34
+ | Publishes to another registry (GitHub Packages, JSR, PyPI) | That publishing stays as it is. Say so in the pull request; move a Worker deploy if there is one. |
35
+ | Deploys somewhere else (Pages, Vercel, Fly, a server, containers) | Stop (below). breakaway's deploy flow runs Cloudflare Workers only; a deploy command of the repository's own is planned, not built. |
36
+ | `package.json` says `"private": true` and nothing deploys | Stop: there is nothing the flows can take. |
37
+
38
+ The first version takes **one staging and one production Worker** and **one package** per repository. With more Workers, move the pair the task names (or that the old deploy targets on the default branch), and leave the others' deploys as they are, saying so. With several publishable packages (a monorepo), move the one the task names (or ask, below), and leave the others' publishing as it is.
39
+
40
+ **Stopping** means changing nothing and opening no pull request: `comment` on the task what you found and why it doesn't move (the row above, in a sentence the owner can act on), then `release` it. When only the owner can choose (which package, which Worker pair, or what to do with a versioning tool), ask with a decision instead (the core's "Asking for a decision") and release.
41
+
42
+ ## 3. Write the config
43
+
44
+ `.github/breakaway-pipeline.json` is the repository's own. Every value comes from what you read, never invented:
45
+
46
+ ```json
47
+ {
48
+ "workers": { "staging": "widgets-staging", "production": "widgets" },
49
+ "wranglerEnv": { "staging": "staging", "production": "production" },
50
+ "branch": "main",
51
+ "checks": ["CI"],
52
+ "install": "npm ci",
53
+ "build": "npm run build",
54
+ "beforeDeploy": ["npx wrangler d1 migrations apply DB --remote --env $BREAKAWAY_ENV"],
55
+ "deployPaths": { "widgets": "^(src|public|migrations)/|^wrangler\\.jsonc$|^package(-lock)?\\.json$" },
56
+ "healthCheck": { "staging": "https://widgets-staging.example.workers.dev/", "production": "https://widgets.example.com/" },
57
+ "package": { "name": "widgets", "directory": ".", "access": "public" }
58
+ }
59
+ ```
60
+
61
+ - **`workers`**: the staging and production Worker names, as the wrangler config deploys them (with its `env` blocks, the name each environment deploys as). Two different Workers. A repository with only production today needs a staging Worker: name it `<production>-staging`, and the owner creates it (the checklist).
62
+ - **`wranglerEnv`**: only when the wrangler config has `env` blocks; the environment name for each Worker, passed as `--env`.
63
+ - **`branch`**: the default branch, when it isn't `main`.
64
+ - **`checks`**: the `name:` line of each workflow that must pass before a deploy or a release, exactly as written. Every check the old deploy waited for, and the ones that run on push to the default branch. Never a workflow you're about to remove.
65
+ - **`install`, `build`**: the repository's own one-line commands, from its old workflow or lockfile. Leave `build` out when nothing builds.
66
+ - **`beforeDeploy`**: the old deploy's steps before `wrangler deploy` (migrations first), one command each. They read `$BREAKAWAY_ENV` (`staging` or `production`) and `$WORKER`. A step that needs more than one line goes in a script the command runs; a step that can't run there stays where it is (step 5).
67
+ - **`deployPaths`**: per Worker, a regular expression of the paths whose change needs a deploy: its source, static assets, migrations, the wrangler config, and the package and lockfile. The board reads the same patterns to say "No deploy needed", so a path left out never ships and a path put in too many deploys for nothing. You can't prove them from reading, so the pull request lists each part and what it covers (step 6).
68
+ - **`healthCheck`**: an address the old workflow or the README checks after a deploy, as a string (staging only) or `{ "staging", "production" }`. Leave it out when there is none; never guess one.
69
+ - **`package`**: `name` exactly as its `package.json` says, `directory` its folder (`.` at the root), and `access` (`restricted` only when `publishConfig.access` or the old publish says so; a scoped package published `public` today stays `public`). Its `package.json` needs a plain `X.Y.Z` version: the flow counts from it. A repository that only publishes has no `workers`, `deployPaths`, `healthCheck`, `beforeDeploy`, or `wranglerEnv`.
70
+
71
+ **A versioning tool with its own scheme** (changesets, semantic-release, release-please) decides versions from commits or changeset files; the release flow takes `package.json`'s version and stages `X.Y.Z-main.N` on `next` for every merge. They can't both run, and one can't be mapped onto the other. Don't replace it: ask the owner with a decision (keep the tool and move only the Worker deploy; or drop the tool for the release flow, which a later task does), and stop the package part until it's answered. Move a Worker deploy in the meantime only if the decision says to.
72
+
73
+ ## 4. Render the workflows
74
+
75
+ In the checkout, run `npx breakaway pipeline init`. It checks the config, naming the field that's wrong and what it should be, and the rendered workflows (YAML, pinned actions, expressions, secrets only in an environment), and writes nothing if either fails. It writes `.github/workflows/deploy.yml`, `promote.yml`, `rollback.yml`, and `.github/deploy-paths.json` for `workers`, and `.github/workflows/release.yml` for `package`. Never edit what it writes: change the config and run it again.
76
+
77
+ - **It refuses a file already there** that it didn't render. When that file is the old deploy or publish workflow you're replacing, delete it in this pull request (step 5) and run `pipeline init` again. When it's something else that only shares the name, stop and ask the owner with a decision: never rename or overwrite it silently.
78
+ - **It names helper scripts the repository lacks** (`scripts/record-deployment.mjs` and the others). `repos init <slug> --update` copies them, and that is the owner's: say so under **After merging** (the workflows fail without them), unless they're already there.
79
+ - **`npx breakaway pipeline check`** passes before you hand over: the config is sound, and the files are what it renders now.
80
+
81
+ ## 5. Nothing lost, nothing twice
82
+
83
+ Every old job and step ends up in exactly one place: the config, a rendered workflow, kept where it was, or dropped with the reason. The rules:
84
+
85
+ - **Checks are the repository's own.** Never edit or remove a workflow that checks. Deploy and Release wait for them by name.
86
+ - **A workflow that only deploys or only publishes** comes out in the same pull request, deleted, or with its trigger on the default branch removed when it also runs for something else (a tag, by hand, a preview on pull requests, which stays). Otherwise the merge deploys or publishes twice.
87
+ - **A workflow that checks and deploys, or checks and publishes**, is split: its checks stay as they are, and the deploy or publish job or steps come out (with the `needs:`, `if:`, permissions, and `environment` only they used). If its deploy job is all that runs on push to the default branch, drop that trigger too, so the checks still run where they did.
88
+ - **Workers Builds** (Cloudflare's git integration) deploys from Cloudflare, not from a file: you can't turn it off. Say under **After merging** that the owner disconnects it before merging, or the merge deploys twice.
89
+ - **A step the flow can't do** (a manual approval, a custom deploy script `beforeDeploy` can't run, a notification, a cache purge, publishing to another registry) stays exactly where it is, and the pull request lists it under **After merging** for the owner to decide. Never invent an equivalent.
90
+ - **Publishing by hand** (a `release` or `publish` script someone runs from a laptop) stays in `package.json`; the pull request says the release flow replaces it and the owner stops running it.
91
+ - **Never** add or change a secret, an environment, or a variable, and never put a token in a file.
92
+
93
+ ## 6. The pull request
94
+
95
+ Open it the way the repository's prompt's **Pull requests** says, closing the task. Its description holds, besides what that asks:
96
+
97
+ - **What moves**: the row from step 2, the Workers, and the package.
98
+ - **Step by step**: a table of every old workflow, job, and step, and where each went:
99
+
100
+ | Old | Was | Now |
101
+ | --- | --- | --- |
102
+ | `deploy.yml` · deploy | `wrangler deploy` on push to `main` | Deploy (`deploy.yml`, rendered) after CI passes; the old file is removed |
103
+ | `ci.yml` · test | `npm test` | Kept as it is; Deploy and Release wait for CI |
104
+ | `ci.yml` · publish | `npm publish` on a tag | Release (`release.yml`): `next` on every merge, `latest` on the owner's Release; the step is removed |
105
+ | `deploy.yml` · migrate | `wrangler d1 migrations apply` | `beforeDeploy` |
106
+ | `deploy.yml` · notify | posts to chat | Kept where it was (After merging) |
107
+
108
+ - **Deploy paths**: each pattern's parts and what they cover, so the owner can check them.
109
+ - **After merging**: the owner's checklist below, the steps kept where they were, and anything you stopped short of.
110
+
111
+ **The owner's part comes before the merge.** The merge itself runs Deploy and Release once its checks pass, so without the owner's part both fail. Add a `+owner` task for it, filled in like any task, with the checklist in its brief and only what this repository needs. It doesn't depend on the move task (it comes first), and the pull request's **After merging** repeats it under "Before you merge":
112
+
113
+ - the staging and production Workers (or confirm the existing ones), and a Cloudflare API token for each;
114
+ - the GitHub environments `staging` and `production`, each with its token as `CLOUDFLARE_API_TOKEN` and restricted to the default branch, and the repository variable `CLOUDFLARE_ACCOUNT_ID`;
115
+ - read and write on **Actions** for the board's GitHub App on the repository (Promote, Roll back, and Release start workflows);
116
+ - the helper scripts, with `repos init <slug> --update`, when `pipeline init` named any;
117
+ - Workers Builds disconnected, when the repository used it;
118
+ - for a package: the GitHub environment `npm`, restricted to the default branch; on npm, a trusted publisher for `release.yml` and the `npm` environment, or a granular `NPM_TOKEN` in that environment that can't bypass 2FA; and, after each run, approving the staged version on npm with 2FA (`npm stage approve <id>`, or Staged Packages on npmjs.com). The board shows what waits and never approves;
119
+ - optional: the repository variable `DEPLOYS_PAUSED` (`true` stops Promote and Release), and the health-check addresses;
120
+ - after the merge, **Turn on deploys** on the repository's GitHub page, which sets its pipeline from the files.
121
+
122
+ ## Before handing over
123
+
124
+ - `npx breakaway pipeline check` passes.
125
+ - The repository's own checks pass, and every workflow you changed still parses and runs the jobs it ran before, minus what moved.
126
+ - Every old step is in the table, and no deploy or publish is left that the merge would run besides Deploy and Release.
127
+ - No secret, token, or address you didn't read in the repository is in a file, the task, or the pull request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakaway",
3
- "version": "1.4.0-main.34",
3
+ "version": "1.4.0-main.36",
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",
@@ -35,6 +35,7 @@
35
35
  "prompts/*.md",
36
36
  "taskrc",
37
37
  ".agents/skills/tasks/SKILL.md",
38
+ ".agents/skills/pipeline/SKILL.md",
38
39
  "template"
39
40
  ],
40
41
  "engines": {
@@ -50,15 +51,16 @@
50
51
  },
51
52
  "scripts": {
52
53
  "dev": "vite",
53
- "build": "vite build",
54
- "test": "vitest run && vitest run --config scripts/tasks/vitest.config.js",
54
+ "prepare": "node scripts/board-files.mjs",
55
+ "build": "node scripts/board-files.mjs && vite build",
56
+ "test": "node scripts/board-files.mjs && vitest run && vitest run --config scripts/tasks/vitest.config.js",
55
57
  "brand": "node scripts/brand-lint.mjs",
56
58
  "format": "biome format --write .",
57
59
  "lint": "biome check .",
58
- "typecheck": "wrangler types && tsc -p tsconfig.json && tsc -p scripts/tsconfig.json && tsc -p web/tsconfig.json",
60
+ "typecheck": "node scripts/board-files.mjs && wrangler types && tsc -p tsconfig.json && tsc -p scripts/tsconfig.json && tsc -p web/tsconfig.json",
59
61
  "site": "node site/build.mjs",
60
62
  "interop": "node interop.mjs",
61
- "deploy": "vite build && wrangler deploy -c wrangler.jsonc"
63
+ "deploy": "node scripts/board-files.mjs && vite build && wrangler deploy -c wrangler.jsonc"
62
64
  },
63
65
  "devDependencies": {
64
66
  "@biomejs/biome": "2.5.15",
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
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. scripts/tasks/init.test.js
5
- * fails when it's behind them; run `node scripts/board-files.mjs` after changing one.
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.
6
7
  */
7
8
  import { readFileSync, writeFileSync } from 'node:fs';
8
9
  import { boardSources } from '../src/init.js';
@@ -14,5 +15,6 @@ export const boardFiles = () => Object.fromEntries(boardSources(read).map((path)
14
15
 
15
16
  if (import.meta.url === `file://${process.argv[1]}`) {
16
17
  writeFileSync(new URL('src/board-files.json', ROOT), `${JSON.stringify(boardFiles(), null, 2)}\n`);
17
- console.log('Wrote src/board-files.json.');
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.');
18
20
  }
@@ -49,17 +49,18 @@ export function unknownSubcommand(command, first) {
49
49
 
50
50
  /**
51
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
52
- * through npx) has nothing to say. In a checkout of the board's own repository, `own` older than the board's (`board`,
53
- * its X-Tasks-Cli header) means pull. Anywhere else the CLI is an old copy that `repos init` used to commit: it
54
- * works while the API stays compatible, and on each run it says how to switch.
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.
55
56
  */
56
- export function staleCliWarning({ own, board, boardCheckout, slug, packaged = false }) {
57
+ export function staleCliWarning({ own, board, boardCheckout, slug, packaged = false, release = null, behind = false }) {
57
58
  if (packaged) return null;
58
59
  const theirs = Number(board);
59
60
  const older = Number.isInteger(theirs) && theirs > own;
60
61
  if (boardCheckout) {
61
- if (!older) return null;
62
- return `this checkout's board CLI (version ${own}) is older than the board's (${theirs}), so a command may be missing or behave differently: pull the default branch to update it.`;
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.`;
63
64
  }
64
65
  const newer = older ? `, older than the board's (${theirs}), so a command may be missing or behave differently` : '';
65
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.`;
@@ -77,6 +78,22 @@ export function githubRequest(repo, { sync = false } = {}) {
77
78
  return ['GET', repo ? `github?repo=${encodeURIComponent(repo)}` : 'github', undefined];
78
79
  }
79
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
+
80
97
  /** What `github fix` accepts for --problem: the same three the pull request page offers. */
81
98
  export const FIX_PROBLEMS = ['conflicts', 'failing', 'review'];
82
99
 
package/scripts/tasks.mjs CHANGED
@@ -72,6 +72,7 @@ import {
72
72
  pullAgentSummary,
73
73
  reviewRequest,
74
74
  staleCliWarning,
75
+ releaseBehind,
75
76
  unknownSubcommand,
76
77
  } from './tasks/cli.js';
77
78
  import { mergeViews, pelotonLines, pelotonPost, pickPeloton } from './tasks/peloton.js';
@@ -423,7 +424,7 @@ async function call(method, path, body, { soft = false } = {}) {
423
424
  `can't reach ${BASE} (${reasonOf(error)}). Cloud sessions need ${new URL(BASE).host} allowed in their network settings.`,
424
425
  );
425
426
  }
426
- warnIfStale(res.headers.get('X-Tasks-Cli'));
427
+ warnIfStale(res.headers.get('X-Tasks-Cli'), res.headers.get('X-Tasks-Release'));
427
428
  if (res.status === 401 && !token)
428
429
  fail(
429
430
  `no token. Set BREAKAWAY_TOKEN, put it in ${ENV_FILE}, or add it as an API credential in the cloud environment (see docs/tasks.md#cloud-agents).`,
@@ -444,8 +445,11 @@ async function call(method, path, body, { soft = false } = {}) {
444
445
  }
445
446
 
446
447
  let warnedStale = false;
447
- /** Once a run: say so when this copy of the CLI is older than the board's (CLD-193). On stderr, so --json stays clean. */
448
- function warnIfStale(board) {
448
+ /**
449
+ * Once a run: say so when this copy of the CLI is older than the board's (CLD-193), or this checkout of the board's own
450
+ * repository is behind the release the board runs (BRK-148). On stderr, so --json stays clean.
451
+ */
452
+ function warnIfStale(board, release = null) {
449
453
  // An old copy says how to switch on every run, even when the board doesn't say its version.
450
454
  if (warnedStale || (board === null && PACKAGED)) return;
451
455
  warnedStale = true;
@@ -459,7 +463,22 @@ function warnIfStale(board) {
459
463
  /* no .taskrc */
460
464
  }
461
465
  }
462
- const warning = staleCliWarning({ own: CLI_VERSION, board, boardCheckout, slug, packaged: PACKAGED });
466
+ const git = (args) => spawnSync('git', ['-C', REPO, ...args], { encoding: 'utf8', timeout: 5000 });
467
+ const behind =
468
+ boardCheckout &&
469
+ Boolean(release) &&
470
+ releaseBehind(release, (args) => git(args).status, {
471
+ shallow: git(['rev-parse', '--is-shallow-repository']).stdout?.trim() === 'true',
472
+ });
473
+ const warning = staleCliWarning({
474
+ own: CLI_VERSION,
475
+ board,
476
+ boardCheckout,
477
+ slug,
478
+ packaged: PACKAGED,
479
+ release,
480
+ behind,
481
+ });
463
482
  if (warning) console.error(`tasks: ${warning}`);
464
483
  }
465
484
 
@@ -2013,7 +2032,7 @@ async function initRepo(slug) {
2013
2032
  title,
2014
2033
  '-m',
2015
2034
  update
2016
- ? `The board's core, skill, release helpers, and Taskwarrior files as they are in ${board ?? 'breakaway'} now, and the session hooks run through npx, so an old copy of the CLI is removed: run it as npx ${CLI_PACKAGE} (CLI version ${CLI_VERSION}). This repository's own files are unchanged. Updated by npx ${CLI_PACKAGE} repos init ${repo.slug} --update.`
2035
+ ? `The board's core, skill, release helpers, and Taskwarrior files as they are in ${board ?? 'breakaway'} now, and the session hooks run through npx, so an old copy of the CLI is removed: run it as npx ${CLI_PACKAGE}. This repository's own files are unchanged. Updated by npx ${CLI_PACKAGE} repos init ${repo.slug} --update.`
2017
2036
  : `${first.body}${starter.files.length ? ` It also adds the deploy and release flows, rendered from ${starter.files[0].path}.` : ''}`,
2018
2037
  );
2019
2038
  const pushed = spawnSync('git', ['-C', dir, 'push', '-u', 'origin', empty ? `HEAD:refs/heads/${branch}` : work], {
@@ -2039,7 +2058,7 @@ async function initRepo(slug) {
2039
2058
  title,
2040
2059
  '--body',
2041
2060
  update
2042
- ? `The task board's copied files (its CLI, version ${CLI_VERSION}, the core and stub, the tasks skill, and the Taskwarrior files) as they are on the board's repository now, updated by \`npx breakaway repos init ${repo.slug} --update\`. This repository's own files (its agent prompt, AGENTS.md, .taskrc, .envrc, package.json, .claude/settings.json) are unchanged.`
2061
+ ? `The task board's copied files (its CLI, the core and stub, the tasks skill, and the Taskwarrior files) as they are on the board's repository now, updated by \`npx breakaway repos init ${repo.slug} --update\`. This repository's own files (its agent prompt, AGENTS.md, .taskrc, .envrc, package.json, .claude/settings.json) are unchanged.`
2043
2062
  : `The files the task board's agents need to claim and work a task in this repository, added by \`npx breakaway repos init ${repo.slug}\`. Nothing that was there is changed. If this repository's linter reads plain JavaScript, exclude the copied scripts (tools/tasks/ and the release helpers) from it.${plan.todo.length ? `\n\nStill to do:\n${plan.todo.map((t) => `- ${t}`).join('\n')}` : ''}`,
2044
2063
  ],
2045
2064
  { encoding: 'utf8' },
@@ -1,8 +1,7 @@
1
1
  /**
2
- * The version of the board's CLI and the files repos init copies with it (CLD-193). The board reports it
3
- * (every API answer's X-Tasks-Cli header, and health), so a copy in another repository can say it's older
4
- * and how to update it. scripts/tasks/version.test.js fails when the copied files change and this doesn't:
5
- * so it lives in the board's package (CLD-135) and the CLI imports it from here.
2
+ * The number the board's CLI and the files repos init copies were versioned by (CLD-193), frozen by BRK-148: never
3
+ * change it. The board still sends it (every API answer's X-Tasks-Cli header, and `cli` in health), so an old copy of
4
+ * the CLI in another repository says how to switch to npx. What a checkout compares now is the board's release (the
5
+ * X-Tasks-Release header): pull requests no longer bump anything when a copied file changes.
6
6
  */
7
7
  export const CLI_VERSION = 73;
8
- export const CLI_FINGERPRINT = '2da33f9e367765c1';
package/src/init.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * scripts/tasks.mjs, and the board's first commit to an empty repository, through its GitHub App, is in
8
8
  * src/store-init.js, which reads the board's files from src/board-files.json (BOARD_SOURCES).
9
9
  */
10
- import { SKILL, areaList, routinePrompt } from './prompt.js';
10
+ import { PIPELINE_SKILL, SKILL, areaList, routinePrompt } from './prompt.js';
11
11
  import { promptPathOf } from './repos.js';
12
12
 
13
13
  /** This machine's folder for the board, as .taskrc names it, where a caller doesn't say (scripts/tasks/settings.js). */
@@ -50,7 +50,7 @@ export const RELEASE_ENTRIES = [
50
50
  /** Copied unchanged: the board's shared core and stub, and the Taskwarrior settings the new .taskrc includes. */
51
51
  const COPIED = ['prompts/core.md', 'prompts/stub.md'];
52
52
  /** Copied with a change for the repository, or made from the board's: their source is versioned too. */
53
- const ADAPTED = ['taskrc', 'scripts/task', SKILL];
53
+ const ADAPTED = ['taskrc', 'scripts/task', SKILL, PIPELINE_SKILL];
54
54
  /**
55
55
  * Where the board's files (the prompts, the shared taskrc) go in another repository. In the board's own checkout they
56
56
  * sit at the root, so the paths `read` takes are the board's; what is written keeps this folder.
@@ -68,7 +68,7 @@ const GITATTRIBUTES = ['scripts/task text eol=lf', '.envrc text eol=lf'];
68
68
  /**
69
69
  * The board's own files initPlan reads (repository paths, sorted): the prompt template, what it copies as it is or
70
70
  * adapted, and the release helpers with what they import. src/board-files.json holds them for the Worker, and
71
- * scripts/tasks/init.test.js fails when it falls behind (`node scripts/board-files.mjs` writes it again).
71
+ * `node scripts/board-files.mjs` writes it before every build and test run (it isn't committed, BRK-148).
72
72
  */
73
73
  export function boardSources(read) {
74
74
  return [...new Set(['prompts/repository.md', ...COPIED, ...ADAPTED, ...importClosure(RELEASE_ENTRIES, read)])].sort();
@@ -116,19 +116,6 @@ export function importClosure(entries, read) {
116
116
  return [...seen].sort();
117
117
  }
118
118
 
119
- /**
120
- * Every file CLI_VERSION in src/cli-version.js versions (repository paths, sorted): what repos init copies, as it is
121
- * or adapted, and the CLI's own files, which the npm package carries and an old copy still holds. cli-version.js
122
- * itself is left out, so the fingerprint can live in it.
123
- */
124
- export function copiedSources(read) {
125
- return [
126
- ...new Set([...importClosure(CLI_ENTRIES, read), ...importClosure(RELEASE_ENTRIES, read), ...COPIED, ...ADAPTED]),
127
- ]
128
- .filter((path) => path !== 'src/cli-version.js')
129
- .sort();
130
- }
131
-
132
119
  /**
133
120
  * The files an old copy of the CLI holds that npx replaces (repository paths, sorted): the CLI, its hooks, and what
134
121
  * only they import. The release helpers' files stay copied, so they are not here.
@@ -201,16 +188,6 @@ export function declaredCopies(agents) {
201
188
  return [...line.split(/ come from \[/u)[0].matchAll(/`([^`]+)`/gu)].map((m) => m[1]);
202
189
  }
203
190
 
204
- /** A short SHA-256 of `paths` and their text: changes whenever one of them does. */
205
- export async function fingerprint(paths, read) {
206
- const text = paths.map((path) => `${path}\0${read(path)}\0`).join('');
207
- const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
208
- return [...new Uint8Array(hash)]
209
- .slice(0, 8)
210
- .map((b) => b.toString(16).padStart(2, '0'))
211
- .join('');
212
- }
213
-
214
191
  /**
215
192
  * The Taskwarrior report and context for repository `slug` (IDEA-14): `task <slug>` lists its open work, and
216
193
  * `task context <slug>` narrows everything to it and puts new tasks in it. For the shared taskrc.
@@ -340,7 +317,7 @@ export function agentsMd(repo, board, dir = DEFAULT_DIR) {
340
317
 
341
318
  - **Work lives on the task board.** This repository's tasks are in the areas ${areaList(repo)}. Use the \`tasks\` skill (\`${SKILL}\`) and the CLI, \`npx breakaway\` (the \`breakaway\` package on npm), to claim, comment, and hand over. It works in this checkout's repository, so \`list\` and \`next\` show only this repository's tasks. The skill is breakaway's, written for this repository's areas and prompt: where it names breakaway's own files or rules, the board's part applies and the rest doesn't.
342
319
  - **Agents started by the board** follow [\`${promptPathOf(repo)}\`](${promptPathOf(repo)}), which starts with the board's core, \`tools/tasks/prompts/core.md\`.
343
- - **Copied files.** \`tools/tasks/\`, the release helpers in \`scripts/\`, and \`${SKILL}\` come from [${board}](https://github.com/${board}). Don't edit them here: change them there. \`${MANIFEST}\` lists every file it copied, and \`repos init --update\` replaces only those: a file it doesn't list is this repository's own, even at a path breakaway copies to. \`.claude/settings.json\` holds the session hooks that show a cloud agent's output on its task, and they run through \`npx\`, so this repository carries no copy of the CLI.
320
+ - **Copied files.** \`tools/tasks/\`, the release helpers in \`scripts/\`, \`${SKILL}\`, and \`${PIPELINE_SKILL}\` come from [${board}](https://github.com/${board}). Don't edit them here: change them there. \`${MANIFEST}\` lists every file it copied, and \`repos init --update\` replaces only those: a file it doesn't list is this repository's own, even at a path breakaway copies to. \`.claude/settings.json\` holds the session hooks that show a cloud agent's output on its task, and they run through \`npx\`, so this repository carries no copy of the CLI.
344
321
  - **Taskwarrior** (optional): \`scripts/task\`, or plain \`task\` with direnv after \`direnv allow\`, uses the board with this checkout's own \`.task/\` database, in the \`${repo.slug}\` context. \`npx breakaway setup\` connects the machine once.
345
322
  - **Changes reach \`${repo.defaultBranch || 'main'}\` through pull requests**, which the owner merges. Never merge, force-push, or rewrite \`${repo.defaultBranch || 'main'}\`.
346
323
  - **Never put a secret or token** in a file, task, comment, or pull request. The board's token lives in \`${dir}/tasks.env\` (or \`$BREAKAWAY_HOME/tasks.env\`) or the cloud environment's credentials, never in this repository.
@@ -495,6 +472,7 @@ export function initPlan({
495
472
  }
496
473
  if (readTarget('.claude/skills') === null) files.push({ path: '.claude/skills', link: '../.agents/skills' });
497
474
  copy(SKILL, skillFor(read(SKILL), board, repo));
475
+ copy(PIPELINE_SKILL, skillFor(read(PIPELINE_SKILL), board, repo));
498
476
  add('AGENTS.md', agentsMd(repo, board, configDir));
499
477
  if (!skipped.includes('AGENTS.md')) todo.push('AGENTS.md: add how to build in this repository');
500
478
 
package/src/prompt.js CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  /** Where the `tasks` skill sits in a repository the board runs. */
8
8
  export const SKILL = '.agents/skills/tasks/SKILL.md';
9
+ /** Where the `pipeline` skill sits: how an agent moves a repository's CI/CD to the deploy flow (BRK-92). */
10
+ export const PIPELINE_SKILL = '.agents/skills/pipeline/SKILL.md';
9
11
 
10
12
  /** The repository's areas for its prompt and AGENTS.md: "product (`BWYP`), cloud (`BWYC`)". */
11
13
  export const areaList = (repo) => repo.areas.map((a) => `${a.project} (\`${a.prefix}\`)`).join(', ');