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 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 repository connection
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
- This setup creates two credentials, both of which you control. Approving in the browser lets this
62
- session act as you to add repositories, connect your coding agent, connect CI, and propose promises;
63
- it cannot approve, activate, or remove anything. That session lasts a day, each use buys it another
64
- day, and it is gone seven days after you approved it however much you used it. The coding agent's
65
- own connection has no expiry: it lets the agent read the promises your team has approved, propose
66
- new ones, and propose replacing, retiring, excepting, or reassigning one you already have; every one
67
- of those waits for a named person to decide it. It can also carry out one of those acts for you, and
68
- only this way: you open a Balladeer page in your own browser, read what the act is, sign it off
69
- there, and read back the one-time code that page gives you. That code covers that one act on that
70
- one thing, once, and Balladeer records you as the person who took it and the connection as the
71
- messenger. Without a code you signed, the connection cannot approve, activate, grant, transfer,
72
- retire, or delete anything. You can revoke either one at any time in Balladeer, and the setup
73
- session also ends on its own.
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 | 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 |
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 keep both.
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
- balladeer setup [--repository owner/name]... [--json] [--wait] [--control-plane <url>]
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
- balladeer repositories [--json] [--control-plane <url>]
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
- balladeer invite <email>... [--role contributor|viewer|administrator] [--json]
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
- balladeer status [<promise id>] [--repo owner/name] [--repository <uuid>] [--json]
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
- balladeer propose --file <path> [--repo owner/name] [--repository <uuid>] [--json]
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
- balladeer prepare <promise id> [--again] [--repo owner/name] [--repository <uuid>]
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
- balladeer discover --file <path> [--repo owner/name] [--repository <uuid>]
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
- balladeer check-seals [--json] [--runner <path>] [--install-hook]
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
- balladeer touch-map [--json]
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
- balladeer affected <paths...> [--json]
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
- balladeer session [--new] [--record] [--repo owner/name] [--repository <uuid>] [--json]
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
- balladeer explain [--control-plane <url>]
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
- balladeer whoami [--json] [--control-plane <url>]
125
+ ${CLI_INVOCATION} whoami [--json] [--control-plane <url>]
122
126
  Report the stored setup session's workspace, role, scopes, and expiry.
123
127
 
124
- balladeer agent rotate [--control-plane <url>]
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
- balladeer mcp [--repository <uuid>] [--control-plane <url>]
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", "--again belongs to prepare: run `balladeer prepare <promise id> --again`.");
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", "--refresh belongs to setup: run `balladeer setup --refresh`.");
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", "--force belongs to setup: run `balladeer setup --force`.");
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", "--claude-desktop belongs to setup: run `balladeer setup --claude-desktop`.");
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("prepare reads one promise id: balladeer prepare <promise id> [--again].\n");
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
- return new URL(path, ensureTrailing(controlPlane)).toString();
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 = "Name at least one path: balladeer affected src/orders/total.ts";
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: balladeer touch-map`
59
- : `${TOUCH_MAP_FILE} could not be read as a touch map. Build it again with: balladeer touch-map`;
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
- ? "One answer above is stale. Run balladeer touch-map to measure it again."
119
- : `${stale} of those answers are stale. Run balladeer touch-map to measure them again.`);
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
- "want the check: balladeer check-seals";
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
- "# Installed by: balladeer check-seals --install-hook",
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
- "balladeer check-seals || exit 1",
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", "Give a catalog file: balladeer discover --file <path>.", 4);
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({
@@ -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);
@@ -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", "Give a proposal file: balladeer propose --file <path>.", 4);
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 = [