breakaway 1.0.2-main.4 → 1.0.2-main.6
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 +1 -1
- package/scripts/tasks/init.js +125 -13
- package/src/cli-version.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.0.2-main.
|
|
3
|
+
"version": "1.0.2-main.6",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/scripts/tasks/init.js
CHANGED
|
@@ -49,6 +49,11 @@ const ADAPTED = ['taskrc', 'scripts/task', SKILL];
|
|
|
49
49
|
* sit at the root, so the paths `read` takes are the board's; what is written keeps this folder.
|
|
50
50
|
*/
|
|
51
51
|
const TARGET_DIR = 'tools/tasks/';
|
|
52
|
+
/**
|
|
53
|
+
* The record of what repos init wrote into a repository (BRK-79): `--update` replaces a copied file only when it's
|
|
54
|
+
* listed here, or sits in TARGET_DIR, breakaway's own folder. Anything else at a path it copies to is the repository's.
|
|
55
|
+
*/
|
|
56
|
+
export const MANIFEST = `${TARGET_DIR}copied.json`;
|
|
52
57
|
const GITIGNORE = ['.task/', '.task-session', '.env'];
|
|
53
58
|
/** Pinned to LF so the shell scripts run on a checkout with core.autocrlf=true (BRK-41). */
|
|
54
59
|
const GITATTRIBUTES = ['scripts/task text eol=lf', '.envrc text eol=lf'];
|
|
@@ -146,11 +151,30 @@ export function rewireHooks(text) {
|
|
|
146
151
|
const now = JSON.stringify(hookCommand(name)).slice(1, -1);
|
|
147
152
|
out = out.split(was).join(now);
|
|
148
153
|
}
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
const current = (_, name) => JSON.stringify(hookCommand(name === 'message-wait' ? 'wait' : name)).slice(1, -1);
|
|
155
|
+
return (
|
|
156
|
+
out
|
|
157
|
+
.replace(/npx --yes breakaway(?:@[^\s"\\]+)? hook (session|wait)\b/gu, current)
|
|
158
|
+
// The old CLI copy's hook scripts, from before BRK-7, quoted or not (BRK-79): its copy is about to go.
|
|
159
|
+
.replace(
|
|
160
|
+
/node (\\")?(?:\$CLAUDE_PROJECT_DIR\/)?(?:tools\/tasks\/cli\/)?scripts\/tasks\/(session-hook|message-wait)\.mjs\1/gu,
|
|
161
|
+
(_, _q, name) => current(_, name === 'session-hook' ? 'session' : name),
|
|
162
|
+
)
|
|
151
163
|
);
|
|
152
164
|
}
|
|
153
165
|
|
|
166
|
+
/**
|
|
167
|
+
* The paths a repository's AGENTS.md says came from the board, in its "Copied files." bullet as any version of repos
|
|
168
|
+
* init wrote it; a folder ends in `/`. None when AGENTS.md is the repository's own (BRK-79).
|
|
169
|
+
*/
|
|
170
|
+
export function declaredCopies(agents) {
|
|
171
|
+
const line = String(agents ?? '')
|
|
172
|
+
.split('\n')
|
|
173
|
+
.find((l) => /^- \*\*Copied files\.\*\*/u.test(l));
|
|
174
|
+
if (!line) return [];
|
|
175
|
+
return [...line.split(/ come from \[/u)[0].matchAll(/`([^`]+)`/gu)].map((m) => m[1]);
|
|
176
|
+
}
|
|
177
|
+
|
|
154
178
|
/** A short SHA-256 of `paths` and their text: changes whenever one of them does. */
|
|
155
179
|
export async function fingerprint(paths, read) {
|
|
156
180
|
const text = paths.map((path) => `${path}\0${read(path)}\0`).join('');
|
|
@@ -266,10 +290,20 @@ export function promptSections(given = {}) {
|
|
|
266
290
|
}
|
|
267
291
|
|
|
268
292
|
/** The `tasks` skill for another repository: links into the board's docs point at them on GitHub. */
|
|
269
|
-
export function skillFor(text, board) {
|
|
270
|
-
|
|
293
|
+
export function skillFor(text, board, repo = null) {
|
|
294
|
+
const linked = String(text)
|
|
271
295
|
.replace(/\]\((?:\.\.\/)+((?:docs|tools)\/[^)]+)\)/gu, `](https://github.com/${board}/blob/main/$1)`)
|
|
272
296
|
.replace(/\]\(((?:\.\.\/)+)prompts\//gu, ']($1tools/tasks/prompts/');
|
|
297
|
+
if (!repo) return linked;
|
|
298
|
+
// The skill is breakaway's own: in another repository it names that repository's work, areas, and prompt (BRK-79).
|
|
299
|
+
const prompt = promptPathOf(repo);
|
|
300
|
+
return linked
|
|
301
|
+
.replace(/breakaway's work is on the board/gu, "This repository's work is on the board")
|
|
302
|
+
.replace(/breakaway's areas: [^.\n]+\./gu, `This repository's areas: ${areaList(repo)}.`)
|
|
303
|
+
.replace(
|
|
304
|
+
/\[`prompts\/breakaway\.md`\]\(((?:\.\.\/)+)tools\/tasks\/prompts\/breakaway\.md\)/gu,
|
|
305
|
+
(_, up) => `[\`${prompt}\`](${up}${prompt})`,
|
|
306
|
+
);
|
|
273
307
|
}
|
|
274
308
|
|
|
275
309
|
/** A starter AGENTS.md: how this repository works with the board. The owner adds how to build here. */
|
|
@@ -278,9 +312,9 @@ export function agentsMd(repo, board, dir = DEFAULT_DIR) {
|
|
|
278
312
|
|
|
279
313
|
<!-- Started by \`npx breakaway repos init\` (breakaway's task board). Add how to build here: setup, tests, style, and anything agents must never do. -->
|
|
280
314
|
|
|
281
|
-
- **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
|
|
315
|
+
- **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.
|
|
282
316
|
- **Agents started by the board** follow [\`${promptPathOf(repo)}\`](${promptPathOf(repo)}), which starts with the board's core, \`tools/tasks/prompts/core.md\`.
|
|
283
|
-
- **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. \`.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.
|
|
317
|
+
- **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.
|
|
284
318
|
- **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.
|
|
285
319
|
- **Changes reach \`${repo.defaultBranch || 'main'}\` through pull requests**, which the owner merges. Never merge, force-push, or rewrite \`${repo.defaultBranch || 'main'}\`.
|
|
286
320
|
- **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.
|
|
@@ -343,13 +377,46 @@ export function initPlan({
|
|
|
343
377
|
if (readTarget(path) !== null) skipped.push(path);
|
|
344
378
|
else files.push({ path, content, ...extra });
|
|
345
379
|
};
|
|
346
|
-
|
|
380
|
+
// What an earlier run recorded writing (BRK-79): null for a repository set up before the record existed.
|
|
381
|
+
const recorded = (() => {
|
|
382
|
+
try {
|
|
383
|
+
const files = JSON.parse(readTarget(MANIFEST) ?? 'null')?.files;
|
|
384
|
+
return Array.isArray(files) ? new Set(files) : null;
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
})();
|
|
389
|
+
// Set up before the record: the AGENTS.md repos init wrote says the copied files came from the board, and a
|
|
390
|
+
// repository's own AGENTS.md doesn't, so only then are files at the copied paths breakaway's.
|
|
391
|
+
const declared = recorded === null ? declaredCopies(readTarget('AGENTS.md')) : [];
|
|
392
|
+
/** Whether repos init wrote `path` here: in breakaway's own folder, on the record, or in AGENTS.md's copied files. */
|
|
393
|
+
const owns = (path) =>
|
|
394
|
+
path.startsWith(TARGET_DIR) ||
|
|
395
|
+
Boolean(recorded?.has(path)) ||
|
|
396
|
+
declared.some((d) => (d.endsWith('/') ? path.startsWith(d) : path === d));
|
|
397
|
+
const ours = new Set();
|
|
398
|
+
const theirs = [];
|
|
399
|
+
/**
|
|
400
|
+
* A file copied from the board: added when it's missing, and with `update`, replaced when it differs, but only when
|
|
401
|
+
* repos init wrote it (the record lists it, or it's in breakaway's own folder). A repository's own file at a path
|
|
402
|
+
* breakaway copies to is left alone (BRK-79).
|
|
403
|
+
*/
|
|
347
404
|
const copy = (path, content, extra = {}) => {
|
|
348
405
|
const there = readTarget(path);
|
|
349
|
-
if (there === null)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
else
|
|
406
|
+
if (there === null) {
|
|
407
|
+
files.push({ path, content, ...extra });
|
|
408
|
+
ours.add(path);
|
|
409
|
+
} else if (there === content) {
|
|
410
|
+
(update ? current : skipped).push(path);
|
|
411
|
+
ours.add(path);
|
|
412
|
+
} else if (!update) skipped.push(path);
|
|
413
|
+
else if (owns(path)) {
|
|
414
|
+
files.push({ path, content, ...extra, changed: true });
|
|
415
|
+
ours.add(path);
|
|
416
|
+
} else {
|
|
417
|
+
skipped.push(path);
|
|
418
|
+
theirs.push(path);
|
|
419
|
+
}
|
|
353
420
|
};
|
|
354
421
|
|
|
355
422
|
const prompt = promptPathOf(repo);
|
|
@@ -362,7 +429,14 @@ export function initPlan({
|
|
|
362
429
|
}
|
|
363
430
|
for (const path of COPIED) copy(TARGET_DIR + path, read(path));
|
|
364
431
|
copy(`${TARGET_DIR}taskrc`, withRepoInTaskrc(read('taskrc'), repo.slug, Boolean(repo.isDefault)));
|
|
365
|
-
|
|
432
|
+
// A release helper the repository keeps as its own doesn't bring the files breakaway's version imports (BRK-79).
|
|
433
|
+
const kept = (entry) => {
|
|
434
|
+
const there = readTarget(entry);
|
|
435
|
+
return there === null || there === read(entry) || (update && owns(entry));
|
|
436
|
+
};
|
|
437
|
+
const needed = new Set(importClosure(RELEASE_ENTRIES.filter(kept), read));
|
|
438
|
+
for (const path of importClosure(RELEASE_ENTRIES, read))
|
|
439
|
+
if (needed.has(path) || readTarget(path) !== null) copy(path, read(path));
|
|
366
440
|
if (HOOKS_FROM_COPY) for (const path of importClosure(HOOK_ENTRIES, read)) copy(HOOKS_DIR + path, read(path));
|
|
367
441
|
// An old copy of the CLI is replaced by npx: with update, its files go. The src/ files it imported may be the
|
|
368
442
|
// repository's own by now, so those are only named.
|
|
@@ -394,7 +468,7 @@ export function initPlan({
|
|
|
394
468
|
);
|
|
395
469
|
}
|
|
396
470
|
if (readTarget('.claude/skills') === null) files.push({ path: '.claude/skills', link: '../.agents/skills' });
|
|
397
|
-
copy(SKILL, skillFor(read(SKILL), board));
|
|
471
|
+
copy(SKILL, skillFor(read(SKILL), board, repo));
|
|
398
472
|
add('AGENTS.md', agentsMd(repo, board, configDir));
|
|
399
473
|
if (!skipped.includes('AGENTS.md')) todo.push('AGENTS.md: add how to build in this repository');
|
|
400
474
|
|
|
@@ -448,5 +522,43 @@ export function initPlan({
|
|
|
448
522
|
append: attributes !== null,
|
|
449
523
|
});
|
|
450
524
|
}
|
|
525
|
+
// A file the repository still runs stays, with the old copy it belongs to (BRK-79): package.json, or settings the
|
|
526
|
+
// rewiring above couldn't move to npx.
|
|
527
|
+
const settingsNow =
|
|
528
|
+
files.find((f) => f.path === '.claude/settings.json')?.content ?? readTarget('.claude/settings.json');
|
|
529
|
+
const uses = [
|
|
530
|
+
['package.json', readTarget('package.json')],
|
|
531
|
+
['.claude/settings.json', settingsNow],
|
|
532
|
+
];
|
|
533
|
+
for (const group of [(p) => p.startsWith('scripts/'), (p) => p.startsWith(HOOKS_DIR)]) {
|
|
534
|
+
const inGroup = removals.filter(group);
|
|
535
|
+
const used = uses.flatMap(([file, text]) =>
|
|
536
|
+
inGroup.filter((p) => String(text ?? '').includes(p)).map((p) => `${file} still runs ${p}`),
|
|
537
|
+
);
|
|
538
|
+
if (!used.length) continue;
|
|
539
|
+
for (const p of inGroup) removals.splice(removals.indexOf(p), 1);
|
|
540
|
+
notes.push(
|
|
541
|
+
`${used.join('; ')}, so its copy stays: switch it to npx breakaway (\`${hookCommand('session')}\` for the hooks), then run --update again to remove the copy.`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (theirs.length)
|
|
545
|
+
notes.push(
|
|
546
|
+
`${theirs.join(', ')} ${theirs.length > 1 ? 'are' : 'is'} at a path breakaway copies to, but repos init has no record of writing ${theirs.length > 1 ? 'them' : 'it'}, so ${theirs.length > 1 ? 'they are left as they are' : 'it is left as it is'}. If one is breakaway's older copy, delete it and run --update again.`,
|
|
547
|
+
);
|
|
548
|
+
// The record of what is breakaway's here; a run without --update leaves an existing one alone.
|
|
549
|
+
const record = `${JSON.stringify(
|
|
550
|
+
{
|
|
551
|
+
about: `Files repos init copied from ${board} and keeps in step with it (npx breakaway repos init <slug> --update). A file not listed is this repository's own, even at a path breakaway copies to.`,
|
|
552
|
+
from: board,
|
|
553
|
+
files: [...ours].sort(),
|
|
554
|
+
},
|
|
555
|
+
null,
|
|
556
|
+
2,
|
|
557
|
+
)}\n`;
|
|
558
|
+
const recordThere = readTarget(MANIFEST);
|
|
559
|
+
if (recordThere === null) files.push({ path: MANIFEST, content: record });
|
|
560
|
+
else if (recordThere === record) current.push(MANIFEST);
|
|
561
|
+
else if (update) files.push({ path: MANIFEST, content: record, changed: true });
|
|
562
|
+
else skipped.push(MANIFEST);
|
|
451
563
|
return { files, removals, skipped, current, notes, todo };
|
|
452
564
|
}
|
package/src/cli-version.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* bump CLI_VERSION and set CLI_FINGERPRINT to the value it prints. Constants only: the board's Worker imports it,
|
|
6
6
|
* so it lives in the board's package (CLD-135) and the CLI imports it from here.
|
|
7
7
|
*/
|
|
8
|
-
export const CLI_VERSION =
|
|
9
|
-
export const CLI_FINGERPRINT = '
|
|
8
|
+
export const CLI_VERSION = 38;
|
|
9
|
+
export const CLI_FINGERPRINT = 'e5cf577cb5354653';
|