balladeer 1.0.0 → 1.0.1
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/README.md +31 -18
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +148 -9
- package/dist/client.d.ts +23 -1
- package/dist/client.js +29 -1
- package/dist/commands/affected.js +2 -1
- package/dist/commands/discover.js +12 -4
- package/dist/commands/explain.d.ts +1 -1
- package/dist/commands/explain.js +1 -1
- package/dist/commands/invite.js +2 -1
- package/dist/commands/prepare.d.ts +74 -0
- package/dist/commands/prepare.js +217 -0
- package/dist/commands/propose.d.ts +10 -0
- package/dist/commands/propose.js +25 -3
- package/dist/commands/session.d.ts +35 -0
- package/dist/commands/session.js +118 -0
- package/dist/commands/setup.d.ts +23 -0
- package/dist/commands/setup.js +157 -28
- package/dist/commands/status.d.ts +16 -0
- package/dist/commands/status.js +67 -7
- package/dist/commands/whoami.js +2 -1
- package/dist/conventions.d.ts +9 -1
- package/dist/conventions.js +9 -1
- package/dist/copy.d.ts +79 -3
- package/dist/copy.js +187 -5
- package/dist/desktop-config.d.ts +85 -0
- package/dist/desktop-config.js +217 -0
- package/dist/git.d.ts +15 -0
- package/dist/git.js +23 -0
- package/dist/legacy.d.ts +41 -0
- package/dist/legacy.js +143 -0
- package/dist/local-time.d.ts +66 -0
- package/dist/local-time.js +84 -0
- package/dist/mcp-config.d.ts +10 -0
- package/dist/mcp-config.js +8 -4
- package/dist/session.d.ts +84 -0
- package/dist/session.js +135 -0
- package/dist/store.d.ts +10 -0
- package/dist/store.js +15 -3
- package/dist/wire.d.ts +88 -2
- package/dist/wire.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,11 +19,12 @@ The agent reads the setup instructions, runs the command, and hands you the two
|
|
|
19
19
|
do. To run it yourself instead:
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
npx balladeer@
|
|
22
|
+
npx -y balladeer@latest setup
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
Use `@latest` so a new session picks up the current command. Balladeer tells an older command when
|
|
26
|
+
an update is available. Run `balladeer setup --refresh` to update an existing repository connection
|
|
27
|
+
and its instructions without replacing other tools' settings.
|
|
27
28
|
|
|
28
29
|
Run it inside the repository you want to protect. It prints the boundary explanation below, then one
|
|
29
30
|
step at a time, and it exits within seconds rather than blocking on anything.
|
|
@@ -107,20 +108,22 @@ your own branch protection.
|
|
|
107
108
|
|
|
108
109
|
## Commands
|
|
109
110
|
|
|
110
|
-
| Command | What it does
|
|
111
|
-
| ------------------------ |
|
|
112
|
-
| `balladeer setup` | Run all five steps, then report what a person still has to do
|
|
113
|
-
| `balladeer repositories` | List what this machine could add, marking the one you are in and the ones in
|
|
114
|
-
| `balladeer invite` | Invite teammates by email, and say per address whether the email went
|
|
115
|
-
| `balladeer status` | Report this repository over its agent connection, and the workspace
|
|
116
|
-
| `balladeer status <id>` | Report one promise: whether it is holding, and if not, what the run reported
|
|
117
|
-
| `balladeer
|
|
118
|
-
| `balladeer
|
|
119
|
-
| `balladeer
|
|
120
|
-
| `balladeer
|
|
121
|
-
| `balladeer
|
|
122
|
-
| `balladeer
|
|
123
|
-
| `balladeer
|
|
111
|
+
| Command | What it does |
|
|
112
|
+
| ------------------------ | -------------------------------------------------------------------------------- |
|
|
113
|
+
| `balladeer setup` | Run all five steps, then report what a person still has to do |
|
|
114
|
+
| `balladeer repositories` | List what this machine could add, marking the one you are in and the ones in |
|
|
115
|
+
| `balladeer invite` | Invite teammates by email, and say per address whether the email went |
|
|
116
|
+
| `balladeer status` | Report this repository over its agent connection, and the workspace |
|
|
117
|
+
| `balladeer status <id>` | Report one promise: whether it is holding, and if not, what the run reported |
|
|
118
|
+
| `balladeer prepare <id>` | Prepare a promise's one-time qualification setup, and write it where CI reads it |
|
|
119
|
+
| `balladeer propose` | Propose one promise from a proposal file, over that connection |
|
|
120
|
+
| `balladeer discover` | File a whole catalog, up to ten promises from one file, behind one review link |
|
|
121
|
+
| `balladeer touch-map` | Record which files each promise's verifier runs, on this machine only |
|
|
122
|
+
| `balladeer affected` | Say which promises the files you name touch, out of that record |
|
|
123
|
+
| `balladeer session` | Print the id for this piece of work, and the line to write into the commit |
|
|
124
|
+
| `balladeer mcp` | Forward one MCP session over stdio using this repository's connection |
|
|
125
|
+
| `balladeer explain` | What Balladeer can and cannot see, where to watch it, and what leaving costs |
|
|
126
|
+
| `balladeer whoami` | Report the stored session's workspace, role, scopes, and expiry |
|
|
124
127
|
|
|
125
128
|
`--json` emits one object per step on stdout and nothing else. `--wait` makes `setup` poll for the
|
|
126
129
|
approval instead of exiting; without it the command exits and a later run finishes the pairing.
|
|
@@ -148,7 +151,17 @@ once they exist, and what stopping costs: your tests are yours, they keep runnin
|
|
|
148
151
|
administrator can download everything Balladeer holds at any time.
|
|
149
152
|
|
|
150
153
|
Exit codes: 0 progress reported truthfully, 2 pairing expired or denied or already claimed, 3 this
|
|
151
|
-
copy is too old for the server, 4 usage or credential store problem, 5 transport or server failure
|
|
154
|
+
copy is too old for the server, 4 usage or credential store problem, 5 transport or server failure,
|
|
155
|
+
6 an earlier Balladeer is still installed on this machine and nothing was changed.
|
|
156
|
+
|
|
157
|
+
## An earlier Balladeer on the same machine
|
|
158
|
+
|
|
159
|
+
A machine that ran the earlier Balladeer still carries it: that client registers an MCP server named
|
|
160
|
+
`balladeer` for the whole machine and installs a session hook of its own, and neither comes off when
|
|
161
|
+
this one goes on. Setup finds it before it writes anything and stops with one instruction. If you
|
|
162
|
+
used the earlier Balladeer on this machine: run `balladeer uninstall`, then the removal command it
|
|
163
|
+
prints, then continue. That uninstall is reversible, it backs up `~/.balladeer`, and it leaves the
|
|
164
|
+
old binary in place. `balladeer setup --force` runs anyway, for whoever has decided to keep both.
|
|
152
165
|
|
|
153
166
|
## Revoking
|
|
154
167
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ type Parsed = Readonly<{
|
|
|
6
6
|
wait: boolean;
|
|
7
7
|
/** Repair the files a previous setup wrote, and do nothing else. */
|
|
8
8
|
refresh: boolean;
|
|
9
|
+
/** `setup --force`: set up even though an earlier Balladeer is still installed. */
|
|
10
|
+
force: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `setup --claude-desktop` / `--no-claude-desktop`. Undefined is neither
|
|
13
|
+
* asked for nor refused, which is the ordinary run: connect the chat client
|
|
14
|
+
* where it is installed and say nothing where it is not.
|
|
15
|
+
*/
|
|
16
|
+
claudeDesktop: boolean | undefined;
|
|
9
17
|
repo: string | undefined;
|
|
10
18
|
file: string | undefined;
|
|
11
19
|
repository: string | undefined;
|
|
@@ -21,6 +29,12 @@ type Parsed = Readonly<{
|
|
|
21
29
|
owner: string | undefined;
|
|
22
30
|
/** `check-seals --install-hook`: write the optional pre-push hook. */
|
|
23
31
|
installHook: boolean;
|
|
32
|
+
/** `session --new`: start a different session even though one is current. */
|
|
33
|
+
fresh: boolean;
|
|
34
|
+
/** `session --record`: read the trailer out of HEAD and record that commit. */
|
|
35
|
+
record: boolean;
|
|
36
|
+
/** `prepare --again`: mint a replacement packet and invalidate the unspent one. */
|
|
37
|
+
again: boolean;
|
|
24
38
|
/** `check-seals --runner`: where the pinned runner is, when it has moved. */
|
|
25
39
|
runner: string | undefined;
|
|
26
40
|
createWorkspace: string | undefined;
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { runAffected } from "./commands/affected.js";
|
|
@@ -7,9 +8,11 @@ import { runDiscover } from "./commands/discover.js";
|
|
|
7
8
|
import { runExplain } from "./commands/explain.js";
|
|
8
9
|
import { parseRole, runInvite } from "./commands/invite.js";
|
|
9
10
|
import { runMcp } from "./commands/mcp.js";
|
|
11
|
+
import { runPrepare } from "./commands/prepare.js";
|
|
10
12
|
import { runPropose } from "./commands/propose.js";
|
|
11
13
|
import { runRepositories } from "./commands/repositories.js";
|
|
12
14
|
import { CREATE_WORKSPACE_MAX_LENGTH, runSetup } from "./commands/setup.js";
|
|
15
|
+
import { runSession } from "./commands/session.js";
|
|
13
16
|
import { runStatus } from "./commands/status.js";
|
|
14
17
|
import { runTouchMap } from "./commands/touch-map.js";
|
|
15
18
|
import { runWhoami } from "./commands/whoami.js";
|
|
@@ -19,7 +22,8 @@ import { CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
|
19
22
|
const USAGE = `balladeer ${CLI_VERSION}
|
|
20
23
|
|
|
21
24
|
balladeer setup [--repository owner/name]... [--json] [--wait] [--control-plane <url>]
|
|
22
|
-
[--create-workspace <name>] [--refresh]
|
|
25
|
+
[--create-workspace <name>] [--refresh] [--force]
|
|
26
|
+
[--claude-desktop | --no-claude-desktop]
|
|
23
27
|
Pair this session, add this repository, connect this coding agent and CI,
|
|
24
28
|
and report what a person still has to do. Name --repository more than once
|
|
25
29
|
to add other repositories to the same workspace; each of those is added and
|
|
@@ -28,10 +32,18 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
28
32
|
signs in and creates the workspace themselves; this command never creates
|
|
29
33
|
one.
|
|
30
34
|
--refresh does one thing and talks to nobody: it rewrites this
|
|
31
|
-
repository's balladeer entry in .mcp.json
|
|
32
|
-
block to the current form, leaves every
|
|
33
|
-
and prints what it changed. Run it when
|
|
34
|
-
available.
|
|
35
|
+
repository's balladeer entry in .mcp.json, its Balladeer instructions
|
|
36
|
+
block and its Claude desktop chat entry to the current form, leaves every
|
|
37
|
+
other entry in those files alone, and prints what it changed. Run it when
|
|
38
|
+
Balladeer says a newer version is available.
|
|
39
|
+
--force sets up even though an earlier Balladeer is still installed on
|
|
40
|
+
this machine. Without it, a run that finds the older client's machine-wide
|
|
41
|
+
MCP entry or session hook stops and says to remove it first.
|
|
42
|
+
Claude desktop chat is connected too, under a key named for this
|
|
43
|
+
repository, wherever the app is installed. --claude-desktop asks for it by
|
|
44
|
+
name, so a machine with no Claude desktop on it says so instead of
|
|
45
|
+
skipping quietly; --no-claude-desktop leaves that file alone entirely.
|
|
46
|
+
Quit and reopen the app afterwards: it reads its configuration at startup.
|
|
35
47
|
|
|
36
48
|
balladeer repositories [--json] [--control-plane <url>]
|
|
37
49
|
List the repositories this machine's GitHub account can see, marking the
|
|
@@ -54,6 +66,19 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
54
66
|
Propose one promise from a proposal file, over this repository's agent
|
|
55
67
|
connection. A named person still agrees to it.
|
|
56
68
|
|
|
69
|
+
balladeer prepare <promise id> [--again] [--repo owner/name] [--repository <uuid>]
|
|
70
|
+
[--json] [--control-plane <url>]
|
|
71
|
+
Prepare the one-time qualification setup for a promise whose meaning is
|
|
72
|
+
agreed and which nothing is checking yet, and write it to
|
|
73
|
+
.continuity/qualification/<promise id>.json, which is where the sealed run
|
|
74
|
+
reads it. No sign-off and no code: agreeing the meaning was the person's
|
|
75
|
+
act, and building the check that proves it is yours. Then build the
|
|
76
|
+
verifier, seal it, and push to the default branch; protection starts by
|
|
77
|
+
itself when that run qualifies. It is one-time, so while nobody has
|
|
78
|
+
published against the packet already prepared this refuses and says who
|
|
79
|
+
prepared it and when. --again prepares a replacement and invalidates that
|
|
80
|
+
earlier packet: a run publishing its identities afterwards is refused.
|
|
81
|
+
|
|
57
82
|
balladeer discover --file <path> [--repo owner/name] [--repository <uuid>]
|
|
58
83
|
[--owner <membership id>] [--json]
|
|
59
84
|
Propose a whole discovered catalog, up to ten promises from one file, each
|
|
@@ -79,6 +104,16 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
79
104
|
answer whose verifier has changed since the map was built. It reads one
|
|
80
105
|
local file and contacts nothing.
|
|
81
106
|
|
|
107
|
+
balladeer session [--new] [--record] [--repo owner/name] [--repository <uuid>] [--json]
|
|
108
|
+
[--control-plane <url>]
|
|
109
|
+
Print the id for this piece of work, and the one line to write into the
|
|
110
|
+
commit it produces. Pass the id to every promise read you make, so a check
|
|
111
|
+
that goes red later can be read back against what Balladeer told you
|
|
112
|
+
before you started. --new starts a different session; --record reads the
|
|
113
|
+
Balladeer-Session line out of the commit at HEAD and tells Balladeer which
|
|
114
|
+
commit this session wrote. Your commit message never leaves this machine:
|
|
115
|
+
only the session id and the commit SHA are sent.
|
|
116
|
+
|
|
82
117
|
balladeer explain [--control-plane <url>]
|
|
83
118
|
Print, word for word, what Balladeer can and cannot see, where to watch
|
|
84
119
|
your promises, and what leaving costs.
|
|
@@ -95,7 +130,8 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
95
130
|
|
|
96
131
|
Exit codes: 0 progress reported truthfully, 2 pairing expired or denied or already
|
|
97
132
|
claimed, 3 this copy is too old for the server, 4 usage or credential store problem,
|
|
98
|
-
5 transport or server failure
|
|
133
|
+
5 transport or server failure, 6 an earlier Balladeer is still installed on this
|
|
134
|
+
machine and nothing was changed.
|
|
99
135
|
`;
|
|
100
136
|
const SUBCOMMANDS = { agent: ["rotate"] };
|
|
101
137
|
export function parseArguments(argv) {
|
|
@@ -111,7 +147,12 @@ export function parseArguments(argv) {
|
|
|
111
147
|
let json = false;
|
|
112
148
|
let wait = false;
|
|
113
149
|
let refresh = false;
|
|
150
|
+
let force = false;
|
|
151
|
+
let claudeDesktop;
|
|
114
152
|
let installHook = false;
|
|
153
|
+
let fresh = false;
|
|
154
|
+
let record = false;
|
|
155
|
+
let again = false;
|
|
115
156
|
let runner;
|
|
116
157
|
let controlPlane;
|
|
117
158
|
let repo;
|
|
@@ -147,14 +188,26 @@ export function parseArguments(argv) {
|
|
|
147
188
|
const inline = rest.length > 0 ? rest.join("=") : undefined;
|
|
148
189
|
if (name === "--json")
|
|
149
190
|
json = true;
|
|
191
|
+
else if (name === "--again")
|
|
192
|
+
again = true;
|
|
150
193
|
else if (name === "--install-hook")
|
|
151
194
|
installHook = true;
|
|
195
|
+
else if (name === "--new")
|
|
196
|
+
fresh = true;
|
|
197
|
+
else if (name === "--record")
|
|
198
|
+
record = true;
|
|
152
199
|
else if (name === "--runner")
|
|
153
200
|
runner = value("--runner", inline);
|
|
154
201
|
else if (name === "--wait")
|
|
155
202
|
wait = true;
|
|
156
203
|
else if (name === "--refresh")
|
|
157
204
|
refresh = true;
|
|
205
|
+
else if (name === "--force")
|
|
206
|
+
force = true;
|
|
207
|
+
else if (name === "--claude-desktop")
|
|
208
|
+
claudeDesktop = true;
|
|
209
|
+
else if (name === "--no-claude-desktop")
|
|
210
|
+
claudeDesktop = false;
|
|
158
211
|
else if (name === "--control-plane")
|
|
159
212
|
controlPlane = value("--control-plane", inline);
|
|
160
213
|
else if (name === "--repo")
|
|
@@ -181,8 +234,20 @@ export function parseArguments(argv) {
|
|
|
181
234
|
// value that lost its flag, and it was refused before this loop learned to
|
|
182
235
|
// collect them, so it is refused here rather than ignored.
|
|
183
236
|
// `affected` is the other one, and the words it takes are paths in the change
|
|
184
|
-
// in front of the person.
|
|
185
|
-
|
|
237
|
+
// in front of the person. `prepare` and `status` each take exactly one bare
|
|
238
|
+
// word, the promise id a person copied off a promise page, which is the form
|
|
239
|
+
// both of those commands are told to people and to agents in.
|
|
240
|
+
// `--again` invalidates a packet somebody may be halfway through using, so it
|
|
241
|
+
// belongs to the one command that mints one. Accepted silently elsewhere, it
|
|
242
|
+
// would read as a general "do it anyway" flag.
|
|
243
|
+
if (again && command !== "prepare") {
|
|
244
|
+
throw new StoreError("usage", "--again belongs to prepare: run `balladeer prepare <promise id> --again`.");
|
|
245
|
+
}
|
|
246
|
+
if (positional.length > 0 &&
|
|
247
|
+
command !== "invite" &&
|
|
248
|
+
command !== "affected" &&
|
|
249
|
+
command !== "prepare" &&
|
|
250
|
+
command !== "status") {
|
|
186
251
|
throw new StoreError("usage", `${command} takes no bare arguments: ${positional.join(", ")}.`);
|
|
187
252
|
}
|
|
188
253
|
// `--repository` names one repository everywhere but `setup`, where it names
|
|
@@ -195,6 +260,18 @@ export function parseArguments(argv) {
|
|
|
195
260
|
if (refresh && command !== "setup") {
|
|
196
261
|
throw new StoreError("usage", "--refresh belongs to setup: run `balladeer setup --refresh`.");
|
|
197
262
|
}
|
|
263
|
+
// `--force` says one thing only: set up alongside an earlier Balladeer. Taken
|
|
264
|
+
// silently by another command it would read as a general "do it anyway" flag,
|
|
265
|
+
// which is exactly what nothing else here offers.
|
|
266
|
+
if (force && command !== "setup") {
|
|
267
|
+
throw new StoreError("usage", "--force belongs to setup: run `balladeer setup --force`.");
|
|
268
|
+
}
|
|
269
|
+
// Connecting a chat client is something setup does, so the flag belongs to
|
|
270
|
+
// setup. Accepted silently on `status`, it would let somebody believe they had
|
|
271
|
+
// connected Claude desktop when nothing had written a line.
|
|
272
|
+
if (claudeDesktop !== undefined && command !== "setup") {
|
|
273
|
+
throw new StoreError("usage", "--claude-desktop belongs to setup: run `balladeer setup --claude-desktop`.");
|
|
274
|
+
}
|
|
198
275
|
if (command !== "setup" && repositories.length > 1) {
|
|
199
276
|
throw new StoreError("usage", `${command} acts on one repository. Name --repository once, or run it again for the other.`);
|
|
200
277
|
}
|
|
@@ -205,12 +282,17 @@ export function parseArguments(argv) {
|
|
|
205
282
|
json,
|
|
206
283
|
wait,
|
|
207
284
|
refresh,
|
|
285
|
+
force,
|
|
286
|
+
claudeDesktop,
|
|
208
287
|
repo,
|
|
209
288
|
file,
|
|
210
289
|
repository: repositories[0],
|
|
211
290
|
repositories,
|
|
212
291
|
owner,
|
|
213
292
|
installHook,
|
|
293
|
+
fresh,
|
|
294
|
+
record,
|
|
295
|
+
again,
|
|
214
296
|
runner,
|
|
215
297
|
createWorkspace,
|
|
216
298
|
positional,
|
|
@@ -248,6 +330,8 @@ async function dispatch(parsed, write) {
|
|
|
248
330
|
json: parsed.json,
|
|
249
331
|
wait: parsed.wait,
|
|
250
332
|
refresh: parsed.refresh,
|
|
333
|
+
force: parsed.force,
|
|
334
|
+
...(parsed.claudeDesktop === undefined ? {} : { claudeDesktop: parsed.claudeDesktop }),
|
|
251
335
|
...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
|
|
252
336
|
repositories: parsed.repositories,
|
|
253
337
|
...(parsed.createWorkspace === undefined
|
|
@@ -296,6 +380,18 @@ async function dispatch(parsed, write) {
|
|
|
296
380
|
cwd: process.cwd(),
|
|
297
381
|
write,
|
|
298
382
|
});
|
|
383
|
+
case "session":
|
|
384
|
+
return runSession({
|
|
385
|
+
controlPlane: parsed.controlPlane,
|
|
386
|
+
json: parsed.json,
|
|
387
|
+
record: parsed.record,
|
|
388
|
+
fresh: parsed.fresh,
|
|
389
|
+
...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
|
|
390
|
+
...(parsed.repository === undefined ? {} : { repository: parsed.repository }),
|
|
391
|
+
environment: process.env,
|
|
392
|
+
cwd: process.cwd(),
|
|
393
|
+
write,
|
|
394
|
+
});
|
|
299
395
|
case "affected":
|
|
300
396
|
return runAffected({
|
|
301
397
|
json: parsed.json,
|
|
@@ -322,6 +418,26 @@ async function dispatch(parsed, write) {
|
|
|
322
418
|
write,
|
|
323
419
|
});
|
|
324
420
|
}
|
|
421
|
+
case "prepare": {
|
|
422
|
+
// One id, and one only. Preparing two packets from one line would mint
|
|
423
|
+
// two one-time identities and write two files, and reporting the first
|
|
424
|
+
// silently would leave the second promise unprepared with nobody told.
|
|
425
|
+
if (parsed.positional.length !== 1) {
|
|
426
|
+
process.stderr.write("prepare reads one promise id: balladeer prepare <promise id> [--again].\n");
|
|
427
|
+
return 4;
|
|
428
|
+
}
|
|
429
|
+
return runPrepare({
|
|
430
|
+
controlPlane: parsed.controlPlane,
|
|
431
|
+
json: parsed.json,
|
|
432
|
+
promiseId: parsed.positional[0],
|
|
433
|
+
again: parsed.again,
|
|
434
|
+
...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
|
|
435
|
+
...(parsed.repository === undefined ? {} : { repository: parsed.repository }),
|
|
436
|
+
environment: process.env,
|
|
437
|
+
cwd: process.cwd(),
|
|
438
|
+
write,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
325
441
|
case "propose":
|
|
326
442
|
return runPropose({
|
|
327
443
|
controlPlane: parsed.controlPlane,
|
|
@@ -384,9 +500,32 @@ async function dispatch(parsed, write) {
|
|
|
384
500
|
return 4;
|
|
385
501
|
}
|
|
386
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* The real file behind a path, following symlinks.
|
|
505
|
+
*
|
|
506
|
+
* `npx` and every global install run this program through a
|
|
507
|
+
* `node_modules/.bin/balladeer` symlink, so `process.argv[1]` is the link and
|
|
508
|
+
* not the file Node loaded. `resolve` does not follow a link, so comparing
|
|
509
|
+
* resolved paths made the guard below false for every customer: the process
|
|
510
|
+
* exited 0 having printed nothing, and only a checkout running
|
|
511
|
+
* `node dist/cli.js` ever saw the program run at all.
|
|
512
|
+
*
|
|
513
|
+
* A path that cannot be read back falls through to the resolved form rather
|
|
514
|
+
* than throwing, because a guard that decides whether the program runs must
|
|
515
|
+
* never be the thing that stops it.
|
|
516
|
+
*/
|
|
517
|
+
function programPath(path) {
|
|
518
|
+
const absolute = resolve(path);
|
|
519
|
+
try {
|
|
520
|
+
return realpathSync(absolute);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return absolute;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
387
526
|
// Only when this file is the program, so a test may import `main` without the
|
|
388
527
|
// import itself running a command.
|
|
389
528
|
const entry = process.argv[1];
|
|
390
|
-
if (entry !== undefined && fileURLToPath(import.meta.url) ===
|
|
529
|
+
if (entry !== undefined && programPath(fileURLToPath(import.meta.url)) === programPath(entry)) {
|
|
391
530
|
process.exitCode = await main(process.argv.slice(2));
|
|
392
531
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare class RefusalError extends Error {
|
|
|
18
18
|
*
|
|
19
19
|
* Behind a platform proxy a server can resolve a link against the machine it is
|
|
20
20
|
* running on rather than against the address customers use, and the first
|
|
21
|
-
* production `propose` printed exactly that: `
|
|
21
|
+
* production `propose` printed exactly that: a review link on `localhost:8080`
|
|
22
22
|
* as the place to go and agree. A command that prints such a link is worse than
|
|
23
23
|
* one that prints none, because the person tries it.
|
|
24
24
|
*
|
|
@@ -27,6 +27,28 @@ export declare class RefusalError extends Error {
|
|
|
27
27
|
* here for a server's own link to arrive through.
|
|
28
28
|
*/
|
|
29
29
|
export declare function reviewLink(controlPlane: string, path: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The one place this command decides where a person reads a proposal.
|
|
32
|
+
*
|
|
33
|
+
* Every review address this command prints, in a receipt, in a discovery run,
|
|
34
|
+
* or in its JSON, is built here, so the address is one edit rather than a hunt
|
|
35
|
+
* and a printed link cannot fall behind the page it names. It matches the
|
|
36
|
+
* server's own `proposal-links` helper: the page is `/proposals`, and
|
|
37
|
+
* the retired address redirects to it permanently, so a link an older copy of
|
|
38
|
+
* this command already printed still lands on it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function proposalReviewLink(controlPlane: string, proposalId: string): string;
|
|
41
|
+
/** The address the whole waiting inbox is read on. */
|
|
42
|
+
export declare function proposalInboxLink(controlPlane: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* One link that opens exactly the proposals named, and nothing else.
|
|
45
|
+
*
|
|
46
|
+
* The ids travel in the query string because the batch is exactly these
|
|
47
|
+
* promises: a link to the whole inbox would also open whatever was already
|
|
48
|
+
* waiting there, and a filter by repository would open a different set
|
|
49
|
+
* tomorrow.
|
|
50
|
+
*/
|
|
51
|
+
export declare function batchProposalReviewLink(controlPlane: string, proposalIds: readonly string[]): string;
|
|
30
52
|
export type RequestOptions = Readonly<{
|
|
31
53
|
method: "GET" | "POST";
|
|
32
54
|
path: string;
|
package/dist/client.js
CHANGED
|
@@ -34,7 +34,7 @@ export class RefusalError extends Error {
|
|
|
34
34
|
*
|
|
35
35
|
* Behind a platform proxy a server can resolve a link against the machine it is
|
|
36
36
|
* running on rather than against the address customers use, and the first
|
|
37
|
-
* production `propose` printed exactly that: `
|
|
37
|
+
* production `propose` printed exactly that: a review link on `localhost:8080`
|
|
38
38
|
* as the place to go and agree. A command that prints such a link is worse than
|
|
39
39
|
* one that prints none, because the person tries it.
|
|
40
40
|
*
|
|
@@ -45,6 +45,34 @@ export class RefusalError extends Error {
|
|
|
45
45
|
export function reviewLink(controlPlane, path) {
|
|
46
46
|
return new URL(path, ensureTrailing(controlPlane)).toString();
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The one place this command decides where a person reads a proposal.
|
|
50
|
+
*
|
|
51
|
+
* Every review address this command prints, in a receipt, in a discovery run,
|
|
52
|
+
* or in its JSON, is built here, so the address is one edit rather than a hunt
|
|
53
|
+
* and a printed link cannot fall behind the page it names. It matches the
|
|
54
|
+
* server's own `proposal-links` helper: the page is `/proposals`, and
|
|
55
|
+
* the retired address redirects to it permanently, so a link an older copy of
|
|
56
|
+
* this command already printed still lands on it.
|
|
57
|
+
*/
|
|
58
|
+
export function proposalReviewLink(controlPlane, proposalId) {
|
|
59
|
+
return reviewLink(controlPlane, `proposals/${encodeURIComponent(proposalId)}`);
|
|
60
|
+
}
|
|
61
|
+
/** The address the whole waiting inbox is read on. */
|
|
62
|
+
export function proposalInboxLink(controlPlane) {
|
|
63
|
+
return reviewLink(controlPlane, "proposals");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One link that opens exactly the proposals named, and nothing else.
|
|
67
|
+
*
|
|
68
|
+
* The ids travel in the query string because the batch is exactly these
|
|
69
|
+
* promises: a link to the whole inbox would also open whatever was already
|
|
70
|
+
* waiting there, and a filter by repository would open a different set
|
|
71
|
+
* tomorrow.
|
|
72
|
+
*/
|
|
73
|
+
export function batchProposalReviewLink(controlPlane, proposalIds) {
|
|
74
|
+
return reviewLink(controlPlane, `proposals/review?ids=${proposalIds.join(",")}`);
|
|
75
|
+
}
|
|
48
76
|
function ensureTrailing(controlPlane) {
|
|
49
77
|
return controlPlane.endsWith("/") ? controlPlane : `${controlPlane}/`;
|
|
50
78
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
2
|
import { TOUCH_MAP_FILE, affectedPaths, readLocalPackages, readTouchMap, verifierDigest, } from "../touch-map.js";
|
|
3
|
+
import { formatInstant } from "../local-time.js";
|
|
3
4
|
import {} from "../wire.js";
|
|
4
5
|
import { promiseRoot } from "./touch-map.js";
|
|
5
6
|
/** How many promises one path lists before the report says how many more. */
|
|
@@ -86,7 +87,7 @@ export async function runAffected(options) {
|
|
|
86
87
|
changed: false,
|
|
87
88
|
});
|
|
88
89
|
if (touched.length === 0) {
|
|
89
|
-
say(`No promise in this map ran any of those files. The map was built ${reading.map.generatedAt}; a promise sealed since then is not in it.`);
|
|
90
|
+
say(`No promise in this map ran any of those files. The map was built ${formatInstant(reading.map.generatedAt)}; a promise sealed since then is not in it.`);
|
|
90
91
|
return 0;
|
|
91
92
|
}
|
|
92
93
|
for (const answer of answers) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { callAgentTool, selectAgent, structuredString } from "../agent.js";
|
|
3
|
-
import {
|
|
3
|
+
import { batchProposalReviewLink, proposalReviewLink } from "../client.js";
|
|
4
4
|
import { commandLine } from "../release.js";
|
|
5
5
|
import { repositoryHint } from "../repository.js";
|
|
6
6
|
import { StoreError, findSession, readCredentials } from "../store.js";
|
|
@@ -140,7 +140,7 @@ function titleOf(request) {
|
|
|
140
140
|
* different set tomorrow.
|
|
141
141
|
*/
|
|
142
142
|
export function batchReviewLink(controlPlane, candidateIds) {
|
|
143
|
-
return
|
|
143
|
+
return batchProposalReviewLink(controlPlane, candidateIds);
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
146
|
* Failures of the connection rather than of one promise, which is the
|
|
@@ -254,7 +254,7 @@ export async function runDiscover(options) {
|
|
|
254
254
|
index: index + 1,
|
|
255
255
|
title,
|
|
256
256
|
candidateId: outcome.candidateId,
|
|
257
|
-
reviewUrl:
|
|
257
|
+
reviewUrl: proposalReviewLink(options.controlPlane, outcome.candidateId),
|
|
258
258
|
};
|
|
259
259
|
filed.push(entry);
|
|
260
260
|
emit({
|
|
@@ -336,6 +336,14 @@ async function proposeOne(agent, request, ownerId) {
|
|
|
336
336
|
// are not nine equally certain readings, and an owner deciding which to read
|
|
337
337
|
// first is entitled to see which one the agent was least sure of.
|
|
338
338
|
confidence: request.teachBack.confidence,
|
|
339
|
+
// The gate, on every entry in the file. A discovery run is where an
|
|
340
|
+
// unfailable promise is easiest to write: nine behaviors read off nine
|
|
341
|
+
// sources, and any one of them can be a sentence nothing could ever
|
|
342
|
+
// contradict. The file is refused entry by entry rather than in bulk.
|
|
343
|
+
wrongOutcome: request.teachBack.wrongOutcome,
|
|
344
|
+
...(request.teachBack.leastSure === undefined
|
|
345
|
+
? {}
|
|
346
|
+
: { leastSure: request.teachBack.leastSure }),
|
|
339
347
|
});
|
|
340
348
|
switch (call.kind) {
|
|
341
349
|
case "endpoint_refused":
|
|
@@ -387,7 +395,7 @@ async function proposeOne(agent, request, ownerId) {
|
|
|
387
395
|
if (status !== "pending_review") {
|
|
388
396
|
return {
|
|
389
397
|
ok: false,
|
|
390
|
-
reason: "
|
|
398
|
+
reason: "proposal_not_pending",
|
|
391
399
|
message: `Balladeer matched an existing proposal, ${candidateId}, which is ${status ?? "in an unreported state"}. Nothing new was recorded.`,
|
|
392
400
|
};
|
|
393
401
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The boundary copy describes the product's boundary and is timeless; it names
|
|
6
6
|
* two credentials, a CI workflow, and a check on pull requests. In balladeer
|
|
7
|
-
* 1.0.
|
|
7
|
+
* 1.0.1 the command reaches none of those, so saying so is the difference
|
|
8
8
|
* between an explanation and a claim.
|
|
9
9
|
*/
|
|
10
10
|
export declare const RELEASE_PREFACE: string;
|
package/dist/commands/explain.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CLI_VERSION, DEFAULT_CONTROL_PLANE } from "../wire.js";
|
|
|
6
6
|
*
|
|
7
7
|
* The boundary copy describes the product's boundary and is timeless; it names
|
|
8
8
|
* two credentials, a CI workflow, and a check on pull requests. In balladeer
|
|
9
|
-
* 1.0.
|
|
9
|
+
* 1.0.1 the command reaches none of those, so saying so is the difference
|
|
10
10
|
* between an explanation and a claim.
|
|
11
11
|
*/
|
|
12
12
|
export const RELEASE_PREFACE = [
|
package/dist/commands/invite.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ClientTooOldError, RefusalError, TransportError, request } from "../client.js";
|
|
2
|
+
import { formatInstant } from "../local-time.js";
|
|
2
3
|
import { commandLine } from "../release.js";
|
|
3
4
|
import { StoreError, findSession, readCredentials } from "../store.js";
|
|
4
5
|
import {} from "../wire.js";
|
|
@@ -127,7 +128,7 @@ async function inviteOne(options, session, email) {
|
|
|
127
128
|
say(options, `Invited ${answer.email} to ${answer.workspaceName} as ${role}. Balladeer has sent them the email.`);
|
|
128
129
|
say(options, ` They can ${ROLE_MEANINGS[answer.requestedRole]}. They join when they accept, and not before.`);
|
|
129
130
|
if (answer.expiresAt !== null) {
|
|
130
|
-
say(options, ` The invitation stops working at ${answer.expiresAt}.`);
|
|
131
|
+
say(options, ` The invitation stops working at ${formatInstant(answer.expiresAt)}.`);
|
|
131
132
|
}
|
|
132
133
|
emit(options, {
|
|
133
134
|
step: "invitation",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type PrepareOptions = Readonly<{
|
|
2
|
+
controlPlane: string;
|
|
3
|
+
json: boolean;
|
|
4
|
+
promiseId: string;
|
|
5
|
+
/** Prepare a replacement, invalidating the packet nobody has spent. */
|
|
6
|
+
again: boolean;
|
|
7
|
+
/** The repository whose connection to use, named as `owner/name`. */
|
|
8
|
+
repo?: string;
|
|
9
|
+
/** The repository whose connection to use, named by id, as `mcp` takes it. */
|
|
10
|
+
repository?: string;
|
|
11
|
+
environment: NodeJS.ProcessEnv;
|
|
12
|
+
cwd: string;
|
|
13
|
+
write: (text: string) => void;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* A refusal Balladeer wrote, as this terminal has to say it.
|
|
17
|
+
*
|
|
18
|
+
* Two changes and no others. The instants move onto the reader's own clock,
|
|
19
|
+
* because the server writes UTC and "already prepared by Robert Clark on
|
|
20
|
+
* 2026-09-07T18:05:10.698Z" reached somebody whose clock said two in the
|
|
21
|
+
* afternoon. And the argument is named the way this command takes it. Every
|
|
22
|
+
* other word, including what to do next and why, is the server's.
|
|
23
|
+
*/
|
|
24
|
+
export declare function atThisTerminal(text: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* The six keys the sealed run reads, and nothing else.
|
|
27
|
+
*
|
|
28
|
+
* The runner rejects an unknown field outright, so a metadata file that carried
|
|
29
|
+
* a helpful comment, a timestamp, or the whole packet would fail the customer's
|
|
30
|
+
* protected run for a reason that had nothing to do with their behavior. This
|
|
31
|
+
* command therefore writes the packet's own `qualificationMetadata` object and
|
|
32
|
+
* refuses to write anything it does not recognise.
|
|
33
|
+
*/
|
|
34
|
+
declare const METADATA_KEYS: readonly ["schemaVersion", "workspaceLocator", "receiptId", "revisionId", "bindingId", "workflowDigest"];
|
|
35
|
+
type Metadata = Record<(typeof METADATA_KEYS)[number], string>;
|
|
36
|
+
export declare function readQualificationMetadata(structured: unknown): Readonly<{
|
|
37
|
+
ok: true;
|
|
38
|
+
value: Metadata;
|
|
39
|
+
}> | Readonly<{
|
|
40
|
+
ok: false;
|
|
41
|
+
reason: string;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Where the file goes, refusing any path that would leave this repository.
|
|
45
|
+
*
|
|
46
|
+
* The path comes off the server's answer, and the whole point of this command is
|
|
47
|
+
* that it writes a file the person did not type. A server that answered with
|
|
48
|
+
* `../../etc/something` would otherwise have this command write there, so the
|
|
49
|
+
* resolved path has to stay under the directory the command was run in.
|
|
50
|
+
*/
|
|
51
|
+
export declare function metadataDestination(cwd: string, metadataPath: string): Readonly<{
|
|
52
|
+
ok: true;
|
|
53
|
+
path: string;
|
|
54
|
+
}> | Readonly<{
|
|
55
|
+
ok: false;
|
|
56
|
+
reason: string;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* Prepares the one-time qualification setup for one promise, and writes the file
|
|
60
|
+
* where the sealed run reads it.
|
|
61
|
+
*
|
|
62
|
+
* It runs over this repository's own agent connection, which is what makes it
|
|
63
|
+
* usable at all: preparing a qualification setup is the agent's job, and the
|
|
64
|
+
* setup session that paired the machine expires. There is no sign-off here and
|
|
65
|
+
* no code to paste. Agreeing the meaning was the person's act; building the
|
|
66
|
+
* check that proves it is this command's caller's.
|
|
67
|
+
*
|
|
68
|
+
* The file is written once. A second run while nobody has published against the
|
|
69
|
+
* first packet is refused, naming who prepared it and when, and `--again` mints
|
|
70
|
+
* a replacement that invalidates the earlier one on the server as well as
|
|
71
|
+
* overwriting the file here.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runPrepare(options: PrepareOptions): Promise<number>;
|
|
74
|
+
export {};
|