balladeer 0.0.4 → 1.0.0

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.
Files changed (60) hide show
  1. package/LICENSE +200 -5
  2. package/README.md +154 -68
  3. package/dist/agent.d.ts +126 -0
  4. package/dist/agent.js +209 -0
  5. package/dist/cli.d.ts +34 -0
  6. package/dist/cli.js +392 -0
  7. package/dist/client.d.ts +44 -0
  8. package/dist/client.js +114 -0
  9. package/dist/commands/affected.d.ts +22 -0
  10. package/dist/commands/affected.js +122 -0
  11. package/dist/commands/check-seals.d.ts +37 -0
  12. package/dist/commands/check-seals.js +289 -0
  13. package/dist/commands/discover.d.ts +68 -0
  14. package/dist/commands/discover.js +395 -0
  15. package/dist/commands/explain.d.ts +35 -0
  16. package/dist/commands/explain.js +90 -0
  17. package/dist/commands/invite.d.ts +24 -0
  18. package/dist/commands/invite.js +197 -0
  19. package/dist/commands/mcp.d.ts +65 -0
  20. package/dist/commands/mcp.js +202 -0
  21. package/dist/commands/propose.d.ts +59 -0
  22. package/dist/commands/propose.js +262 -0
  23. package/dist/commands/repositories.d.ts +18 -0
  24. package/dist/commands/repositories.js +185 -0
  25. package/dist/commands/setup.d.ts +75 -0
  26. package/dist/commands/setup.js +1471 -0
  27. package/dist/commands/status.d.ts +35 -0
  28. package/dist/commands/status.js +482 -0
  29. package/dist/commands/touch-map.d.ts +42 -0
  30. package/dist/commands/touch-map.js +251 -0
  31. package/dist/commands/whoami.d.ts +8 -0
  32. package/dist/commands/whoami.js +79 -0
  33. package/dist/conventions.d.ts +69 -0
  34. package/dist/conventions.js +175 -0
  35. package/dist/copy.d.ts +148 -0
  36. package/dist/copy.js +459 -0
  37. package/dist/currency.d.ts +31 -0
  38. package/dist/currency.js +72 -0
  39. package/dist/gh.d.ts +80 -0
  40. package/dist/gh.js +188 -0
  41. package/dist/git.d.ts +76 -0
  42. package/dist/git.js +203 -0
  43. package/dist/markers.d.ts +76 -0
  44. package/dist/markers.js +125 -0
  45. package/dist/mcp-config.d.ts +99 -0
  46. package/dist/mcp-config.js +230 -0
  47. package/dist/release.d.ts +55 -0
  48. package/dist/release.js +67 -0
  49. package/dist/repository.d.ts +8 -0
  50. package/dist/repository.js +32 -0
  51. package/dist/seals.d.ts +48 -0
  52. package/dist/seals.js +112 -0
  53. package/dist/store.d.ts +98 -0
  54. package/dist/store.js +225 -0
  55. package/dist/touch-map.d.ts +241 -0
  56. package/dist/touch-map.js +487 -0
  57. package/dist/wire.d.ts +588 -0
  58. package/dist/wire.js +20 -0
  59. package/package.json +19 -10
  60. package/bin/balladeer.js +0 -136
@@ -0,0 +1,197 @@
1
+ import { ClientTooOldError, RefusalError, TransportError, request } from "../client.js";
2
+ import { commandLine } from "../release.js";
3
+ import { StoreError, findSession, readCredentials } from "../store.js";
4
+ import {} from "../wire.js";
5
+ /**
6
+ * What a person types, and what the ledger stores.
7
+ *
8
+ * "Administrator" is the word every screen uses and the word a person says out
9
+ * loud, and `admin` is the value the ledger holds. Both are accepted so nobody
10
+ * has to know which of the two this command wanted, and the confirmation prints
11
+ * the long form back, because that is the word on the settings page they will
12
+ * see this person listed under.
13
+ */
14
+ const ROLES = {
15
+ administrator: "admin",
16
+ admin: "admin",
17
+ contributor: "contributor",
18
+ viewer: "viewer",
19
+ };
20
+ export const ROLE_NAMES = {
21
+ admin: "administrator",
22
+ contributor: "contributor",
23
+ viewer: "viewer",
24
+ };
25
+ /** What each role may do, in one clause, so nobody invites by guessing. */
26
+ export const ROLE_MEANINGS = {
27
+ admin: "read this workspace, propose promises, change setup, and invite people",
28
+ contributor: "read this workspace and propose promises",
29
+ viewer: "read this workspace",
30
+ };
31
+ export function parseRole(value) {
32
+ if (value === undefined)
33
+ return "contributor";
34
+ return ROLES[value.trim().toLowerCase()];
35
+ }
36
+ /**
37
+ * An address this command will send. Deliberately narrow rather than clever: it
38
+ * refuses here so a typo costs a sentence rather than an invitation nobody can
39
+ * recall, and the server's own schema refuses it again.
40
+ */
41
+ const EMAIL = /^[^\s@,;]+@[^\s@,;.]+(?:\.[^\s@,;.]+)+$/;
42
+ export function looksLikeEmail(value) {
43
+ return value.length <= 320 && EMAIL.test(value);
44
+ }
45
+ function emit(options, step) {
46
+ if (options.json)
47
+ options.write(`${JSON.stringify(step)}\n`);
48
+ }
49
+ function say(options, text) {
50
+ if (!options.json)
51
+ options.write(`${text}\n`);
52
+ }
53
+ function fail(options, reason, message, exitCode) {
54
+ say(options, message);
55
+ emit(options, { step: "error", reason, message, changed: false, exitCode });
56
+ return exitCode;
57
+ }
58
+ /**
59
+ * The refusal a person meets when their approver's role never carried this.
60
+ *
61
+ * Said before anything is sent, from the scopes the session itself reports,
62
+ * because the alternative is a person typing four addresses and being refused
63
+ * by the server four times for a reason that was knowable before the first one.
64
+ */
65
+ function withoutTheScope(options, session) {
66
+ return fail(options, "invite_not_permitted", [
67
+ `This setup session cannot invite anybody into ${session.workspaceName}.`,
68
+ " Inviting is an administrator's act, and a session carries only what the person who approved it could do unaided.",
69
+ ` Next: ask an administrator of ${session.workspaceName} to run this command, or to invite people in Balladeer under workspace settings.`,
70
+ ].join("\n"), 4);
71
+ }
72
+ /**
73
+ * Invites teammates by email, one at a time, and says what happened to each.
74
+ *
75
+ * Per address rather than per run. Sending four invitations is four separate
76
+ * things that can each succeed or be refused on their own, and a person reading
77
+ * "3 of 4 sent" cannot tell whose email is missing. Every address gets its own
78
+ * line and its own object, and the exit code says only whether every one of
79
+ * them went.
80
+ */
81
+ export async function runInvite(options) {
82
+ if (options.emails.length === 0) {
83
+ return fail(options, "usage", `Name at least one email address to invite. For example: ${commandLine(null, "invite alice@example.com")}`, 4);
84
+ }
85
+ const malformed = options.emails.filter((email) => !looksLikeEmail(email));
86
+ if (malformed.length > 0) {
87
+ return fail(options, "usage", `That is not an email address: ${malformed.join(", ")}. Nothing was sent.`, 4);
88
+ }
89
+ let credentials;
90
+ try {
91
+ credentials = readCredentials(options.environment);
92
+ }
93
+ catch (error) {
94
+ return fail(options, error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
95
+ }
96
+ const session = findSession(credentials, options.controlPlane);
97
+ if (session === undefined) {
98
+ return fail(options, "no_stored_session", `No Balladeer session is stored for ${options.controlPlane}. Run: ${commandLine(null, "setup")}`, 4);
99
+ }
100
+ // Read off the stored session rather than attempted and reported back. The
101
+ // approval page lists what the person granted, and this is one of the five.
102
+ if (!session.scopes.includes("workspace:invite")) {
103
+ return withoutTheScope(options, session);
104
+ }
105
+ let refused = 0;
106
+ for (const email of options.emails) {
107
+ const sent = await inviteOne(options, session, email);
108
+ if (!sent)
109
+ refused += 1;
110
+ }
111
+ if (refused === 0) {
112
+ say(options, `\nEach person gets an email from Balladeer with a link to accept. Nobody is in ${session.workspaceName} until they accept, and until then they are listed as invited under workspace settings.`);
113
+ return 0;
114
+ }
115
+ return 5;
116
+ }
117
+ async function inviteOne(options, session, email) {
118
+ const body = { email, requestedRole: options.role };
119
+ try {
120
+ const answer = await request(options.controlPlane, {
121
+ method: "POST",
122
+ path: "/api/setup/v1/invitations",
123
+ bearer: session.token,
124
+ body,
125
+ });
126
+ const role = ROLE_NAMES[answer.requestedRole];
127
+ say(options, `Invited ${answer.email} to ${answer.workspaceName} as ${role}. Balladeer has sent them the email.`);
128
+ say(options, ` They can ${ROLE_MEANINGS[answer.requestedRole]}. They join when they accept, and not before.`);
129
+ if (answer.expiresAt !== null) {
130
+ say(options, ` The invitation stops working at ${answer.expiresAt}.`);
131
+ }
132
+ emit(options, {
133
+ step: "invitation",
134
+ status: "sent",
135
+ email: answer.email,
136
+ role: answer.requestedRole,
137
+ workspace: answer.workspaceName,
138
+ expiresAt: answer.expiresAt,
139
+ });
140
+ return true;
141
+ }
142
+ catch (error) {
143
+ const { reason, message } = invitationRefusal(options, email, error);
144
+ say(options, message);
145
+ emit(options, {
146
+ step: "invitation",
147
+ status: "refused",
148
+ email,
149
+ role: options.role,
150
+ reason,
151
+ message,
152
+ });
153
+ return false;
154
+ }
155
+ }
156
+ /**
157
+ * One refusal, in the words the person can act on.
158
+ *
159
+ * Every arm names the address, because a run inviting four people prints four
160
+ * lines and a bare "already a member" belongs to one of them. Nothing here
161
+ * invents a remedy the server did not offer: where the server said why, that
162
+ * sentence is what is printed.
163
+ */
164
+ function invitationRefusal(options, email, error) {
165
+ if (error instanceof ClientTooOldError) {
166
+ return { reason: "client_too_old", message: `${email}: ${error.message}` };
167
+ }
168
+ if (error instanceof TransportError) {
169
+ return {
170
+ reason: "control_plane_unreachable",
171
+ message: `${email}: Balladeer could not be reached at ${options.controlPlane}: ${error.message}. No invitation was sent to this address.`,
172
+ };
173
+ }
174
+ if (error instanceof RefusalError) {
175
+ if (error.code === "delegated_authority_refused" || error.code === "action_not_allowed") {
176
+ return {
177
+ reason: error.code,
178
+ message: `${email}: this setup session may not invite anybody. Ask an administrator to run this command, or to invite people in Balladeer under workspace settings.`,
179
+ };
180
+ }
181
+ if (error.code === "session_expired" || error.code === "session_revoked") {
182
+ return {
183
+ reason: error.code,
184
+ message: `${email}: this setup session has ended, so nothing was sent. Run \`${commandLine(null, "setup")}\` to pair again.`,
185
+ };
186
+ }
187
+ // The server's own sentence. "That email already has an active membership"
188
+ // and "an incompatible or already-confirmed invitation is still open for
189
+ // that email" are both things a person acts on, and both are already
190
+ // written the way a person reads them.
191
+ return { reason: error.code, message: `${email}: ${error.message}` };
192
+ }
193
+ return {
194
+ reason: "invitation_failed",
195
+ message: `${email}: Balladeer could not send this invitation. Nothing was sent to this address.`,
196
+ };
197
+ }
@@ -0,0 +1,65 @@
1
+ import { type StoredAgent } from "../store.js";
2
+ export { forwarderHeaders, selectAgent, type AgentSelection } from "../agent.js";
3
+ export type McpOptions = Readonly<{
4
+ controlPlane: string;
5
+ /**
6
+ * Which repository's credential to forward. The host config setup writes
7
+ * always names it, because a workspace holds several repositories and each one
8
+ * has its own connection. Without it, the credential belonging to the
9
+ * repository this process was started in is used, and where there is no such
10
+ * credential the command refuses rather than guessing.
11
+ */
12
+ repositoryId?: string;
13
+ environment: NodeJS.ProcessEnv;
14
+ cwd: string;
15
+ stdin: NodeJS.ReadableStream;
16
+ write: (text: string) => void;
17
+ error: (text: string) => void;
18
+ }>;
19
+ export type ProbeResult = Readonly<{
20
+ kind: "answered";
21
+ repositoryId: string;
22
+ }> | Readonly<{
23
+ kind: "refused";
24
+ reason: string;
25
+ nextAction: string;
26
+ }>;
27
+ /**
28
+ * One real call over the connection just issued, before anything claims it
29
+ * works.
30
+ *
31
+ * It is `get_promise_setup` because that tool answers for the bound repository
32
+ * and nothing else, so its answer proves three things at once: the credential
33
+ * authenticated on the MCP endpoint, Balladeer served this repository's
34
+ * connection rather than another's, and the frames this forwarder writes are
35
+ * frames that server accepts. A response for a different repository is a failure
36
+ * here, not a curiosity.
37
+ *
38
+ * It sends what the forwarder sends, over the endpoint the forwarder would use,
39
+ * and refuses the same entries the forwarder refuses. What it cannot prove is
40
+ * whether an agent host has loaded the entry yet, which is why the step that
41
+ * calls this also names the host's own next action rather than implying there
42
+ * is none.
43
+ */
44
+ export declare function probeAgentConnection(agent: StoredAgent, timeoutMs?: number): Promise<ProbeResult>;
45
+ /**
46
+ * The frame a host is owed when the server refuses this copy of the program.
47
+ *
48
+ * A 426 body is the control plane's JSON, not JSON-RPC, and relaying it to a
49
+ * host would surface as a protocol error with no sentence in it. The host is
50
+ * told in its own protocol what happened and which line fixes it, and the same
51
+ * sentence goes to stderr for whoever is reading the log.
52
+ */
53
+ export declare function staleClientFrame(id: unknown, update: string): string;
54
+ /**
55
+ * The frame a host is owed when the credential itself is finished.
56
+ *
57
+ * The server refuses a revoked connection at the door, so the model never
58
+ * reaches a tool and never sees the tool-level refusal that would have told it
59
+ * apart from a missing scope. Without this it sees a server that died. The
60
+ * sentence is the same distinction the server's own refusals draw: this is the
61
+ * connection being gone, not this tool being unavailable, and the remedy is a
62
+ * person's.
63
+ */
64
+ export declare function revokedConnectionFrame(id: unknown, controlPlane: string): string;
65
+ export declare function runMcp(options: McpOptions): Promise<number>;
@@ -0,0 +1,202 @@
1
+ import { createInterface } from "node:readline";
2
+ import { callAgentTool, forwarderHeaders, noAgentCredentialSentence, safeJson, selectAgent, structuredString, updateLine, } from "../agent.js";
3
+ import { noteServerVersion, updateNotice } from "../currency.js";
4
+ import { repositoryHint } from "../repository.js";
5
+ import { StoreError, readCredentials } from "../store.js";
6
+ // The three commands that use an agent connection select and address it through
7
+ // one module. These are re-exported because this is where the forwarder's
8
+ // callers already look for them.
9
+ export { forwarderHeaders, selectAgent } from "../agent.js";
10
+ const MAX_REQUEST_BYTES = 1024 * 1024;
11
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
12
+ /**
13
+ * One real call over the connection just issued, before anything claims it
14
+ * works.
15
+ *
16
+ * It is `get_promise_setup` because that tool answers for the bound repository
17
+ * and nothing else, so its answer proves three things at once: the credential
18
+ * authenticated on the MCP endpoint, Balladeer served this repository's
19
+ * connection rather than another's, and the frames this forwarder writes are
20
+ * frames that server accepts. A response for a different repository is a failure
21
+ * here, not a curiosity.
22
+ *
23
+ * It sends what the forwarder sends, over the endpoint the forwarder would use,
24
+ * and refuses the same entries the forwarder refuses. What it cannot prove is
25
+ * whether an agent host has loaded the entry yet, which is why the step that
26
+ * calls this also names the host's own next action rather than implying there
27
+ * is none.
28
+ */
29
+ export async function probeAgentConnection(agent, timeoutMs = 15_000) {
30
+ const call = await callAgentTool(agent, "get_promise_setup", {}, timeoutMs);
31
+ switch (call.kind) {
32
+ case "endpoint_refused":
33
+ return {
34
+ kind: "refused",
35
+ reason: call.reason,
36
+ nextAction: "Issue the connection again from the Balladeer setup page.",
37
+ };
38
+ case "unreachable":
39
+ return {
40
+ kind: "refused",
41
+ reason: "Balladeer's MCP endpoint could not be reached from this machine",
42
+ nextAction: "Check this machine's network access, then run this command again.",
43
+ };
44
+ case "client_too_old":
45
+ return {
46
+ kind: "refused",
47
+ reason: "this copy of the command is too old for that Balladeer",
48
+ nextAction: `Update it with: ${call.update}, then run this command again.`,
49
+ };
50
+ case "unauthorized":
51
+ return {
52
+ kind: "refused",
53
+ reason: "Balladeer refused the credential this step just issued",
54
+ nextAction: "Issue the connection again from the Balladeer setup page.",
55
+ };
56
+ case "http":
57
+ return {
58
+ kind: "refused",
59
+ reason: `Balladeer answered ${call.status} to a first call over this connection`,
60
+ nextAction: "Run this command again; if it repeats, report the status above.",
61
+ };
62
+ case "result": {
63
+ const repositoryId = structuredString(call.structured, "repositoryId");
64
+ if (repositoryId === undefined)
65
+ break;
66
+ if (repositoryId !== agent.repositoryId) {
67
+ return {
68
+ kind: "refused",
69
+ reason: "that connection answered for a different repository",
70
+ nextAction: "Issue the connection again from the Balladeer setup page, and do not use it.",
71
+ };
72
+ }
73
+ return { kind: "answered", repositoryId };
74
+ }
75
+ default:
76
+ break;
77
+ }
78
+ return {
79
+ kind: "refused",
80
+ reason: "Balladeer's answer to a first call over this connection was not a tool result",
81
+ nextAction: "Run this command again; if it repeats, report that to Balladeer.",
82
+ };
83
+ }
84
+ /**
85
+ * The frame a host is owed when the server refuses this copy of the program.
86
+ *
87
+ * A 426 body is the control plane's JSON, not JSON-RPC, and relaying it to a
88
+ * host would surface as a protocol error with no sentence in it. The host is
89
+ * told in its own protocol what happened and which line fixes it, and the same
90
+ * sentence goes to stderr for whoever is reading the log.
91
+ */
92
+ export function staleClientFrame(id, update) {
93
+ return errorFrame(id, `This copy of the Balladeer command is too old for this Balladeer. Update it with: ${update}`);
94
+ }
95
+ /**
96
+ * The frame a host is owed when the credential itself is finished.
97
+ *
98
+ * The server refuses a revoked connection at the door, so the model never
99
+ * reaches a tool and never sees the tool-level refusal that would have told it
100
+ * apart from a missing scope. Without this it sees a server that died. The
101
+ * sentence is the same distinction the server's own refusals draw: this is the
102
+ * connection being gone, not this tool being unavailable, and the remedy is a
103
+ * person's.
104
+ */
105
+ export function revokedConnectionFrame(id, controlPlane) {
106
+ return errorFrame(id, `Balladeer refused this agent connection: it was revoked, or the repository it was bound to is no longer enrolled. This is the connection being gone rather than one tool being unavailable, so retrying will not help. Ask the person to run Balladeer setup again in this repository, or to issue a new connection at ${controlPlane}/setup.`);
107
+ }
108
+ function errorFrame(id, message) {
109
+ return JSON.stringify({
110
+ jsonrpc: "2.0",
111
+ id: id ?? null,
112
+ error: { code: -32000, message },
113
+ });
114
+ }
115
+ function frameId(line) {
116
+ try {
117
+ const parsed = JSON.parse(line);
118
+ if (parsed !== null && typeof parsed === "object") {
119
+ const id = parsed.id;
120
+ if (typeof id === "string" || typeof id === "number")
121
+ return id;
122
+ }
123
+ }
124
+ catch {
125
+ // A frame we cannot parse still gets an answer, with a null id.
126
+ }
127
+ return null;
128
+ }
129
+ export async function runMcp(options) {
130
+ let credentials;
131
+ try {
132
+ credentials = readCredentials(options.environment);
133
+ }
134
+ catch (error) {
135
+ options.error(`${error instanceof StoreError ? error.message : String(error)}\n`);
136
+ return 4;
137
+ }
138
+ const selection = selectAgent(credentials.agents, options.controlPlane, options.repositoryId, repositoryHint(options.cwd));
139
+ if (selection.kind === "refused") {
140
+ // Issuing one in the browser is what this used to suggest here, and it is the
141
+ // move that produces this state: the bearer is shown once, there, and the
142
+ // forwarder reads the store on this machine.
143
+ options.error(selection.missingFor === undefined
144
+ ? `${selection.reason} Run setup in this repository, or issue the connection at ${options.controlPlane}/setup.\n`
145
+ : `${noAgentCredentialSentence(selection.missingFor)}\n`);
146
+ return 4;
147
+ }
148
+ const { agent } = selection;
149
+ let saidUpdate = false;
150
+ const lines = createInterface({ input: options.stdin, crlfDelay: Infinity });
151
+ for await (const line of lines) {
152
+ if (line.trim().length === 0)
153
+ continue;
154
+ if (Buffer.byteLength(line, "utf8") > MAX_REQUEST_BYTES) {
155
+ options.error("Balladeer refused a request frame larger than 1 MiB.\n");
156
+ return 5;
157
+ }
158
+ let response;
159
+ try {
160
+ response = await fetch(agent.mcpUrl, {
161
+ method: "POST",
162
+ headers: forwarderHeaders(agent.token),
163
+ body: line,
164
+ redirect: "manual",
165
+ });
166
+ }
167
+ catch {
168
+ options.error("Balladeer could not be reached. Nothing was changed.\n");
169
+ return 5;
170
+ }
171
+ // The host's own channel for this is the instructions the server sends on
172
+ // initialize, which reach the model. This line reaches whoever is reading
173
+ // the log, once per session rather than once per frame.
174
+ noteServerVersion(response.headers);
175
+ const notice = updateNotice();
176
+ if (notice !== undefined && !saidUpdate) {
177
+ saidUpdate = true;
178
+ options.error(`${notice}\n`);
179
+ }
180
+ if (response.status === 401) {
181
+ options.write(`${revokedConnectionFrame(frameId(line), options.controlPlane)}\n`);
182
+ options.error("Balladeer refused this agent connection. Rotate it in Balladeer.\n");
183
+ return 5;
184
+ }
185
+ // The version floor, answered in the host's own protocol. A stale forwarder
186
+ // that has been running against this control plane for months learns here
187
+ // what to run, and the host sees a sentence rather than a parse failure.
188
+ if (response.status === 426) {
189
+ const update = updateLine(await safeJson(response));
190
+ options.write(`${staleClientFrame(frameId(line), update)}\n`);
191
+ options.error(`This copy of the Balladeer command is too old for ${options.controlPlane}. Update it with: ${update}\n`);
192
+ return 3;
193
+ }
194
+ const text = await response.text();
195
+ if (Buffer.byteLength(text, "utf8") > MAX_RESPONSE_BYTES) {
196
+ options.error("Balladeer refused a response frame larger than 4 MiB.\n");
197
+ return 5;
198
+ }
199
+ options.write(`${text.replace(/\n+$/, "")}\n`);
200
+ }
201
+ return 0;
202
+ }
@@ -0,0 +1,59 @@
1
+ export type ProposeOptions = Readonly<{
2
+ controlPlane: string;
3
+ json: boolean;
4
+ file?: string;
5
+ /** The repository whose connection to use, named as `owner/name`. */
6
+ repo?: string;
7
+ /** The repository whose connection to use, named by id, as `mcp` takes it. */
8
+ repository?: string;
9
+ environment: NodeJS.ProcessEnv;
10
+ cwd: string;
11
+ write: (text: string) => void;
12
+ }>;
13
+ export type TeachBackRequest = Readonly<{
14
+ explicitIntent: string;
15
+ teachBack: Readonly<{
16
+ meaning: Record<string, unknown>;
17
+ unresolvedQuestions?: unknown;
18
+ proposedOwnerId?: unknown;
19
+ /** How sure whoever wrote this file was, 0 to 1. Read, never assumed. */
20
+ confidence: number;
21
+ }>;
22
+ }>;
23
+ /**
24
+ * The shape check that happens on the developer's machine, and the body it
25
+ * builds.
26
+ *
27
+ * `--file` reads whatever path it is handed, and an agent that mistypes one
28
+ * would otherwise upload a `.env`, a source file, or a log to a service whose
29
+ * whole boundary is that it never receives those. Refusing here means the wrong
30
+ * file never leaves the machine, rather than leaving it and being refused.
31
+ *
32
+ * So this returns the exact object to send rather than a verdict on the file.
33
+ * Spreading the parsed file into the request was the same mistake in a quieter
34
+ * form: whatever else the file happened to carry travelled with it, and the
35
+ * strict schema on the other side is a refusal after the fact, not a boundary.
36
+ */
37
+ export declare function readTeachBackFile(raw: string): Readonly<{
38
+ ok: true;
39
+ value: TeachBackRequest;
40
+ }> | Readonly<{
41
+ ok: false;
42
+ reason: string;
43
+ }>;
44
+ /**
45
+ * Proposes one promise over this repository's own agent connection.
46
+ *
47
+ * It used to go over the delegated setup session, which lives for a bounded time
48
+ * and is meant for setting Balladeer up. That made proposing something a person
49
+ * could only do for a while after pairing: the credential expired, and a command
50
+ * whose whole job is "propose this" answered "run setup again". The connection
51
+ * this uses instead is the one setup issued for this repository, the same one
52
+ * the agent host forwards, and it does not expire.
53
+ *
54
+ * Nothing widens by moving: the call is `propose_promise` on the MCP endpoint,
55
+ * so it is authenticated as the agent principal, bound to that one repository,
56
+ * and refused everything except proposing. It lands as a candidate awaiting a
57
+ * named person's agreement, and nothing this command can do agrees to it.
58
+ */
59
+ export declare function runPropose(options: ProposeOptions): Promise<number>;