breakaway 1.4.0-main.34 → 1.4.0-main.35

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.34",
3
+ "version": "1.4.0-main.35",
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",
@@ -50,15 +50,16 @@
50
50
  },
51
51
  "scripts": {
52
52
  "dev": "vite",
53
- "build": "vite build",
54
- "test": "vitest run && vitest run --config scripts/tasks/vitest.config.js",
53
+ "prepare": "node scripts/board-files.mjs",
54
+ "build": "node scripts/board-files.mjs && vite build",
55
+ "test": "node scripts/board-files.mjs && vitest run && vitest run --config scripts/tasks/vitest.config.js",
55
56
  "brand": "node scripts/brand-lint.mjs",
56
57
  "format": "biome format --write .",
57
58
  "lint": "biome check .",
58
- "typecheck": "wrangler types && tsc -p tsconfig.json && tsc -p scripts/tsconfig.json && tsc -p web/tsconfig.json",
59
+ "typecheck": "node scripts/board-files.mjs && wrangler types && tsc -p tsconfig.json && tsc -p scripts/tsconfig.json && tsc -p web/tsconfig.json",
59
60
  "site": "node site/build.mjs",
60
61
  "interop": "node interop.mjs",
61
- "deploy": "vite build && wrangler deploy -c wrangler.jsonc"
62
+ "deploy": "node scripts/board-files.mjs && vite build && wrangler deploy -c wrangler.jsonc"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@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
@@ -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.