balladeer 1.0.1 → 1.0.2
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 +41 -33
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +13 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +39 -21
- package/dist/client.d.ts +2 -2
- package/dist/client.js +7 -4
- package/dist/commands/affected.js +6 -6
- package/dist/commands/check-seals.js +4 -4
- package/dist/commands/discover.js +5 -4
- package/dist/commands/prepare.js +2 -1
- package/dist/commands/propose.js +5 -4
- package/dist/commands/repositories.js +1 -0
- package/dist/commands/session.js +17 -4
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.js +156 -75
- package/dist/commands/status.js +41 -18
- package/dist/commands/touch-map.js +2 -2
- package/dist/conventions.d.ts +1 -1
- package/dist/conventions.js +1 -1
- package/dist/copy.d.ts +7 -7
- package/dist/copy.js +45 -30
- package/dist/store.d.ts +1 -1
- package/dist/store.js +3 -3
- package/dist/wire.d.ts +9 -4
- package/dist/wire.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@ npx -y balladeer@latest setup
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
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
|
|
27
|
-
and its instructions without replacing other tools' settings.
|
|
26
|
+
an update is available. Run `npx -y balladeer@latest setup --refresh` to update an existing
|
|
27
|
+
repository connection and its instructions without replacing other tools' settings.
|
|
28
28
|
|
|
29
29
|
Run it inside the repository you want to protect. It prints the boundary explanation below, then one
|
|
30
30
|
step at a time, and it exits within seconds rather than blocking on anything.
|
|
@@ -34,6 +34,12 @@ connects this coding agent, records the CI identity and opens the pull request w
|
|
|
34
34
|
and hands you the playbook for discovering this repository's own promises. The two things it never
|
|
35
35
|
does are sign you in and agree to a promise; both of those are yours, in a browser.
|
|
36
36
|
|
|
37
|
+
If you are joining an existing workspace, use `npx -y balladeer@latest setup --existing`. It
|
|
38
|
+
completes browser onboarding without adding a repository or changing CI. When this checkout is
|
|
39
|
+
already enrolled it connects this machine and writes its MCP configuration; when it is absent, setup
|
|
40
|
+
leaves the checkout unchanged and Balladeer shows that enforcement tracking is unavailable until an
|
|
41
|
+
administrator connects a repository.
|
|
42
|
+
|
|
37
43
|
## Requirements
|
|
38
44
|
|
|
39
45
|
- Node 22 or newer. The command has no runtime dependencies at all: it uses Node builtins and
|
|
@@ -58,19 +64,20 @@ test output.
|
|
|
58
64
|
The verify job in your CI runs your tests with your checkout and has no Balladeer credential. A
|
|
59
65
|
separate publish job, with no checkout, sends only those outcomes and hashes.
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
Browser approval creates a temporary setup session limited by that person's workspace role. A
|
|
68
|
+
contributor can connect this machine's coding agent to an already-enrolled repository and propose
|
|
69
|
+
promises; an administrator can also add repositories, connect CI, and invite teammates. The session
|
|
70
|
+
cannot approve, activate, rotate, revoke, or remove anything. It lasts a day, each use buys it
|
|
71
|
+
another day, and it is gone seven days after approval however much it was used. When setup connects
|
|
72
|
+
an enrolled repository it also issues that machine a coding-agent credential. That connection has no
|
|
73
|
+
expiry: it lets the agent read the promises your team has approved, propose new ones, and propose
|
|
74
|
+
replacing, retiring, excepting, or reassigning one you already have; every one of those waits for a
|
|
75
|
+
named person to decide it. It can also carry out one of those acts for you, and only this way: you
|
|
76
|
+
open a Balladeer page in your own browser, read what the act is, sign it off there, and read back
|
|
77
|
+
the one-time code that page gives you. That code covers that one act on that one thing, once, and
|
|
78
|
+
Balladeer records you as the person who took it and the connection as the messenger. Without a code
|
|
79
|
+
you signed, the connection cannot approve, activate, grant, transfer, retire, or delete anything.
|
|
80
|
+
You can revoke either one at any time in Balladeer, and the setup session also ends on its own.
|
|
74
81
|
|
|
75
82
|
Adding the workflow puts a check on pull requests into your default branch. That check is advisory
|
|
76
83
|
on Balladeer's side; whether it blocks a merge is your own branch protection.
|
|
@@ -108,22 +115,22 @@ your own branch protection.
|
|
|
108
115
|
|
|
109
116
|
## Commands
|
|
110
117
|
|
|
111
|
-
| Command
|
|
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 |
|
|
118
|
+
| Command | What it does |
|
|
119
|
+
| -------------------------------------- | -------------------------------------------------------------------------------- |
|
|
120
|
+
| `npx -y balladeer@latest setup` | Run all five steps, then report what a person still has to do |
|
|
121
|
+
| `npx -y balladeer@latest repositories` | List what this machine could add, marking the one you are in and the ones in |
|
|
122
|
+
| `npx -y balladeer@latest invite` | Invite teammates by email, and say per address whether the email went |
|
|
123
|
+
| `npx -y balladeer@latest status` | Report this repository over its agent connection, and the workspace |
|
|
124
|
+
| `npx -y balladeer@latest status <id>` | Report one promise: whether it is holding, and if not, what the run reported |
|
|
125
|
+
| `npx -y balladeer@latest prepare <id>` | Prepare a promise's one-time qualification setup, and write it where CI reads it |
|
|
126
|
+
| `npx -y balladeer@latest propose` | Propose one promise from a proposal file, over that connection |
|
|
127
|
+
| `npx -y balladeer@latest discover` | File a whole catalog, up to ten promises from one file, behind one review link |
|
|
128
|
+
| `npx -y balladeer@latest touch-map` | Record which files each promise's verifier runs, on this machine only |
|
|
129
|
+
| `npx -y balladeer@latest affected` | Say which promises the files you name touch, out of that record |
|
|
130
|
+
| `npx -y balladeer@latest session` | Print the id for this piece of work, and the line to write into the commit |
|
|
131
|
+
| `npx -y balladeer@latest mcp` | Forward one MCP session over stdio using this repository's connection |
|
|
132
|
+
| `npx -y balladeer@latest explain` | What Balladeer can and cannot see, where to watch it, and what leaving costs |
|
|
133
|
+
| `npx -y balladeer@latest whoami` | Report the stored session's workspace, role, scopes, and expiry |
|
|
127
134
|
|
|
128
135
|
`--json` emits one object per step on stdout and nothing else. `--wait` makes `setup` poll for the
|
|
129
136
|
approval instead of exiting; without it the command exits and a later run finishes the pairing.
|
|
@@ -132,7 +139,7 @@ approval instead of exiting; without it the command exits and a later run finish
|
|
|
132
139
|
to be the one you are standing in, because connecting a coding agent writes files into a working
|
|
133
140
|
tree and connecting CI pushes a branch to a remote. The rest are added to the workspace and nothing
|
|
134
141
|
more, and the run says so for each of them; somebody runs setup in a checkout of each one to finish
|
|
135
|
-
it. `balladeer repositories` is what you read first to decide which ones to name.
|
|
142
|
+
it. `npx -y balladeer@latest repositories` is what you read first to decide which ones to name.
|
|
136
143
|
|
|
137
144
|
`invite` takes one or more addresses and `--role contributor|viewer|administrator`, defaulting to
|
|
138
145
|
contributor. It goes over the setup session's `workspace:invite` grant, so it works only for a
|
|
@@ -161,7 +168,8 @@ A machine that ran the earlier Balladeer still carries it: that client registers
|
|
|
161
168
|
this one goes on. Setup finds it before it writes anything and stops with one instruction. If you
|
|
162
169
|
used the earlier Balladeer on this machine: run `balladeer uninstall`, then the removal command it
|
|
163
170
|
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
|
|
171
|
+
old binary in place. `npx -y balladeer@latest setup --force` runs anyway, for whoever has decided to
|
|
172
|
+
keep both.
|
|
165
173
|
|
|
166
174
|
## Revoking
|
|
167
175
|
|
package/dist/agent.d.ts
CHANGED
|
@@ -124,3 +124,8 @@ export declare function safeJson(response: Response): Promise<unknown>;
|
|
|
124
124
|
export declare function updateLine(payload: unknown): string;
|
|
125
125
|
/** One field of a tool result, when it is a string. Never a guess. */
|
|
126
126
|
export declare function structuredString(structured: unknown, field: string): string | undefined;
|
|
127
|
+
/** Render the server's live enforcement limitation once per CLI operation. */
|
|
128
|
+
export declare function reportAgentEnforcementWarning(call: AgentToolCall, output: Readonly<{
|
|
129
|
+
json: boolean;
|
|
130
|
+
write: (text: string) => void;
|
|
131
|
+
}>): void;
|
package/dist/agent.js
CHANGED
|
@@ -21,7 +21,8 @@ const REPOSITORY_NAME = /^[A-Za-z0-9._-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
|
|
|
21
21
|
export function selectAgent(agents, controlPlane, repositoryId, currentRepository) {
|
|
22
22
|
const here = agents.filter((agent) => agent.controlPlane === controlPlane);
|
|
23
23
|
if (repositoryId !== undefined) {
|
|
24
|
-
const named = here.find((agent) => agent.repositoryId === repositoryId
|
|
24
|
+
const named = here.find((agent) => agent.repositoryId === repositoryId ||
|
|
25
|
+
agent.repository?.toLowerCase() === repositoryId.toLowerCase());
|
|
25
26
|
if (named === undefined) {
|
|
26
27
|
return {
|
|
27
28
|
kind: "refused",
|
|
@@ -207,3 +208,14 @@ export function structuredString(structured, field) {
|
|
|
207
208
|
const value = structured[field];
|
|
208
209
|
return typeof value === "string" ? value : undefined;
|
|
209
210
|
}
|
|
211
|
+
/** Render the server's live enforcement limitation once per CLI operation. */
|
|
212
|
+
export function reportAgentEnforcementWarning(call, output) {
|
|
213
|
+
if (call.kind !== "result" && call.kind !== "tool_refusal")
|
|
214
|
+
return;
|
|
215
|
+
const message = structuredString(call.structured, "enforcementWarning");
|
|
216
|
+
if (message === undefined)
|
|
217
|
+
return;
|
|
218
|
+
output.write(output.json
|
|
219
|
+
? `${JSON.stringify({ step: "warning", code: "enforcement_unavailable", message })}\n`
|
|
220
|
+
: `WARNING ${message}\n`);
|
|
221
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ type Parsed = Readonly<{
|
|
|
8
8
|
refresh: boolean;
|
|
9
9
|
/** `setup --force`: set up even though an earlier Balladeer is still installed. */
|
|
10
10
|
force: boolean;
|
|
11
|
+
/** `setup --existing`: connect only when this checkout is already enrolled. */
|
|
12
|
+
existingOnly: boolean;
|
|
11
13
|
/**
|
|
12
14
|
* `setup --claude-desktop` / `--no-claude-desktop`. Undefined is neither
|
|
13
15
|
* asked for nor refused, which is the ordinary run: connect the chat client
|
package/dist/cli.js
CHANGED
|
@@ -18,11 +18,12 @@ import { runTouchMap } from "./commands/touch-map.js";
|
|
|
18
18
|
import { runWhoami } from "./commands/whoami.js";
|
|
19
19
|
import { updateNotice } from "./currency.js";
|
|
20
20
|
import { StoreError, normalizeControlPlane } from "./store.js";
|
|
21
|
-
import { CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
21
|
+
import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
22
22
|
const USAGE = `balladeer ${CLI_VERSION}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
${CLI_INVOCATION} setup [--repository owner/name]... [--json] [--wait] [--control-plane <url>]
|
|
25
25
|
[--create-workspace <name>] [--refresh] [--force]
|
|
26
|
+
[--existing]
|
|
26
27
|
[--claude-desktop | --no-claude-desktop]
|
|
27
28
|
Pair this session, add this repository, connect this coding agent and CI,
|
|
28
29
|
and report what a person still has to do. Name --repository more than once
|
|
@@ -31,6 +32,9 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
31
32
|
--create-workspace carries a name to the approval page, where a person
|
|
32
33
|
signs in and creates the workspace themselves; this command never creates
|
|
33
34
|
one.
|
|
35
|
+
--existing is for an invited teammate and never enrolls repositories or changes CI.
|
|
36
|
+
If this repository is already connected, connect this machine; otherwise
|
|
37
|
+
continue in the browser while an administrator connects it.
|
|
34
38
|
--refresh does one thing and talks to nobody: it rewrites this
|
|
35
39
|
repository's balladeer entry in .mcp.json, its Balladeer instructions
|
|
36
40
|
block and its Claude desktop chat entry to the current form, leaves every
|
|
@@ -45,15 +49,15 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
45
49
|
skipping quietly; --no-claude-desktop leaves that file alone entirely.
|
|
46
50
|
Quit and reopen the app afterwards: it reads its configuration at startup.
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
${CLI_INVOCATION} repositories [--json] [--control-plane <url>]
|
|
49
53
|
List the repositories this machine's GitHub account can see, marking the
|
|
50
54
|
one you are standing in and the ones Balladeer already has.
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
${CLI_INVOCATION} invite <email>... [--role contributor|viewer|administrator] [--json]
|
|
53
57
|
Invite teammates into this workspace by email, and say per address whether
|
|
54
58
|
the invitation went. Inviting is an administrator's act.
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
${CLI_INVOCATION} status [<promise id>] [--repo owner/name] [--repository <uuid>] [--json]
|
|
57
61
|
[--control-plane <url>]
|
|
58
62
|
Report this repository over its agent connection, and every repository in
|
|
59
63
|
the workspace when a setup session is still live. Named with a promise id,
|
|
@@ -62,11 +66,11 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
62
66
|
the agreed meaning that run was checking. That is the form the line on a
|
|
63
67
|
broken promise tells a person to run first.
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
${CLI_INVOCATION} propose --file <path> [--repo owner/name] [--repository <uuid>] [--json]
|
|
66
70
|
Propose one promise from a proposal file, over this repository's agent
|
|
67
71
|
connection. A named person still agrees to it.
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
${CLI_INVOCATION} prepare <promise id> [--again] [--repo owner/name] [--repository <uuid>]
|
|
70
74
|
[--json] [--control-plane <url>]
|
|
71
75
|
Prepare the one-time qualification setup for a promise whose meaning is
|
|
72
76
|
agreed and which nothing is checking yet, and write it to
|
|
@@ -79,13 +83,13 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
79
83
|
prepared it and when. --again prepares a replacement and invalidates that
|
|
80
84
|
earlier packet: a run publishing its identities afterwards is refused.
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
${CLI_INVOCATION} discover --file <path> [--repo owner/name] [--repository <uuid>]
|
|
83
87
|
[--owner <membership id>] [--json]
|
|
84
88
|
Propose a whole discovered catalog, up to ten promises from one file, each
|
|
85
89
|
owned by whoever paired this machine, and print one link that opens all of
|
|
86
90
|
them. A named person still agrees to every one.
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
${CLI_INVOCATION} check-seals [--json] [--runner <path>] [--install-hook]
|
|
89
93
|
Say whether what you are about to push would break a promise's seal. It
|
|
90
94
|
prints nothing and exits 0 when it would not, and names the promise, its
|
|
91
95
|
owner, its page and the line that seals it again when it would. It runs
|
|
@@ -93,18 +97,18 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
93
97
|
writes .git/hooks/pre-push so every push asks first; nothing installs it
|
|
94
98
|
for you, and deleting that file removes it.
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
${CLI_INVOCATION} touch-map [--json]
|
|
97
101
|
Run this repository's promise verifiers under coverage and write down
|
|
98
102
|
which files each one actually executed, to .continuity/touch-map.json.
|
|
99
103
|
It runs offline and the map stays on this machine: Balladeer is never
|
|
100
104
|
sent it. Node verifiers only in this release.
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
${CLI_INVOCATION} affected <paths...> [--json]
|
|
103
107
|
Say which promises the named files touch, out of that map, marking any
|
|
104
108
|
answer whose verifier has changed since the map was built. It reads one
|
|
105
109
|
local file and contacts nothing.
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
${CLI_INVOCATION} session [--new] [--record] [--repo owner/name] [--repository <uuid>] [--json]
|
|
108
112
|
[--control-plane <url>]
|
|
109
113
|
Print the id for this piece of work, and the one line to write into the
|
|
110
114
|
commit it produces. Pass the id to every promise read you make, so a check
|
|
@@ -114,18 +118,18 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
114
118
|
commit this session wrote. Your commit message never leaves this machine:
|
|
115
119
|
only the session id and the commit SHA are sent.
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
${CLI_INVOCATION} explain [--control-plane <url>]
|
|
118
122
|
Print, word for word, what Balladeer can and cannot see, where to watch
|
|
119
123
|
your promises, and what leaving costs.
|
|
120
124
|
|
|
121
|
-
|
|
125
|
+
${CLI_INVOCATION} whoami [--json] [--control-plane <url>]
|
|
122
126
|
Report the stored setup session's workspace, role, scopes, and expiry.
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
${CLI_INVOCATION} agent rotate [--control-plane <url>]
|
|
125
129
|
Replace this repository's agent credential. A setup session cannot do this,
|
|
126
130
|
because rotating revokes the connections the repository already has.
|
|
127
131
|
|
|
128
|
-
|
|
132
|
+
${CLI_INVOCATION} mcp [--repository <uuid>] [--control-plane <url>]
|
|
129
133
|
Forward one MCP session over stdio using this repository's agent connection.
|
|
130
134
|
|
|
131
135
|
Exit codes: 0 progress reported truthfully, 2 pairing expired or denied or already
|
|
@@ -148,6 +152,7 @@ export function parseArguments(argv) {
|
|
|
148
152
|
let wait = false;
|
|
149
153
|
let refresh = false;
|
|
150
154
|
let force = false;
|
|
155
|
+
let existingOnly = false;
|
|
151
156
|
let claudeDesktop;
|
|
152
157
|
let installHook = false;
|
|
153
158
|
let fresh = false;
|
|
@@ -204,6 +209,8 @@ export function parseArguments(argv) {
|
|
|
204
209
|
refresh = true;
|
|
205
210
|
else if (name === "--force")
|
|
206
211
|
force = true;
|
|
212
|
+
else if (name === "--existing")
|
|
213
|
+
existingOnly = true;
|
|
207
214
|
else if (name === "--claude-desktop")
|
|
208
215
|
claudeDesktop = true;
|
|
209
216
|
else if (name === "--no-claude-desktop")
|
|
@@ -241,7 +248,7 @@ export function parseArguments(argv) {
|
|
|
241
248
|
// belongs to the one command that mints one. Accepted silently elsewhere, it
|
|
242
249
|
// would read as a general "do it anyway" flag.
|
|
243
250
|
if (again && command !== "prepare") {
|
|
244
|
-
throw new StoreError("usage",
|
|
251
|
+
throw new StoreError("usage", `--again belongs to prepare: run \`${CLI_INVOCATION} prepare <promise id> --again\`.`);
|
|
245
252
|
}
|
|
246
253
|
if (positional.length > 0 &&
|
|
247
254
|
command !== "invite" &&
|
|
@@ -258,19 +265,28 @@ export function parseArguments(argv) {
|
|
|
258
265
|
// nothing else. Accepting it silently elsewhere would let somebody run
|
|
259
266
|
// `status --refresh`, see no error, and believe their install was repaired.
|
|
260
267
|
if (refresh && command !== "setup") {
|
|
261
|
-
throw new StoreError("usage",
|
|
268
|
+
throw new StoreError("usage", `--refresh belongs to setup: run \`${CLI_INVOCATION} setup --refresh\`.`);
|
|
262
269
|
}
|
|
263
270
|
// `--force` says one thing only: set up alongside an earlier Balladeer. Taken
|
|
264
271
|
// silently by another command it would read as a general "do it anyway" flag,
|
|
265
272
|
// which is exactly what nothing else here offers.
|
|
266
273
|
if (force && command !== "setup") {
|
|
267
|
-
throw new StoreError("usage",
|
|
274
|
+
throw new StoreError("usage", `--force belongs to setup: run \`${CLI_INVOCATION} setup --force\`.`);
|
|
275
|
+
}
|
|
276
|
+
if (existingOnly && command !== "setup") {
|
|
277
|
+
throw new StoreError("usage", `--existing belongs to setup: run \`${CLI_INVOCATION} setup --existing\`.`);
|
|
278
|
+
}
|
|
279
|
+
if (existingOnly && createWorkspace !== undefined) {
|
|
280
|
+
throw new StoreError("usage", "--existing cannot be combined with --create-workspace: one refuses repository enrollment and the other starts a new workspace.");
|
|
281
|
+
}
|
|
282
|
+
if (existingOnly && repositories.length > 1) {
|
|
283
|
+
throw new StoreError("usage", "--existing connects one checkout and never adds repositories. Leave off --repository, or name only this checkout.");
|
|
268
284
|
}
|
|
269
285
|
// Connecting a chat client is something setup does, so the flag belongs to
|
|
270
286
|
// setup. Accepted silently on `status`, it would let somebody believe they had
|
|
271
287
|
// connected Claude desktop when nothing had written a line.
|
|
272
288
|
if (claudeDesktop !== undefined && command !== "setup") {
|
|
273
|
-
throw new StoreError("usage",
|
|
289
|
+
throw new StoreError("usage", `--claude-desktop belongs to setup: run \`${CLI_INVOCATION} setup --claude-desktop\`.`);
|
|
274
290
|
}
|
|
275
291
|
if (command !== "setup" && repositories.length > 1) {
|
|
276
292
|
throw new StoreError("usage", `${command} acts on one repository. Name --repository once, or run it again for the other.`);
|
|
@@ -283,6 +299,7 @@ export function parseArguments(argv) {
|
|
|
283
299
|
wait,
|
|
284
300
|
refresh,
|
|
285
301
|
force,
|
|
302
|
+
existingOnly,
|
|
286
303
|
claudeDesktop,
|
|
287
304
|
repo,
|
|
288
305
|
file,
|
|
@@ -331,6 +348,7 @@ async function dispatch(parsed, write) {
|
|
|
331
348
|
wait: parsed.wait,
|
|
332
349
|
refresh: parsed.refresh,
|
|
333
350
|
force: parsed.force,
|
|
351
|
+
existingOnly: parsed.existingOnly,
|
|
334
352
|
...(parsed.claudeDesktop === undefined ? {} : { claudeDesktop: parsed.claudeDesktop }),
|
|
335
353
|
...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
|
|
336
354
|
repositories: parsed.repositories,
|
|
@@ -423,7 +441,7 @@ async function dispatch(parsed, write) {
|
|
|
423
441
|
// two one-time identities and write two files, and reporting the first
|
|
424
442
|
// silently would leave the second promise unprepared with nobody told.
|
|
425
443
|
if (parsed.positional.length !== 1) {
|
|
426
|
-
process.stderr.write(
|
|
444
|
+
process.stderr.write(`prepare reads one promise id: ${CLI_INVOCATION} prepare <promise id> [--again].\n`);
|
|
427
445
|
return 4;
|
|
428
446
|
}
|
|
429
447
|
return runPrepare({
|
package/dist/client.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class RefusalError extends Error {
|
|
|
26
26
|
* identifier and this builds the address, which is why there is no parameter
|
|
27
27
|
* here for a server's own link to arrive through.
|
|
28
28
|
*/
|
|
29
|
-
export declare function reviewLink(controlPlane: string, path: string): string;
|
|
29
|
+
export declare function reviewLink(controlPlane: string, path: string, workspaceId?: string): string;
|
|
30
30
|
/**
|
|
31
31
|
* The one place this command decides where a person reads a proposal.
|
|
32
32
|
*
|
|
@@ -37,7 +37,7 @@ export declare function reviewLink(controlPlane: string, path: string): string;
|
|
|
37
37
|
* the retired address redirects to it permanently, so a link an older copy of
|
|
38
38
|
* this command already printed still lands on it.
|
|
39
39
|
*/
|
|
40
|
-
export declare function proposalReviewLink(controlPlane: string, proposalId: string): string;
|
|
40
|
+
export declare function proposalReviewLink(controlPlane: string, proposalId: string, workspaceId?: string): string;
|
|
41
41
|
/** The address the whole waiting inbox is read on. */
|
|
42
42
|
export declare function proposalInboxLink(controlPlane: string): string;
|
|
43
43
|
/**
|
package/dist/client.js
CHANGED
|
@@ -42,8 +42,11 @@ export class RefusalError extends Error {
|
|
|
42
42
|
* identifier and this builds the address, which is why there is no parameter
|
|
43
43
|
* here for a server's own link to arrive through.
|
|
44
44
|
*/
|
|
45
|
-
export function reviewLink(controlPlane, path) {
|
|
46
|
-
|
|
45
|
+
export function reviewLink(controlPlane, path, workspaceId) {
|
|
46
|
+
const url = new URL(path, ensureTrailing(controlPlane));
|
|
47
|
+
if (workspaceId !== undefined)
|
|
48
|
+
url.searchParams.set("workspaceId", workspaceId);
|
|
49
|
+
return url.toString();
|
|
47
50
|
}
|
|
48
51
|
/**
|
|
49
52
|
* The one place this command decides where a person reads a proposal.
|
|
@@ -55,8 +58,8 @@ export function reviewLink(controlPlane, path) {
|
|
|
55
58
|
* the retired address redirects to it permanently, so a link an older copy of
|
|
56
59
|
* this command already printed still lands on it.
|
|
57
60
|
*/
|
|
58
|
-
export function proposalReviewLink(controlPlane, proposalId) {
|
|
59
|
-
return reviewLink(controlPlane, `proposals/${encodeURIComponent(proposalId)}
|
|
61
|
+
export function proposalReviewLink(controlPlane, proposalId, workspaceId) {
|
|
62
|
+
return reviewLink(controlPlane, `proposals/${encodeURIComponent(proposalId)}`, workspaceId);
|
|
60
63
|
}
|
|
61
64
|
/** The address the whole waiting inbox is read on. */
|
|
62
65
|
export function proposalInboxLink(controlPlane) {
|
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
import { formatInstant } from "../local-time.js";
|
|
4
|
-
import {} from "../wire.js";
|
|
4
|
+
import { CLI_INVOCATION } from "../wire.js";
|
|
5
5
|
import { promiseRoot } from "./touch-map.js";
|
|
6
6
|
/** How many promises one path lists before the report says how many more. */
|
|
7
7
|
const NAMED_LIMIT = 20;
|
|
@@ -36,7 +36,7 @@ export async function runAffected(options) {
|
|
|
36
36
|
options.write(`${text}\n`);
|
|
37
37
|
};
|
|
38
38
|
if (options.paths.length === 0) {
|
|
39
|
-
const message =
|
|
39
|
+
const message = `Name at least one path: ${CLI_INVOCATION} affected src/orders/total.ts`;
|
|
40
40
|
if (options.json)
|
|
41
41
|
emit({ step: "error", reason: "usage", message, changed: false, exitCode: 4 });
|
|
42
42
|
else
|
|
@@ -55,8 +55,8 @@ export async function runAffected(options) {
|
|
|
55
55
|
const reading = readTouchMap(root);
|
|
56
56
|
if (reading.kind !== "map") {
|
|
57
57
|
const message = reading.kind === "absent"
|
|
58
|
-
? `There is no touch map in this checkout yet. Build one with:
|
|
59
|
-
: `${TOUCH_MAP_FILE} could not be read as a touch map. Build it again with:
|
|
58
|
+
? `There is no touch map in this checkout yet. Build one with: ${CLI_INVOCATION} touch-map`
|
|
59
|
+
: `${TOUCH_MAP_FILE} could not be read as a touch map. Build it again with: ${CLI_INVOCATION} touch-map`;
|
|
60
60
|
emit({ step: "affected", mapped: false, paths: [], changed: false });
|
|
61
61
|
say(message);
|
|
62
62
|
// Zero, because having no map is not a fault in the change somebody is
|
|
@@ -115,8 +115,8 @@ export async function runAffected(options) {
|
|
|
115
115
|
say("");
|
|
116
116
|
if (stale > 0) {
|
|
117
117
|
say(stale === 1
|
|
118
|
-
?
|
|
119
|
-
: `${stale} of those answers are stale. Run
|
|
118
|
+
? `One answer above is stale. Run ${CLI_INVOCATION} touch-map to measure it again.`
|
|
119
|
+
: `${stale} of those answers are stale. Run ${CLI_INVOCATION} touch-map to measure them again.`);
|
|
120
120
|
}
|
|
121
121
|
say("This came from your own checkout. Balladeer was not asked, and was told nothing.");
|
|
122
122
|
return 0;
|
|
@@ -3,7 +3,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { runCommand } from "../gh.js";
|
|
4
4
|
import { repositoryRoot } from "../git.js";
|
|
5
5
|
import { promiseForPath, sealedPromises } from "../seals.js";
|
|
6
|
-
import {} from "../wire.js";
|
|
6
|
+
import { CLI_INVOCATION } from "../wire.js";
|
|
7
7
|
/**
|
|
8
8
|
* Where the pinned runner sits once setup's instructions have been followed.
|
|
9
9
|
*
|
|
@@ -258,17 +258,17 @@ function installPrePushHook(root, options, emit, say) {
|
|
|
258
258
|
const hookPath = join(root, ".git", "hooks", "pre-push");
|
|
259
259
|
if (existsSync(hookPath)) {
|
|
260
260
|
const message = `${hookPath} already exists, so nothing was written. Add this line to it yourself if you ` +
|
|
261
|
-
|
|
261
|
+
`want the check: ${CLI_INVOCATION} check-seals`;
|
|
262
262
|
emit({ step: "error", reason: "hook_exists", message, changed: false, exitCode: 4 });
|
|
263
263
|
say(message);
|
|
264
264
|
return 4;
|
|
265
265
|
}
|
|
266
266
|
const script = [
|
|
267
267
|
"#!/bin/sh",
|
|
268
|
-
|
|
268
|
+
`# Installed by: ${CLI_INVOCATION} check-seals --install-hook`,
|
|
269
269
|
"# It stops a push that would break a promise's seal, and says nothing otherwise.",
|
|
270
270
|
"# Delete this file to remove it.",
|
|
271
|
-
|
|
271
|
+
`${CLI_INVOCATION} check-seals || exit 1`,
|
|
272
272
|
"",
|
|
273
273
|
].join("\n");
|
|
274
274
|
try {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { callAgentTool, selectAgent, structuredString } from "../agent.js";
|
|
2
|
+
import { callAgentTool, reportAgentEnforcementWarning, selectAgent, structuredString, } from "../agent.js";
|
|
3
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";
|
|
7
|
-
import {} from "../wire.js";
|
|
7
|
+
import { CLI_INVOCATION } from "../wire.js";
|
|
8
8
|
import { readTeachBackFile } from "./propose.js";
|
|
9
9
|
/**
|
|
10
10
|
* Ten promises at 64 KiB each, which is the per-promise cap `propose` already
|
|
@@ -197,7 +197,7 @@ export async function runDiscover(options) {
|
|
|
197
197
|
return exitCode;
|
|
198
198
|
};
|
|
199
199
|
if (!options.file) {
|
|
200
|
-
return fail("usage",
|
|
200
|
+
return fail("usage", `Give a catalog file: ${CLI_INVOCATION} discover --file <path>.`, 4);
|
|
201
201
|
}
|
|
202
202
|
let raw;
|
|
203
203
|
try {
|
|
@@ -232,6 +232,7 @@ export async function runDiscover(options) {
|
|
|
232
232
|
`Run \`${commandLine(null, "setup")}\` here again to pair this machine, which records who you are, or name the owner yourself with --owner <membership id> from the members list on the Balladeer MCP server's setup tool.`, 4);
|
|
233
233
|
}
|
|
234
234
|
const setup = await callAgentTool(agent, "get_promise_setup", {});
|
|
235
|
+
reportAgentEnforcementWarning(setup, options);
|
|
235
236
|
if (setup.kind !== "result") {
|
|
236
237
|
return fail("owner_unverified", `${memberListRefusal(setup.kind, options.controlPlane)} Nothing was sent.`, 5);
|
|
237
238
|
}
|
|
@@ -254,7 +255,7 @@ export async function runDiscover(options) {
|
|
|
254
255
|
index: index + 1,
|
|
255
256
|
title,
|
|
256
257
|
candidateId: outcome.candidateId,
|
|
257
|
-
reviewUrl: proposalReviewLink(options.controlPlane, outcome.candidateId),
|
|
258
|
+
reviewUrl: proposalReviewLink(options.controlPlane, outcome.candidateId, agent.workspaceId),
|
|
258
259
|
};
|
|
259
260
|
filed.push(entry);
|
|
260
261
|
emit({
|
package/dist/commands/prepare.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
-
import { callAgentTool, noAgentCredentialSentence, selectAgent } from "../agent.js";
|
|
3
|
+
import { callAgentTool, noAgentCredentialSentence, reportAgentEnforcementWarning, selectAgent, } from "../agent.js";
|
|
4
4
|
import { inReadersZone } from "../local-time.js";
|
|
5
5
|
import { commandLine } from "../release.js";
|
|
6
6
|
import { repositoryHint } from "../repository.js";
|
|
@@ -150,6 +150,7 @@ export async function runPrepare(options) {
|
|
|
150
150
|
promiseId: options.promiseId,
|
|
151
151
|
again: options.again,
|
|
152
152
|
});
|
|
153
|
+
reportAgentEnforcementWarning(call, options);
|
|
153
154
|
switch (call.kind) {
|
|
154
155
|
case "endpoint_refused":
|
|
155
156
|
return fail("agent_endpoint_unsafe", `${call.reason} Nothing was prepared.`, 4);
|
package/dist/commands/propose.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { callAgentTool, noAgentCredentialSentence, selectAgent, structuredString, } from "../agent.js";
|
|
2
|
+
import { callAgentTool, noAgentCredentialSentence, reportAgentEnforcementWarning, selectAgent, structuredString, } from "../agent.js";
|
|
3
3
|
import { proposalReviewLink } from "../client.js";
|
|
4
4
|
import { commandLine } from "../release.js";
|
|
5
5
|
import { repositoryHint } from "../repository.js";
|
|
6
6
|
import { StoreError, readCredentials } from "../store.js";
|
|
7
|
-
import {} from "../wire.js";
|
|
7
|
+
import { CLI_INVOCATION } from "../wire.js";
|
|
8
8
|
const MAX_FILE_BYTES = 64 * 1024;
|
|
9
9
|
/**
|
|
10
10
|
* Every field the server's schema requires with no default. It is every
|
|
@@ -190,7 +190,7 @@ export async function runPropose(options) {
|
|
|
190
190
|
return exitCode;
|
|
191
191
|
};
|
|
192
192
|
if (!options.file) {
|
|
193
|
-
return fail("usage",
|
|
193
|
+
return fail("usage", `Give a proposal file: ${CLI_INVOCATION} propose --file <path>.`, 4);
|
|
194
194
|
}
|
|
195
195
|
let raw;
|
|
196
196
|
try {
|
|
@@ -240,6 +240,7 @@ export async function runPropose(options) {
|
|
|
240
240
|
? {}
|
|
241
241
|
: { leastSure: parsed.value.teachBack.leastSure }),
|
|
242
242
|
});
|
|
243
|
+
reportAgentEnforcementWarning(call, options);
|
|
243
244
|
switch (call.kind) {
|
|
244
245
|
case "endpoint_refused":
|
|
245
246
|
return fail("agent_endpoint_unsafe", `${call.reason} Nothing was sent.`, 4);
|
|
@@ -273,7 +274,7 @@ export async function runPropose(options) {
|
|
|
273
274
|
// The review link is built from the address this copy paired with. The tool
|
|
274
275
|
// answers with an id and no link at all, so there is nothing here a
|
|
275
276
|
// misconfigured public base URL could redirect.
|
|
276
|
-
const review = proposalReviewLink(options.controlPlane, candidateId);
|
|
277
|
+
const review = proposalReviewLink(options.controlPlane, candidateId, agent.workspaceId);
|
|
277
278
|
emit({ step: "promise", status: "proposed", candidateId, reviewUrl: review });
|
|
278
279
|
if (!options.json) {
|
|
279
280
|
options.write("Proposed. You will own it unless you named someone else; the owner reads it and clicks Agree, and nothing else can.\n");
|
|
@@ -94,6 +94,7 @@ function report(options, rows, workspaceKnown) {
|
|
|
94
94
|
say(options, workspaceKnown
|
|
95
95
|
? "Repositories your GitHub account can see. Ones already in Balladeer are marked."
|
|
96
96
|
: "Repositories your GitHub account can see. Nothing is stored for this Balladeer, so which of them are already added is unknown.");
|
|
97
|
+
say(options, "These are GitHub repositories, not local directories or git worktrees.");
|
|
97
98
|
say(options, "");
|
|
98
99
|
for (const row of rows) {
|
|
99
100
|
const marks = [
|