breakaway 1.4.0-main.13 → 1.4.0-main.15
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/cli.js +117 -9
- package/scripts/tasks.mjs +22 -1
- package/src/cli-version.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.4.0-main.
|
|
3
|
+
"version": "1.4.0-main.15",
|
|
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/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). */
|
|
@@ -163,17 +164,27 @@ export function forceFields(force, by) {
|
|
|
163
164
|
* With `decision` (`agents new --decision <ID> ["<note>"]`, BRK-110) the board writes the prompt from that answered
|
|
164
165
|
* decision, in the decision's repository, and the text is the owner's note under it. With `next`
|
|
165
166
|
* (`agents new --next minor|major ["<note>"]`, BRK-100) it writes the prompt that sets the repository's next version.
|
|
167
|
+
* With `spec` (`agents new --spec <path> "<what should change>"`, BRK-121) it writes the prompt that refines that spec
|
|
168
|
+
* and the tasks that link it, and the text, required, is what should change.
|
|
166
169
|
* @param {string} prompt
|
|
167
|
-
* @param {{ repo?: string | null, force?: boolean, by?: string, decision?: string | null, next?: string | null }} [options]
|
|
170
|
+
* @param {{ repo?: string | null, force?: boolean, by?: string, decision?: string | null, next?: string | null, spec?: string | null }} [options]
|
|
168
171
|
*/
|
|
169
|
-
export function generalAgentRequest(
|
|
172
|
+
export function generalAgentRequest(
|
|
173
|
+
prompt,
|
|
174
|
+
{ repo = null, force = false, by, decision = null, next = null, spec = null } = {},
|
|
175
|
+
) {
|
|
170
176
|
const text = String(prompt ?? '').trim();
|
|
171
|
-
if (decision
|
|
177
|
+
if ([decision, next, spec].filter(Boolean).length > 1)
|
|
178
|
+
return { error: 'start one from --decision, --spec, or --next: only one of them' };
|
|
172
179
|
if (next && !['minor', 'major'].includes(next))
|
|
173
180
|
return { error: 'patches count by themselves: --next minor or --next major' };
|
|
181
|
+
if (spec && !text)
|
|
182
|
+
return {
|
|
183
|
+
error: 'say what should change in the spec: npx breakaway agents new --spec <path> "<what should change>"',
|
|
184
|
+
};
|
|
174
185
|
if (!text && !decision && !next)
|
|
175
186
|
return { error: 'say what the agent should do: npx breakaway agents new "Tidy the docs" [--image <file>]' };
|
|
176
|
-
const board = decision ? { decision } : next ? { next } : null;
|
|
187
|
+
const board = decision ? { decision } : next ? { next } : spec ? { spec: specPath(spec) } : null;
|
|
177
188
|
const body = {
|
|
178
189
|
...(board ? { ...board, ...(text ? { note: text } : {}) } : { prompt: text }),
|
|
179
190
|
...(repo ? { repo } : {}),
|
|
@@ -185,18 +196,54 @@ export function generalAgentRequest(prompt, { repo = null, force = false, by, de
|
|
|
185
196
|
|
|
186
197
|
/**
|
|
187
198
|
* What the CLI says about a general agent's answer: the task and that it started, or why it waits (and whether Force
|
|
188
|
-
* start could skip that), or, from a decision
|
|
199
|
+
* start could skip that), or, from a decision, for the next version (`next`), or on a spec (`spec`), the open one
|
|
200
|
+
* that already has it.
|
|
189
201
|
* @param {{ task: { wid?: string, short?: string }, run?: { url?: string, agent?: string } | null, waiting?: string | null, forceable?: boolean, already?: string | null }} answer
|
|
190
|
-
* @param {{ next?: string | null }} [options]
|
|
202
|
+
* @param {{ next?: string | null, spec?: unknown }} [options]
|
|
191
203
|
*/
|
|
192
|
-
export function generalAgentSummary({ task, run, waiting, forceable, already }, { next = null } = {}) {
|
|
204
|
+
export function generalAgentSummary({ task, run, waiting, forceable, already }, { next = null, spec = null } = {}) {
|
|
193
205
|
const id = task.wid ?? task.short;
|
|
194
|
-
if (!run && already)
|
|
195
|
-
|
|
206
|
+
if (!run && already) {
|
|
207
|
+
const what = next ? 'prepares the next version' : spec ? 'refines this spec' : 'refines from these answers';
|
|
208
|
+
return `${id} already ${what}: ${already}.`;
|
|
209
|
+
}
|
|
196
210
|
if (run) return `Started ${run.agent ? `${run.agent} ` : 'an agent '}on ${id}${run.url ? `: ${run.url}` : ''}`;
|
|
197
211
|
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` : ''}`;
|
|
198
212
|
}
|
|
199
213
|
|
|
214
|
+
/** A spec's path as the board reads it: no leading `./`, no doubled or trailing slashes. */
|
|
215
|
+
const specPath = (path) =>
|
|
216
|
+
String(path ?? '')
|
|
217
|
+
.trim()
|
|
218
|
+
.replace(/^(\.\/)+/u, '')
|
|
219
|
+
.split('/')
|
|
220
|
+
.filter((part) => part && part !== '.')
|
|
221
|
+
.join('/');
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The request behind `npx breakaway specs [list]` (BRK-121): the specs of the checkout's repository, or the one `--repo`
|
|
225
|
+
* names; without either, the board answers with its default repository's.
|
|
226
|
+
* @param {string | null} repo
|
|
227
|
+
* @returns {[string, string, undefined]}
|
|
228
|
+
*/
|
|
229
|
+
export function specsRequest(repo) {
|
|
230
|
+
return ['GET', repo ? `specs?repo=${encodeURIComponent(repo)}` : 'specs', undefined];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The request behind `npx breakaway specs show <path>` (BRK-121): one spec, by its path in the repository. The board
|
|
235
|
+
* refuses a path outside the specs directory; one that climbs out with `..` is refused here first.
|
|
236
|
+
* @param {string | undefined} path
|
|
237
|
+
* @param {string | null} repo
|
|
238
|
+
*/
|
|
239
|
+
export function specRequest(path, repo) {
|
|
240
|
+
const clean = specPath(path);
|
|
241
|
+
if (!clean) return { error: 'say which spec: npx breakaway specs show <path>, like docs/specs/BRK-1-thing.md' };
|
|
242
|
+
if (clean.split('/').includes('..')) return { error: `${clean.slice(0, 200)} climbs out of the repository` };
|
|
243
|
+
const query = repo ? `?repo=${encodeURIComponent(repo)}` : '';
|
|
244
|
+
return { request: ['GET', `specs/${clean.split('/').map(encodeURIComponent).join('/')}${query}`, undefined] };
|
|
245
|
+
}
|
|
246
|
+
|
|
200
247
|
/**
|
|
201
248
|
* What the CLI says about an answer to those requests: which task and agent took the pull request, or who already has it.
|
|
202
249
|
* @param {'fix' | 'review'} action
|
|
@@ -424,3 +471,64 @@ export function chaseSummary(slug, { dryRun, chase, started = [], wouldStart = [
|
|
|
424
471
|
}
|
|
425
472
|
return [first, '', ...chaseLines(chase, slug)].join('\n');
|
|
426
473
|
}
|
|
474
|
+
|
|
475
|
+
/** A spec's tasks in a few words: "3 tasks, 2 open", or "no tasks". */
|
|
476
|
+
const specTaskCount = (tasks = []) => {
|
|
477
|
+
if (!tasks.length) return 'no tasks';
|
|
478
|
+
const open = tasks.filter((t) => t.status === 'pending').length;
|
|
479
|
+
return `${plural(tasks.length, 'task')}, ${open} open`;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* What `npx breakaway specs` prints: the repository's specs newest first, each with its work ID, status, title, and its
|
|
484
|
+
* tasks' count; with none, where specs go and how to point the board at another directory.
|
|
485
|
+
* @param {{ slug: string, dir: string, missing?: boolean, readme?: { path: string } | null, specs: any[] }} answer
|
|
486
|
+
*/
|
|
487
|
+
export function specListLines({ slug, dir, missing, readme, specs }) {
|
|
488
|
+
if (!specs.length)
|
|
489
|
+
return [
|
|
490
|
+
`No specs in ${dir} yet${missing ? `: ${slug} has no ${dir} on its default branch` : ''}.`,
|
|
491
|
+
'A spec is a Markdown file in that directory, merged like any change.',
|
|
492
|
+
`If ${slug} keeps its specs somewhere else, the owner sets it with npx breakaway repos modify ${slug} --specs <dir>.`,
|
|
493
|
+
];
|
|
494
|
+
const intro = readme ? ` (its introduction is ${readme.path})` : '';
|
|
495
|
+
const out = [`${slug}: ${plural(specs.length, 'spec')} in ${dir}${intro}`, ''];
|
|
496
|
+
for (const s of specs) {
|
|
497
|
+
const extra = s.tooLarge ? ', over 1 MB: read it on GitHub' : '';
|
|
498
|
+
out.push(
|
|
499
|
+
` ${(s.wid ?? '').padEnd(9)} ${(s.status ?? '-').padEnd(10)} ${s.title} (${specTaskCount(s.tasks)}${extra})`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
out.push('', `Read one: npx breakaway specs show <path>, like ${specs[0].path}`);
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* What `npx breakaway specs show <path>` prints: the spec's title and path, status, the commit that last changed it,
|
|
508
|
+
* its GitHub link, its Markdown (or, over 1 MB, a pointer to GitHub), and the tasks that link it.
|
|
509
|
+
* @param {any} spec
|
|
510
|
+
*/
|
|
511
|
+
export function specLines(spec) {
|
|
512
|
+
const out = [`${spec.title} (${spec.path})`, ''];
|
|
513
|
+
const row = (k, v) => v && out.push(` ${k.padEnd(11)} ${v}`);
|
|
514
|
+
row('Status', spec.status);
|
|
515
|
+
const c = spec.commit;
|
|
516
|
+
if (c)
|
|
517
|
+
row(
|
|
518
|
+
'Changed',
|
|
519
|
+
`${c.date ? `${String(c.date).slice(0, 16).replace('T', ' ')} ` : ''}in ${String(c.sha).slice(0, 7)}${c.message ? `: ${c.message}` : ''}`,
|
|
520
|
+
);
|
|
521
|
+
row('GitHub', spec.url);
|
|
522
|
+
out.push('');
|
|
523
|
+
if (spec.tooLarge || spec.text === null || spec.text === undefined)
|
|
524
|
+
out.push('Over 1 MB, too large to show here: read it on GitHub.');
|
|
525
|
+
else out.push(String(spec.text).replace(/\s+$/u, ''));
|
|
526
|
+
const tasks = spec.tasks ?? [];
|
|
527
|
+
out.push('');
|
|
528
|
+
if (tasks.length) {
|
|
529
|
+
out.push(` Tasks (${tasks.length}, ${tasks.filter((t) => t.status === 'pending').length} open)`);
|
|
530
|
+
for (const t of tasks) out.push(` ${idOf(t).padEnd(9)} ${t.status.padEnd(9)} ${t.description}`);
|
|
531
|
+
} else out.push(` No task links it yet: npx breakaway modify <ref> --spec ${spec.path}`);
|
|
532
|
+
out.push('', `Refine it: npx breakaway agents new --spec ${spec.path} "<what should change>"`);
|
|
533
|
+
return out;
|
|
534
|
+
}
|
package/scripts/tasks.mjs
CHANGED
|
@@ -54,6 +54,10 @@ import {
|
|
|
54
54
|
generalAgentRequest,
|
|
55
55
|
generalAgentSummary,
|
|
56
56
|
githubRequest,
|
|
57
|
+
specLines,
|
|
58
|
+
specListLines,
|
|
59
|
+
specRequest,
|
|
60
|
+
specsRequest,
|
|
57
61
|
ideaTask,
|
|
58
62
|
packageReleaseRequest,
|
|
59
63
|
pullAgentRequest,
|
|
@@ -148,6 +152,8 @@ Reading (list, next, claim, and add work in this checkout's repos
|
|
|
148
152
|
agents new "<prompt>" start an agent from a prompt: it makes its own task [--image <file>]… [--repo <slug>] [--force] (owner)
|
|
149
153
|
agents new --decision <ref> ["<note>"] start an agent that brings the work waiting for an answered decision in line with its answers; the board writes its prompt [--force] (owner)
|
|
150
154
|
agents new --next minor|major ["<note>"] start an agent that sets package.json to the next minor or major release; the board writes its prompt [--repo <slug>] [--force] (owner)
|
|
155
|
+
agents new --spec <path> "<what should change>" start an agent that changes a spec as you ask and brings the tasks that
|
|
156
|
+
link it in line; the board writes its prompt [--repo <slug>] [--force] (owner)
|
|
151
157
|
agents start <ref> start a Claude cloud agent on a task [--note <text>] [--force]
|
|
152
158
|
agents refine <ref> start an agent that improves a task, not builds it --note <what to look at or change> [--force]
|
|
153
159
|
agents plan [<plan>] your Claude plan and what it allows; pro, max5, or max20 picks one (owner) and sets the limits to its defaults
|
|
@@ -168,6 +174,8 @@ Reading (list, next, claim, and add work in this checkout's repos
|
|
|
168
174
|
(owner) [--note <text>] [--repo <slug>] [--force]
|
|
169
175
|
github release <pre-release> release a package's pre-release (1.4.0-main.5) as its stable on latest, as Release on the
|
|
170
176
|
GitHub page does: the board starts release.yml's stable job, and npm waits for your 2FA (owner) [--repo <slug>]
|
|
177
|
+
specs the repository's specs, newest first: each one's status and its tasks [--repo <slug>]
|
|
178
|
+
specs show <path> one spec: its status, last change, Markdown, and the tasks that link it [--repo <slug>]
|
|
171
179
|
github the checkout's repository on GitHub: open pull requests, checks, reviews, CI, deploys, alerts [--sync] [--repo <slug>]
|
|
172
180
|
hook session|wait the Claude Code session hooks a repository's .claude/settings.json runs (npx breakaway hook session)
|
|
173
181
|
health the server's state
|
|
@@ -803,11 +811,13 @@ const commands = {
|
|
|
803
811
|
if (sub === 'new') {
|
|
804
812
|
const decision = typeof opts.decision === 'string' ? opts.decision : null;
|
|
805
813
|
const next = typeof opts.next === 'string' ? opts.next : null;
|
|
814
|
+
const spec = typeof opts.spec === 'string' ? opts.spec : null;
|
|
806
815
|
const built = generalAgentRequest(args.slice(1).join(' '), {
|
|
807
816
|
// From a decision, the board runs it in the decision's repository unless --repo says otherwise.
|
|
808
817
|
repo: opts.repo ?? (decision ? null : (await checkoutRepo()).slug),
|
|
809
818
|
decision,
|
|
810
819
|
next,
|
|
820
|
+
spec,
|
|
811
821
|
force: Boolean(opts.force),
|
|
812
822
|
by: opts.as ?? setting('AGENT'),
|
|
813
823
|
});
|
|
@@ -820,7 +830,7 @@ const commands = {
|
|
|
820
830
|
const image = await upload(ref(answer.task), file);
|
|
821
831
|
if (!opts.json) console.log(`Attached ${image.name} (${Math.ceil(image.size / 1024)} KB).`);
|
|
822
832
|
}
|
|
823
|
-
print(answer, (d) => generalAgentSummary(d, { next }));
|
|
833
|
+
print(answer, (d) => generalAgentSummary(d, { next, spec }));
|
|
824
834
|
return;
|
|
825
835
|
}
|
|
826
836
|
if (sub === 'start') {
|
|
@@ -1099,6 +1109,17 @@ const commands = {
|
|
|
1099
1109
|
].join('\n'),
|
|
1100
1110
|
);
|
|
1101
1111
|
},
|
|
1112
|
+
async specs() {
|
|
1113
|
+
// A repository's specs (IDEA-31), read from its default branch on GitHub: the checkout's unless --repo names another.
|
|
1114
|
+
const { slug } = await checkoutRepo();
|
|
1115
|
+
if (args[0] === 'show') {
|
|
1116
|
+
const built = specRequest(args.slice(1).join(' ') || undefined, slug);
|
|
1117
|
+
if (built.error || !built.request) fail(built.error ?? 'bad request');
|
|
1118
|
+
print(await call(...built.request), (d) => specLines(d).join('\n'));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
print(await call(...specsRequest(slug)), (d) => specListLines(d).join('\n'));
|
|
1122
|
+
},
|
|
1102
1123
|
async features() {
|
|
1103
1124
|
const sub = args[0];
|
|
1104
1125
|
const body = () =>
|
package/src/cli-version.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* and how to update it. scripts/tasks/version.test.js fails when the copied files change and this doesn't:
|
|
5
5
|
* so it lives in the board's package (CLD-135) and the CLI imports it from here.
|
|
6
6
|
*/
|
|
7
|
-
export const CLI_VERSION =
|
|
8
|
-
export const CLI_FINGERPRINT = '
|
|
7
|
+
export const CLI_VERSION = 68;
|
|
8
|
+
export const CLI_FINGERPRINT = 'd62907097b579845';
|