breakaway 1.2.1-main.11 → 1.2.1-main.13
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 +44 -11
- package/scripts/tasks.mjs +17 -2
- package/src/cli-version.js +2 -2
- package/src/decision.js +72 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.2.1-main.
|
|
3
|
+
"version": "1.2.1-main.13",
|
|
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
|
@@ -79,7 +79,7 @@ export const FIX_PROBLEMS = ['conflicts', 'failing', 'review'];
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* The request behind `npx breakaway github fix <n>` and `github review <n>` (BRK-81): the pull request page's "Fix with an
|
|
82
|
-
* agent" and "Safe to merge?" buttons (`POST github/pulls/<n>/fix` and `/review`). It names the checkout's repository
|
|
82
|
+
* agent" and "Safe to merge?" or "Review with an agent" buttons (`POST github/pulls/<n>/fix` and `/review`). It names the checkout's repository
|
|
83
83
|
* like `github` does. Returns an error message instead when the number or `problem` can't be right.
|
|
84
84
|
* @param {'fix' | 'review'} action
|
|
85
85
|
* @param {string | number | undefined} number
|
|
@@ -96,11 +96,40 @@ export function pullAgentRequest(action, number, { repo = null, problem, note, f
|
|
|
96
96
|
...(repo ? { repo } : {}),
|
|
97
97
|
...(problem ? { problem } : {}),
|
|
98
98
|
...(typeof note === 'string' && note.trim() ? { note } : {}),
|
|
99
|
-
|
|
99
|
+
// Review with an agent is the owner's, so a review always says who asks (BRK-111); a fix only when forcing.
|
|
100
|
+
...(action === 'review' ? { ...(force ? { force: true } : {}), ...(by ? { by } : {}) } : forceFields(force, by)),
|
|
100
101
|
};
|
|
101
102
|
return { request: ['POST', `github/pulls/${n}/${action}`, body] };
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
export const REVIEW_VERDICTS = ['ready', 'follow-up', 'changes'];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `npx breakaway review <ID> --verdict ready|follow-up|changes "<note>"` (BRK-111): an agent's answer on the pull
|
|
109
|
+
* request that closes its task. The board adds it to the task as a comment and keeps it for the pull request page.
|
|
110
|
+
* `pr` picks the pull request when the task has several open.
|
|
111
|
+
* @param {string | undefined} ref
|
|
112
|
+
* @param {string | undefined} verdict
|
|
113
|
+
* @param {string | undefined} note
|
|
114
|
+
* @param {{ by?: string, pr?: string | number }} [options]
|
|
115
|
+
*/
|
|
116
|
+
export function reviewRequest(ref, verdict, note, { by, pr } = {}) {
|
|
117
|
+
if (!ref) return { error: 'say which task: npx breakaway review <task> --verdict ready "<note>"' };
|
|
118
|
+
if (!REVIEW_VERDICTS.includes(String(verdict)))
|
|
119
|
+
return { error: `say the verdict: --verdict ${REVIEW_VERDICTS.join('|')}` };
|
|
120
|
+
const text = String(note ?? '').trim();
|
|
121
|
+
if (!text) return { error: 'say what you found: the note is the review (Markdown)' };
|
|
122
|
+
const n = pr === undefined ? null : String(pr).replace(/^#/u, '');
|
|
123
|
+
if (n !== null && !/^[1-9]\d{0,8}$/u.test(n)) return { error: '--pr is a pull request number' };
|
|
124
|
+
return {
|
|
125
|
+
request: [
|
|
126
|
+
'POST',
|
|
127
|
+
`tasks/${encodeURIComponent(ref)}/review`,
|
|
128
|
+
{ verdict, note: text, ...(by ? { by } : {}), ...(n ? { pr: Number(n) } : {}) },
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
104
133
|
/**
|
|
105
134
|
* Force start on a request that starts an agent (BRK-107): `force`, and who is asking, so the board can refuse an
|
|
106
135
|
* agent's name (only the owner forces a start). Nothing when it isn't forced.
|
|
@@ -114,15 +143,17 @@ export function forceFields(force, by) {
|
|
|
114
143
|
/**
|
|
115
144
|
* `agents new`: the request that makes a task from a prompt and starts an agent on it. It's the checkout's repository
|
|
116
145
|
* unless `--repo` names another. It always says who asks, so the board refuses an agent's name: only the owner starts one.
|
|
146
|
+
* With `decision` (`agents new --decision <ID> ["<note>"]`, BRK-110) the board writes the prompt from that answered
|
|
147
|
+
* decision, in the decision's repository, and the text is the owner's note under it.
|
|
117
148
|
* @param {string} prompt
|
|
118
|
-
* @param {{ repo?: string | null, force?: boolean, by?: string }} [options]
|
|
149
|
+
* @param {{ repo?: string | null, force?: boolean, by?: string, decision?: string | null }} [options]
|
|
119
150
|
*/
|
|
120
|
-
export function generalAgentRequest(prompt, { repo = null, force = false, by } = {}) {
|
|
151
|
+
export function generalAgentRequest(prompt, { repo = null, force = false, by, decision = null } = {}) {
|
|
121
152
|
const text = String(prompt ?? '').trim();
|
|
122
|
-
if (!text)
|
|
153
|
+
if (!text && !decision)
|
|
123
154
|
return { error: 'say what the agent should do: npx breakaway agents new "Tidy the docs" [--image <file>]' };
|
|
124
155
|
const body = {
|
|
125
|
-
prompt: text,
|
|
156
|
+
...(decision ? { decision, ...(text ? { note: text } : {}) } : { prompt: text }),
|
|
126
157
|
...(repo ? { repo } : {}),
|
|
127
158
|
...(force ? { force: true } : {}),
|
|
128
159
|
...(by ? { by } : {}),
|
|
@@ -132,11 +163,12 @@ export function generalAgentRequest(prompt, { repo = null, force = false, by } =
|
|
|
132
163
|
|
|
133
164
|
/**
|
|
134
165
|
* What the CLI says about a general agent's answer: the task and that it started, or why it waits (and whether Force
|
|
135
|
-
* start could skip that).
|
|
136
|
-
* @param {{ task: { wid?: string, short?: string }, run?: { url?: string, agent?: string } | null, waiting?: string | null, forceable?: boolean }} answer
|
|
166
|
+
* start could skip that), or, from a decision, the open one that already has it.
|
|
167
|
+
* @param {{ task: { wid?: string, short?: string }, run?: { url?: string, agent?: string } | null, waiting?: string | null, forceable?: boolean, already?: string | null }} answer
|
|
137
168
|
*/
|
|
138
|
-
export function generalAgentSummary({ task, run, waiting, forceable }) {
|
|
169
|
+
export function generalAgentSummary({ task, run, waiting, forceable, already }) {
|
|
139
170
|
const id = task.wid ?? task.short;
|
|
171
|
+
if (!run && already) return `${id} already refines from these answers: ${already}.`;
|
|
140
172
|
if (run) return `Started ${run.agent ? `${run.agent} ` : 'an agent '}on ${id}${run.url ? `: ${run.url}` : ''}`;
|
|
141
173
|
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` : ''}`;
|
|
142
174
|
}
|
|
@@ -145,12 +177,13 @@ export function generalAgentSummary({ task, run, waiting, forceable }) {
|
|
|
145
177
|
* What the CLI says about an answer to those requests: which task and agent took the pull request, or who already has it.
|
|
146
178
|
* @param {'fix' | 'review'} action
|
|
147
179
|
* @param {string | number} number
|
|
148
|
-
* @param {{ task: { wid?: string, short?: string }, run?: { url?: string, agent?: string } | null, already?: string | null }} answer
|
|
180
|
+
* @param {{ task: { wid?: string, short?: string }, run?: { url?: string, agent?: string, kind?: string } | null, already?: string | null }} answer
|
|
149
181
|
*/
|
|
150
182
|
export function pullAgentSummary(action, number, { task, run, already }) {
|
|
151
183
|
const id = task.wid ?? task.short;
|
|
152
184
|
if (!run) return `${id} already has it: ${already}.`;
|
|
153
|
-
const what =
|
|
185
|
+
const what =
|
|
186
|
+
action === 'fix' ? `fixing #${number}` : run.kind === 'pr-review' ? `reviewing #${number}` : `testing #${number}`;
|
|
154
187
|
return `Started ${run.agent ? `${run.agent}, ` : 'an agent '}${what} on ${id}${run.url ? `: ${run.url}` : ''}`;
|
|
155
188
|
}
|
|
156
189
|
|
package/scripts/tasks.mjs
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
ideaTask,
|
|
52
52
|
pullAgentRequest,
|
|
53
53
|
pullAgentSummary,
|
|
54
|
+
reviewRequest,
|
|
54
55
|
staleCliWarning,
|
|
55
56
|
unknownSubcommand,
|
|
56
57
|
} from './tasks/cli.js';
|
|
@@ -137,6 +138,7 @@ Reading (list, next, claim, and add work in this checkout's repos
|
|
|
137
138
|
activity recent changes, newest first [--limit <n>]
|
|
138
139
|
agents cloud agents: what's running, what's waiting to start
|
|
139
140
|
agents new "<prompt>" start an agent from a prompt: it makes its own task [--image <file>]… [--repo <slug>] [--force] (owner)
|
|
141
|
+
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)
|
|
140
142
|
agents start <ref> start a Claude cloud agent on a task [--note <text>] [--force]
|
|
141
143
|
agents refine <ref> start an agent that improves a task, not builds it --note <what to look at or change> [--force]
|
|
142
144
|
agents plan [<plan>] your Claude plan and what it allows; pro, max5, or max20 picks one (owner) and sets the limits to its defaults
|
|
@@ -145,7 +147,9 @@ Reading (list, next, claim, and add work in this checkout's repos
|
|
|
145
147
|
routines run <slug> run one now: makes a RUN task and starts an agent on it [--note <text>] [--force]
|
|
146
148
|
horizon close close now: finished tasks go to the archive, next becomes now, later becomes next [--dry-run]
|
|
147
149
|
github fix <n> start an agent on a pull request's conflicts, failing checks, or review comments (owner) [--problem conflicts|failing|review] [--note <text>] [--repo <slug>] [--force]
|
|
148
|
-
github review <n> start an agent that
|
|
150
|
+
github review <n> start an agent that reviews a pull request that can merge as it stands, on the task it closes, as
|
|
151
|
+
Review with an agent does; on a Dependabot one it tests the update, as Safe to merge? does
|
|
152
|
+
(owner) [--note <text>] [--repo <slug>] [--force]
|
|
149
153
|
github the checkout's repository on GitHub: open pull requests, checks, reviews, CI, deploys, alerts [--sync] [--repo <slug>]
|
|
150
154
|
hook session|wait the Claude Code session hooks a repository's .claude/settings.json runs (npx breakaway hook session)
|
|
151
155
|
health the server's state
|
|
@@ -157,6 +161,8 @@ Working
|
|
|
157
161
|
refuses another repository's task unless --repo names it
|
|
158
162
|
release <ref> give it back [--force]
|
|
159
163
|
comment <ref> <text> add a comment (signed with your agent name); note is the same command
|
|
164
|
+
review <ref> --verdict ready|follow-up|changes <note> your review of the pull request that closes the task you
|
|
165
|
+
hold: a comment on it, and the review on the pull request's page (the note is Markdown) [--pr <n>]
|
|
160
166
|
done <ref> finish it [--note <text>] [--pr <url>]
|
|
161
167
|
add <description> new task; gets the next work ID for its project
|
|
162
168
|
--project <p> --tag <t>… --priority H|M|L --horizon now|next|later
|
|
@@ -757,8 +763,11 @@ const commands = {
|
|
|
757
763
|
return;
|
|
758
764
|
}
|
|
759
765
|
if (sub === 'new') {
|
|
766
|
+
const decision = typeof opts.decision === 'string' ? opts.decision : null;
|
|
760
767
|
const built = generalAgentRequest(args.slice(1).join(' '), {
|
|
761
|
-
|
|
768
|
+
// From a decision, the board runs it in the decision's repository unless --repo says otherwise.
|
|
769
|
+
repo: opts.repo ?? (decision ? null : (await checkoutRepo()).slug),
|
|
770
|
+
decision,
|
|
762
771
|
force: Boolean(opts.force),
|
|
763
772
|
by: opts.as ?? setting('AGENT'),
|
|
764
773
|
});
|
|
@@ -1076,6 +1085,12 @@ const commands = {
|
|
|
1076
1085
|
unmarkSession(task);
|
|
1077
1086
|
print(task, (t) => `Released ${ref(t)}.`);
|
|
1078
1087
|
},
|
|
1088
|
+
async review() {
|
|
1089
|
+
const built = reviewRequest(args[0], opts.verdict, args.slice(1).join(' '), { by: agent(), pr: opts.pr });
|
|
1090
|
+
if (built.error || !built.request) fail(built.error ?? 'bad request');
|
|
1091
|
+
const { review, task } = await call(...built.request);
|
|
1092
|
+
print({ review, task }, (r) => `Left your review of #${r.review.pr} on ${ref(r.task)}: ${r.review.label}.`);
|
|
1093
|
+
},
|
|
1079
1094
|
// The board's `annotate` route is the comments route's alias; using it keeps this working on a board that hasn't deployed /comments yet.
|
|
1080
1095
|
async comment() {
|
|
1081
1096
|
const text = args.slice(1).join(' ');
|
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 = 54;
|
|
8
|
+
export const CLI_FINGERPRINT = 'c5c5e4b72de52bfb';
|
package/src/decision.js
CHANGED
|
@@ -239,3 +239,75 @@ export function summarize(questions, answers) {
|
|
|
239
239
|
});
|
|
240
240
|
return `Decided by the owner: ${parts.join('; ')}`;
|
|
241
241
|
}
|
|
242
|
+
|
|
243
|
+
/** Tags that aren't a feature's (IDEA-28 section 1): the board's own, horizons, and release tags. */
|
|
244
|
+
const NOT_FEATURES = new Set(['agent', 'owner', 'decide', 'idea', 'general']);
|
|
245
|
+
export const featureTags = (tags) =>
|
|
246
|
+
(tags ?? []).filter((t) => !NOT_FEATURES.has(t) && !t.startsWith('horizon-') && !/^v\d/u.test(t));
|
|
247
|
+
|
|
248
|
+
/** One answer as words, in full but for a long open answer. */
|
|
249
|
+
function spell(q, answer) {
|
|
250
|
+
if (!answer) return 'no answer';
|
|
251
|
+
if (q.type === 'open') return clip(answer.value.trim(), 1500);
|
|
252
|
+
const label = (id) =>
|
|
253
|
+
id === 'other' ? `something else: ${answer.other}` : (q.options.find((o) => o.id === id)?.label ?? id);
|
|
254
|
+
if (q.type === 'choice') return label(answer.value);
|
|
255
|
+
if (q.type === 'multi') return answer.value.length ? answer.value.map(label).join(', ') : 'none';
|
|
256
|
+
if (q.type === 'rank') return answer.value.map((id, i) => `${i + 1}. ${label(id)}`).join(', ');
|
|
257
|
+
if (q.type === 'scale')
|
|
258
|
+
return `${answer.value} (${q.min}${q.minLabel ? ` ${q.minLabel}` : ''} to ${q.max}${q.maxLabel ? ` ${q.maxLabel}` : ''})`;
|
|
259
|
+
return String(answer.value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The longest the prompt may be: a task's description. */
|
|
263
|
+
const MAX_BRIEF = 10000;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Refine from the answers (docs/specs/IDEA-30-new-agent.md, section 8): the prompt the board writes for a general
|
|
267
|
+
* agent that brings the work waiting for an answered decision in line with its answers. `decision` is the decision's
|
|
268
|
+
* task (its `ref` is its work ID or short ID), `waiting` the open tasks that depend on it, and `note` the owner's,
|
|
269
|
+
* which goes under the board's prompt. Returns the task's title and description.
|
|
270
|
+
* @param {{ ref: string, description: string, spec?: string | null, questions: any[], answers: Record<string, any> }} decision
|
|
271
|
+
* @param {{ ref: string, description: string, tags?: string[], spec?: string | null }[]} waiting
|
|
272
|
+
* @param {string | null} [note]
|
|
273
|
+
*/
|
|
274
|
+
export function refinePrompt(decision, waiting, note = null) {
|
|
275
|
+
const title = clip(`Refine from the answers to ${decision.ref}: ${decision.description}`, 200);
|
|
276
|
+
const questions = decision.questions.flatMap((q, i) => {
|
|
277
|
+
const answer = decision.answers[q.id];
|
|
278
|
+
return [
|
|
279
|
+
`${i + 1}. ${q.prompt.trim()}`,
|
|
280
|
+
` Answer: ${spell(q, answer)}`,
|
|
281
|
+
...(answer?.comment ? [` The owner's note: ${answer.comment.trim()}`] : []),
|
|
282
|
+
];
|
|
283
|
+
});
|
|
284
|
+
const specs = [...new Set([decision.spec, ...waiting.map((t) => t.spec)].filter(Boolean))];
|
|
285
|
+
const held = waiting.map((t) => {
|
|
286
|
+
const features = featureTags(t.tags);
|
|
287
|
+
return `- ${t.ref}: ${t.description}${features.length ? ` (feature: ${features.join(', ')})` : ''}`;
|
|
288
|
+
});
|
|
289
|
+
const after = [
|
|
290
|
+
'',
|
|
291
|
+
'What to do',
|
|
292
|
+
`- Change the tasks waiting for ${decision.ref}, and their dependencies, so they match the answers (the cross-task edits a general agent may make, each change noted).`,
|
|
293
|
+
`- Update ${specs.length ? 'the spec' : 'any spec the tasks link'} to match, in one pull request that closes your own task.`,
|
|
294
|
+
'- Add the tasks the answers need, filled in and depending on what they wait for.',
|
|
295
|
+
'- Ask a new decision for anything the answers leave open. Never change these answers: only the owner does.',
|
|
296
|
+
'- If nothing in the repository needs to change, comment what you changed on the board, task by task, and release your task.',
|
|
297
|
+
...(note && String(note).trim() ? ['', 'Note from the owner:', String(note).trim().slice(0, 4000)] : []),
|
|
298
|
+
];
|
|
299
|
+
const intro = `The owner answered the decision on ${decision.ref} (${decision.description}). Bring the work waiting for it in line with the answers.`;
|
|
300
|
+
const rest = [
|
|
301
|
+
'',
|
|
302
|
+
`Waiting for ${decision.ref}`,
|
|
303
|
+
...(held.length ? held : ['- Nothing open waits for it: look for tasks and specs the answers change.']),
|
|
304
|
+
...(specs.length ? ['', specs.length === 1 ? 'Spec' : 'Specs', ...specs.map((s) => `- ${s}`)] : []),
|
|
305
|
+
...after,
|
|
306
|
+
].join('\n');
|
|
307
|
+
// Too long for a description: the answers give way first, since they stay on the decision for the agent to read.
|
|
308
|
+
const more = `\n… (the rest is on ${decision.ref}: npx breakaway show ${decision.ref})`;
|
|
309
|
+
const answers = ['', 'Questions and answers', ...questions].join('\n');
|
|
310
|
+
const room = MAX_BRIEF - intro.length - rest.length - 1;
|
|
311
|
+
const middle = answers.length <= room ? answers : `${answers.slice(0, Math.max(0, room - more.length))}${more}`;
|
|
312
|
+
return { title, brief: `${intro}${middle}\n${rest}`.slice(0, MAX_BRIEF) };
|
|
313
|
+
}
|