breakaway 2.0.0-main.52 → 2.0.0-main.53
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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/tasks/cli.js +1 -1
- package/scripts/tasks/infra-act.js +108 -0
- package/scripts/tasks.mjs +26 -0
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "2.0.0-main.
|
|
3
|
+
"version": "2.0.0-main.53",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "breakaway",
|
|
9
|
-
"version": "2.0.0-main.
|
|
9
|
+
"version": "2.0.0-main.53",
|
|
10
10
|
"license": "FSL-1.1-Apache-2.0",
|
|
11
11
|
"bin": {
|
|
12
12
|
"breakaway": "scripts/tasks.mjs"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "2.0.0-main.
|
|
3
|
+
"version": "2.0.0-main.53",
|
|
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
|
@@ -29,7 +29,7 @@ export const SUBCOMMANDS = {
|
|
|
29
29
|
'listen',
|
|
30
30
|
],
|
|
31
31
|
specs: ['list', 'show'],
|
|
32
|
-
infra: ['init', 'runner', 'check', ...INFRA_READS],
|
|
32
|
+
infra: ['init', 'runner', 'check', 'act', ...INFRA_READS],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
/** Commands that take nothing after their name, so a word there is a mistake (an old copy's missing subcommand, say). */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npx breakaway infra act <environment> <resource> scale <n>|restart` (CLI-24, docs/specs/IDEA-19-architect.md,
|
|
3
|
+
* "Envelopes"): a runbook's agent asks for one scale or restart, for the run it holds. It's the one change a run may
|
|
4
|
+
* ask for, and it's the CLI's only: Architect's MCP tools stay read only.
|
|
5
|
+
*
|
|
6
|
+
* The request is POST /api/infra/envelopes/<environment>/act (BRK-186) with { resource, change, value, task, by }: the
|
|
7
|
+
* board checks the agent holds the runbook's run, builds the plan itself, and answers whether the envelope the owner
|
|
8
|
+
* approved covers it (the executor applies it) or it waits for the owner. The board's words go through unchanged when
|
|
9
|
+
* it refuses. The requests go through `get` and `post`, which the CLI gives (and the tests mock), and which answer
|
|
10
|
+
* `{ ok, status, data }` for any answer.
|
|
11
|
+
*/
|
|
12
|
+
import { PLAN_STATE_LABELS } from './infra-read.js';
|
|
13
|
+
|
|
14
|
+
/** An act the CLI won't send, with what to do instead. */
|
|
15
|
+
export class InfraActError extends Error {}
|
|
16
|
+
const bad = (message) => {
|
|
17
|
+
throw new InfraActError(message);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const enc = encodeURIComponent;
|
|
21
|
+
const USAGE = 'infra act <environment> <resource> scale <n>|restart';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What follows `infra act`, as the act route's body.
|
|
25
|
+
* @param {string[]} args
|
|
26
|
+
* @returns {{ environment: string, resource: string, change: 'scale' | 'restart', value: number | null }}
|
|
27
|
+
*/
|
|
28
|
+
export function parseAct(args) {
|
|
29
|
+
const [environment, resource, change, value, ...rest] = args;
|
|
30
|
+
if (!environment || !resource || !change)
|
|
31
|
+
bad(`${USAGE}: name the environment, the resource, and the change, like infra act production widgets-api scale 4.`);
|
|
32
|
+
if (change === 'restart') {
|
|
33
|
+
if (value !== undefined) bad(`${USAGE}: a restart takes no number.`);
|
|
34
|
+
return { environment, resource, change: 'restart', value: null };
|
|
35
|
+
}
|
|
36
|
+
if (change !== 'scale') bad(`${USAGE}: the change is scale or restart, not ${change}.`);
|
|
37
|
+
if (value === undefined || !/^\d{1,5}$/u.test(value)) bad(`${USAGE}: scale needs a whole number, like scale 4.`);
|
|
38
|
+
if (rest.length) bad(`${USAGE}: one change at a time.`);
|
|
39
|
+
return { environment, resource, change: 'scale', value: Number(value) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The run this agent holds: `--task` when it names one, else the only routine run in this repository it has claimed.
|
|
44
|
+
* The board checks it's a runbook's either way.
|
|
45
|
+
* @param {{ task?: string, get: (path: string) => Promise<{ ok: boolean, status: number, data: any }>,
|
|
46
|
+
* agent: string, inRepo: (task: any) => boolean }} ctx
|
|
47
|
+
*/
|
|
48
|
+
export async function heldRun({ task, get, agent, inRepo }) {
|
|
49
|
+
if (task) return String(task);
|
|
50
|
+
const res = await get('tasks?status=pending');
|
|
51
|
+
if (!res.ok) bad(res.data?.error ?? `HTTP ${res.status}`);
|
|
52
|
+
const runs = (res.data.tasks ?? []).filter((t) => t.claim === agent && t.project === 'routines' && inRepo(t));
|
|
53
|
+
if (runs.length === 1) return runs[0].wid ?? runs[0].uuid;
|
|
54
|
+
if (!runs.length)
|
|
55
|
+
bad(
|
|
56
|
+
`${agent} holds no routine run here: only a runbook’s agent acts in an envelope, for the run it holds. Name it with --task <ID>.`,
|
|
57
|
+
);
|
|
58
|
+
return bad(`${agent} holds ${runs.length} runs (${runs.map((t) => t.wid).join(', ')}): name one with --task <ID>.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What the board answered, in words: inside the envelope it applies without a press; outside it waits for the owner.
|
|
63
|
+
* @param {{ act: { inside: boolean, why: string, plan: any } }} data
|
|
64
|
+
* @param {{ environment: string, resource: string, change: string, value: number | null }} asked
|
|
65
|
+
*/
|
|
66
|
+
export function actText({ act }, asked) {
|
|
67
|
+
const what = asked.change === 'scale' ? `Scale ${asked.resource} to ${asked.value}` : `Restart ${asked.resource}`;
|
|
68
|
+
const plan = act.plan ?? {};
|
|
69
|
+
const state = PLAN_STATE_LABELS[plan.state] ?? plan.state;
|
|
70
|
+
const out = act.inside
|
|
71
|
+
? [
|
|
72
|
+
`${what} in ${asked.environment}: inside its envelope (${act.why}).`,
|
|
73
|
+
`${plan.id} · ${state}: it applies without a press.`,
|
|
74
|
+
]
|
|
75
|
+
: [
|
|
76
|
+
`${what} in ${asked.environment}: waits for the owner (${act.why}).`,
|
|
77
|
+
`${plan.id} · ${state}: the owner approves or rejects it on the board.`,
|
|
78
|
+
];
|
|
79
|
+
out.push('', `The plan: npx breakaway infra plan ${plan.id}`);
|
|
80
|
+
return out.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Runs `infra act`. `code` is what the CLI exits with: 1 when the board refuses the act.
|
|
85
|
+
* @param {string[]} args what follows `infra act`
|
|
86
|
+
* @param {{ get: (path: string) => Promise<{ ok: boolean, status: number, data: any }>,
|
|
87
|
+
* post: (path: string, body: any) => Promise<{ ok: boolean, status: number, data: any }>,
|
|
88
|
+
* repo: string | null, agent: string, opts?: Record<string, any>, inRepo?: (task: any) => boolean }} ctx
|
|
89
|
+
* @returns {Promise<{ data: any, text: string, code: number }>}
|
|
90
|
+
*/
|
|
91
|
+
export async function infraAct(args, { get, post, repo, agent, opts = {}, inRepo = () => true }) {
|
|
92
|
+
const asked = parseAct(args);
|
|
93
|
+
const task = await heldRun({ task: opts.task, get, agent, inRepo });
|
|
94
|
+
const res = await post(`infra/envelopes/${enc(asked.environment)}/act`, {
|
|
95
|
+
resource: asked.resource,
|
|
96
|
+
change: asked.change,
|
|
97
|
+
value: asked.value,
|
|
98
|
+
task,
|
|
99
|
+
by: agent,
|
|
100
|
+
...(repo ? { repo } : {}),
|
|
101
|
+
});
|
|
102
|
+
if (res.ok) return { data: res.data, text: actText(res.data, asked), code: 0 };
|
|
103
|
+
if (res.status === 0) bad(`can’t reach the board (${res.data?.error ?? 'no answer'}).`);
|
|
104
|
+
if (res.status === 404 && /^no route for/u.test(String(res.data?.error ?? '')))
|
|
105
|
+
bad('this board doesn’t have envelopes yet: its owner updates it to a release that does, then try again.');
|
|
106
|
+
const error = res.data?.error ?? `HTTP ${res.status}`;
|
|
107
|
+
return { data: { status: res.status, error }, text: `The board refused it: ${error}`, code: 1 };
|
|
108
|
+
}
|
package/scripts/tasks.mjs
CHANGED
|
@@ -429,6 +429,10 @@ Infrastructure (init and add need no board: run them in the checkout of the re
|
|
|
429
429
|
and policy.json, naming the file, line, and field that's wrong), then show the plan each valid
|
|
430
430
|
file would make from what runs now and the policy's answer; the board keeps none of it
|
|
431
431
|
(exits 1 if a file doesn't check or the board refuses one)
|
|
432
|
+
infra act <environment> <resource> scale <n>|restart a runbook's agent asks for one scale or restart, for the
|
|
433
|
+
run it holds (--task <ID>, else the one routine run it has claimed here); it says whether
|
|
434
|
+
the owner's envelope covers it or it waits for the owner, and names the plan. Only a
|
|
435
|
+
runbook's run may; it's the one change a run may ask for, and it never applies anything
|
|
432
436
|
infra add [<template>] [<environment>] [<input>=<value> …] a golden path: write the change one of the owner's
|
|
433
437
|
templates makes (.github/breakaway-infra/templates/<name>/, else breakaway's example of that
|
|
434
438
|
name) into the environment's file and the code it needs, for a pull request; it never
|
|
@@ -1436,6 +1440,28 @@ const commands = {
|
|
|
1436
1440
|
process.exitCode = result.code;
|
|
1437
1441
|
return;
|
|
1438
1442
|
}
|
|
1443
|
+
if (args[0] === 'act') {
|
|
1444
|
+
// infra act (CLI-24): a runbook's agent asks for one scale or restart, for the run it holds; the board decides.
|
|
1445
|
+
const { infraAct, InfraActError } = await import('./tasks/infra-act.js');
|
|
1446
|
+
let result;
|
|
1447
|
+
try {
|
|
1448
|
+
result = await infraAct(args.slice(1), {
|
|
1449
|
+
get: (path) => call('GET', path, undefined, { raw: true }),
|
|
1450
|
+
post: (path, body) => call('POST', path, body, { raw: true }),
|
|
1451
|
+
repo: slug,
|
|
1452
|
+
agent: agent(),
|
|
1453
|
+
opts,
|
|
1454
|
+
inRepo: (t) => !slug || inRepo(t, slug, repoContext.registry),
|
|
1455
|
+
});
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
if (!(error instanceof InfraActError)) throw error;
|
|
1458
|
+
if (opts.json) console.log(JSON.stringify({ error: error.message }, null, 2));
|
|
1459
|
+
fail(error.message);
|
|
1460
|
+
}
|
|
1461
|
+
print(result.data, () => result.text);
|
|
1462
|
+
process.exitCode = result.code;
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1439
1465
|
// Architect's reads (CLI-13): every request is a GET, so an agent's token reads and never writes.
|
|
1440
1466
|
let result;
|
|
1441
1467
|
try {
|