breakaway 1.2.1-main.12 → 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 +10 -7
- package/scripts/tasks.mjs +5 -1
- 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
|
@@ -143,15 +143,17 @@ export function forceFields(force, by) {
|
|
|
143
143
|
/**
|
|
144
144
|
* `agents new`: the request that makes a task from a prompt and starts an agent on it. It's the checkout's repository
|
|
145
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.
|
|
146
148
|
* @param {string} prompt
|
|
147
|
-
* @param {{ repo?: string | null, force?: boolean, by?: string }} [options]
|
|
149
|
+
* @param {{ repo?: string | null, force?: boolean, by?: string, decision?: string | null }} [options]
|
|
148
150
|
*/
|
|
149
|
-
export function generalAgentRequest(prompt, { repo = null, force = false, by } = {}) {
|
|
151
|
+
export function generalAgentRequest(prompt, { repo = null, force = false, by, decision = null } = {}) {
|
|
150
152
|
const text = String(prompt ?? '').trim();
|
|
151
|
-
if (!text)
|
|
153
|
+
if (!text && !decision)
|
|
152
154
|
return { error: 'say what the agent should do: npx breakaway agents new "Tidy the docs" [--image <file>]' };
|
|
153
155
|
const body = {
|
|
154
|
-
prompt: text,
|
|
156
|
+
...(decision ? { decision, ...(text ? { note: text } : {}) } : { prompt: text }),
|
|
155
157
|
...(repo ? { repo } : {}),
|
|
156
158
|
...(force ? { force: true } : {}),
|
|
157
159
|
...(by ? { by } : {}),
|
|
@@ -161,11 +163,12 @@ export function generalAgentRequest(prompt, { repo = null, force = false, by } =
|
|
|
161
163
|
|
|
162
164
|
/**
|
|
163
165
|
* What the CLI says about a general agent's answer: the task and that it started, or why it waits (and whether Force
|
|
164
|
-
* start could skip that).
|
|
165
|
-
* @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
|
|
166
168
|
*/
|
|
167
|
-
export function generalAgentSummary({ task, run, waiting, forceable }) {
|
|
169
|
+
export function generalAgentSummary({ task, run, waiting, forceable, already }) {
|
|
168
170
|
const id = task.wid ?? task.short;
|
|
171
|
+
if (!run && already) return `${id} already refines from these answers: ${already}.`;
|
|
169
172
|
if (run) return `Started ${run.agent ? `${run.agent} ` : 'an agent '}on ${id}${run.url ? `: ${run.url}` : ''}`;
|
|
170
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` : ''}`;
|
|
171
174
|
}
|
package/scripts/tasks.mjs
CHANGED
|
@@ -138,6 +138,7 @@ Reading (list, next, claim, and add work in this checkout's repos
|
|
|
138
138
|
activity recent changes, newest first [--limit <n>]
|
|
139
139
|
agents cloud agents: what's running, what's waiting to start
|
|
140
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)
|
|
141
142
|
agents start <ref> start a Claude cloud agent on a task [--note <text>] [--force]
|
|
142
143
|
agents refine <ref> start an agent that improves a task, not builds it --note <what to look at or change> [--force]
|
|
143
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
|
|
@@ -762,8 +763,11 @@ const commands = {
|
|
|
762
763
|
return;
|
|
763
764
|
}
|
|
764
765
|
if (sub === 'new') {
|
|
766
|
+
const decision = typeof opts.decision === 'string' ? opts.decision : null;
|
|
765
767
|
const built = generalAgentRequest(args.slice(1).join(' '), {
|
|
766
|
-
|
|
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,
|
|
767
771
|
force: Boolean(opts.force),
|
|
768
772
|
by: opts.as ?? setting('AGENT'),
|
|
769
773
|
});
|
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
|
+
}
|