balladeer 1.0.4 → 1.0.5
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 +56 -4
- package/dist/cli.js +20 -2
- package/dist/commands/setup.d.ts +5 -3
- package/dist/commands/setup.js +5 -3
- package/dist/commands/status.js +2 -2
- package/dist/conventions.d.ts +1 -1
- package/dist/conventions.js +1 -1
- package/dist/copy.d.ts +5 -5
- package/dist/copy.js +17 -10
- package/dist/currency.js +1 -1
- package/dist/install.d.ts +23 -0
- package/dist/install.js +270 -0
- package/dist/release.d.ts +4 -16
- package/dist/release.js +6 -16
- package/dist/wire.d.ts +2 -2
- package/dist/wire.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,10 +22,36 @@ do. To run it yourself instead:
|
|
|
22
22
|
npx -y balladeer@latest setup
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
Starting with CLI 1.0.5 on supported macOS/Linux hosts, setup installs the `balladeer` executable
|
|
26
|
+
before pairing. After setup, use `balladeer status`, `balladeer session` and other everyday
|
|
27
|
+
commands. To install or repair only the executable, without pairing or changing a repository:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx -y balladeer@latest install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That bootstrap selects the latest published release. `balladeer install` repairs the running
|
|
34
|
+
release. If your shell cannot find `balladeer` after installation, reopen the terminal or coding
|
|
35
|
+
agent so it sees the current PATH, then check `balladeer --version`. A running child command cannot
|
|
36
|
+
change its parent's environment. Older setups without the executable can still use
|
|
37
|
+
`npx -y balladeer@latest <command>`; the new install command requires 1.0.5 or newer.
|
|
38
|
+
|
|
39
|
+
To upgrade the executable and refresh in one command, run `npx -y balladeer@latest setup --refresh`
|
|
40
|
+
(add `--client codex` for Codex). This bootstrap selects the published update.
|
|
41
|
+
|
|
42
|
+
Run `balladeer setup --refresh` to refresh repository instructions without replacing other tools'
|
|
43
|
+
settings. For Codex, keep `--client codex`: `balladeer setup --client codex --refresh`. Existing
|
|
44
|
+
instructions are not refreshed just because a package was installed. Refresh can contact npm and
|
|
45
|
+
write the local toolchain when ensuring the executable is installed; it does not perform a Balladeer
|
|
46
|
+
authority action. The repository MCP entry continues to use `npx -y balladeer@latest mcp` so native
|
|
47
|
+
tools can pick up published updates.
|
|
48
|
+
|
|
49
|
+
Automatic installation supports macOS/Linux with zsh, bash or sh. It uses the active npm global
|
|
50
|
+
prefix or a writable personal prefix; when needed, it adds a bounded PATH block to your shell
|
|
51
|
+
startup files while preserving unrelated content. An unfamiliar shell receives an explicit manual
|
|
52
|
+
PATH step. Windows automatic installation is not qualified: use the reported manual
|
|
53
|
+
`npm install --global balladeer@latest` instruction and verify the executable separately. Setup can
|
|
54
|
+
continue with a truthful partial installation state; that does not mean the CLI is ready on PATH.
|
|
29
55
|
|
|
30
56
|
Run it inside the repository you want to protect. It prints the boundary explanation below, then one
|
|
31
57
|
step at a time, and it exits within seconds rather than blocking on anything.
|
|
@@ -41,6 +67,32 @@ already enrolled it connects this machine and writes its MCP configuration; when
|
|
|
41
67
|
leaves the checkout unchanged and Balladeer shows that enforcement tracking is unavailable until an
|
|
42
68
|
administrator connects a repository.
|
|
43
69
|
|
|
70
|
+
## Know what is ready
|
|
71
|
+
|
|
72
|
+
The CLI being on PATH, an MCP entry being configured, native tools loading in your coding host, and
|
|
73
|
+
an authenticated repository read succeeding are separate observations. Check the executable in the
|
|
74
|
+
shell you use, then load the host's real tools in a fresh session. A configuration file alone does
|
|
75
|
+
not prove native tool loading or server access.
|
|
76
|
+
|
|
77
|
+
`balladeer session` saves an optional local stamp for retrieval evidence. If only that stamp write
|
|
78
|
+
is refused, authorized MCP reads, coding and explicitly requested capture may continue without a
|
|
79
|
+
session ID. Do not invent an ID or trailer. This does not fix missing credentials, installation or
|
|
80
|
+
authorization failures, and does not guarantee every CLI command works in every sandbox.
|
|
81
|
+
|
|
82
|
+
## If npm cannot start the bootstrap
|
|
83
|
+
|
|
84
|
+
An EPERM or EACCES error naming npm's cache happens before Balladeer starts. Use an allowed writable
|
|
85
|
+
cache for the same command, or request normal host permission. If `/tmp` is allowed, for example:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
balladeer_npm_cache=$(mktemp -d /tmp/balladeer-npm-cache.XXXXXX)
|
|
89
|
+
npm_config_cache="$balladeer_npm_cache" npx -y balladeer@latest setup
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Do not infer root ownership from a permission error, use `sudo`, change ownership or delete saved
|
|
93
|
+
credentials. A writable npm cache grants neither network access nor access to the installation
|
|
94
|
+
prefix or credential folder. Follow the specific refusal; preserve a denied permission request.
|
|
95
|
+
|
|
44
96
|
## If setup cannot save its local connection
|
|
45
97
|
|
|
46
98
|
Setup needs access to its private credential folder, normally `~/.config/balladeer`. If your
|
package/dist/cli.js
CHANGED
|
@@ -16,11 +16,17 @@ import { runSession } from "./commands/session.js";
|
|
|
16
16
|
import { runStatus } from "./commands/status.js";
|
|
17
17
|
import { runTouchMap } from "./commands/touch-map.js";
|
|
18
18
|
import { runWhoami } from "./commands/whoami.js";
|
|
19
|
+
import { runInstall } from "./install.js";
|
|
19
20
|
import { updateNotice } from "./currency.js";
|
|
20
21
|
import { StoreError, normalizeControlPlane } from "./store.js";
|
|
21
22
|
import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
22
23
|
const USAGE = `balladeer ${CLI_VERSION}
|
|
23
24
|
|
|
25
|
+
${CLI_INVOCATION} install [--json]
|
|
26
|
+
Install this exact release as the durable balladeer command, without pairing
|
|
27
|
+
or changing any workspace, repository, or CI configuration. Use the latest
|
|
28
|
+
npx bootstrap to upgrade an older global command.
|
|
29
|
+
|
|
24
30
|
${CLI_INVOCATION} setup [--repository owner/name]... [--json] [--wait] [--control-plane <url>]
|
|
25
31
|
[--create-workspace <name>] [--choose-workspace] [--refresh] [--force]
|
|
26
32
|
[--client codex|claude]
|
|
@@ -46,10 +52,11 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
46
52
|
--existing is for an invited teammate and never enrolls repositories or changes CI.
|
|
47
53
|
If this repository is already connected, connect this machine; otherwise
|
|
48
54
|
continue in the browser while an administrator connects it.
|
|
49
|
-
--refresh
|
|
55
|
+
--refresh repairs the durable command if needed, then rewrites this
|
|
50
56
|
selected client's project MCP entry and Balladeer instructions block
|
|
51
57
|
(and Claude desktop entry for Claude) to the current form, leaves every
|
|
52
|
-
other entry in those files alone, and prints what it changed.
|
|
58
|
+
other entry in those files alone, and prints what it changed. Installing
|
|
59
|
+
the command may contact npm; refresh never contacts Balladeer. Run it when
|
|
53
60
|
Balladeer says a newer version is available.
|
|
54
61
|
--force sets up even though an earlier Balladeer is still installed on
|
|
55
62
|
this machine. Without it, a run that finds the older client's machine-wide
|
|
@@ -379,8 +386,19 @@ async function dispatch(parsed, write) {
|
|
|
379
386
|
switch (parsed.command) {
|
|
380
387
|
case "explain":
|
|
381
388
|
return runExplain(write, parsed.controlPlane);
|
|
389
|
+
case "install":
|
|
390
|
+
return runInstall({ environment: process.env, json: parsed.json, write });
|
|
382
391
|
case "setup":
|
|
383
392
|
return runSetup({
|
|
393
|
+
installCommand: () => {
|
|
394
|
+
const result = runInstall({
|
|
395
|
+
environment: process.env,
|
|
396
|
+
json: parsed.json,
|
|
397
|
+
write,
|
|
398
|
+
allowPartial: true,
|
|
399
|
+
});
|
|
400
|
+
return process.platform === "win32" ? 0 : result;
|
|
401
|
+
},
|
|
384
402
|
controlPlane: parsed.controlPlane,
|
|
385
403
|
json: parsed.json,
|
|
386
404
|
wait: parsed.wait,
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -24,9 +24,9 @@ export type SetupOptions = Readonly<{
|
|
|
24
24
|
existingOnly?: boolean;
|
|
25
25
|
/**
|
|
26
26
|
* Repair the two files a previous setup wrote in this repository, and do
|
|
27
|
-
* nothing else. No pairing,
|
|
28
|
-
* stale install is repaired
|
|
29
|
-
*
|
|
27
|
+
* nothing else in the workspace. No pairing, enrollment, or credential: a
|
|
28
|
+
* stale install is repaired without the control plane. The real CLI also
|
|
29
|
+
* ensures its durable command is installed and may contact npm.
|
|
30
30
|
*/
|
|
31
31
|
refresh?: boolean;
|
|
32
32
|
/**
|
|
@@ -55,6 +55,8 @@ export type SetupOptions = Readonly<{
|
|
|
55
55
|
environment: NodeJS.ProcessEnv;
|
|
56
56
|
cwd: string;
|
|
57
57
|
write: (text: string) => void;
|
|
58
|
+
/** Real CLI installer; tests of pairing can inject a no-write observer. */
|
|
59
|
+
installCommand?: () => number;
|
|
58
60
|
sleep?: (ms: number) => Promise<void>;
|
|
59
61
|
now?: () => Date;
|
|
60
62
|
}>;
|
package/dist/commands/setup.js
CHANGED
|
@@ -291,6 +291,8 @@ export async function runSetup(options) {
|
|
|
291
291
|
catch (error) {
|
|
292
292
|
return storeFailure(options, error);
|
|
293
293
|
}
|
|
294
|
+
if (options.installCommand && options.installCommand() !== 0)
|
|
295
|
+
return 4;
|
|
294
296
|
// A completed create intent resumes only its own authenticated session. The
|
|
295
297
|
// name is local intent, never a lookup or grant of workspace authority. An
|
|
296
298
|
// explicit chooser always starts a switch. Keep the old session until the
|
|
@@ -1030,9 +1032,7 @@ function reportAgentHost(options, configured, published) {
|
|
|
1030
1032
|
say(options, line);
|
|
1031
1033
|
for (const line of hostNextSteps(options))
|
|
1032
1034
|
say(options, line);
|
|
1033
|
-
say(options,
|
|
1034
|
-
? ` Setup does not install a global balladeer executable. This checkout's CLI runs as \`${commandLine(null, "<command>")}\` after it is built.`
|
|
1035
|
-
: ` Setup does not install a global balladeer executable. Run the CLI as \`${commandLine(published, "<command>")}\`; ${hostConfigFile(options)} invokes that same package automatically.`);
|
|
1035
|
+
say(options, " Use `balladeer <command>` for everyday CLI calls. If installation or PATH still needs attention, use `npx -y balladeer@latest <command>` until it is repaired. The MCP launcher remains independently configured.");
|
|
1036
1036
|
if (configured.files.length > 0) {
|
|
1037
1037
|
say(options, ` ${configured.files.join(" and ")} ${configured.files.length === 1 ? "is an uncommitted change" : "are uncommitted changes"} in your working tree. Commit ${configured.files.length === 1 ? "it" : "them"} when you are ready; they carry no secret.`);
|
|
1038
1038
|
}
|
|
@@ -1671,6 +1671,8 @@ async function runRefresh(input) {
|
|
|
1671
1671
|
if (repositoryId === undefined) {
|
|
1672
1672
|
return failRefresh(options, "not_connected", `Nothing here names a repository to refresh: this machine holds no Balladeer credential for it and ${hostConfigFile(options)} has no balladeer entry. Run \`${commandLine(null, "setup")}\` to connect it.`);
|
|
1673
1673
|
}
|
|
1674
|
+
if (options.installCommand && options.installCommand() !== 0)
|
|
1675
|
+
return 4;
|
|
1674
1676
|
const merged = mergeHostConfig(options, root, repositoryId, refreshPublishedForm(existing));
|
|
1675
1677
|
if (merged.kind === "refused") {
|
|
1676
1678
|
say(options, merged.reason);
|
package/dist/commands/status.js
CHANGED
|
@@ -48,7 +48,7 @@ const POSTURE_REASONS = {
|
|
|
48
48
|
/** What a posture this copy does not recognise says, rather than a green word. */
|
|
49
49
|
const UNREPORTED_POSTURE = "Balladeer is not reporting this behavior as holding, and this copy of the command does not recognise the state it is in.";
|
|
50
50
|
function noSessionSentence(controlPlane) {
|
|
51
|
-
return `Every repository in the workspace is read over a setup session, and none is stored for ${controlPlane}. Run \`${commandLine(null, "setup")}\` to
|
|
51
|
+
return `Every repository in the workspace is read over a setup session, and none is stored for ${controlPlane}. Run \`${commandLine(null, "setup --existing --choose-workspace")}\` to choose the existing workspace and read its optional workspace view.`;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* What Balladeer looks like right now, read from the server and never from
|
|
@@ -476,7 +476,7 @@ async function reportWorkspace(options, session, here, emit, say, alreadyReporte
|
|
|
476
476
|
// This repository has already been reported over its own connection, so
|
|
477
477
|
// the workspace-wide half is a view this run could not read rather than a
|
|
478
478
|
// command that failed.
|
|
479
|
-
sayWorkspaceUnavailable(emit, say, code, `Every repository in the workspace is read over a setup session, and the stored one could not be used (${code}). Run \`${commandLine(null, "setup")}\` to
|
|
479
|
+
sayWorkspaceUnavailable(emit, say, code, `Every repository in the workspace is read over a setup session, and the stored one could not be used (${code}). Run \`${commandLine(null, "setup --existing --choose-workspace")}\` to choose the existing workspace and read its optional workspace view.`);
|
|
480
480
|
return 0;
|
|
481
481
|
}
|
|
482
482
|
if (error instanceof ClientTooOldError) {
|
package/dist/conventions.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare const CONVENTIONS_END = "<!-- balladeer:conventions:end -->";
|
|
|
27
27
|
* instructions marker instead. A bump here with no change to the block would
|
|
28
28
|
* rewrite twelve repositories to say exactly what they already said.
|
|
29
29
|
*/
|
|
30
|
-
export declare const CONVENTIONS_VERSION =
|
|
30
|
+
export declare const CONVENTIONS_VERSION = 18;
|
|
31
31
|
export declare function managedByLine(version: number): string;
|
|
32
32
|
/**
|
|
33
33
|
* Which version of the block a file already carries, or nothing when it carries
|
package/dist/conventions.js
CHANGED
|
@@ -31,7 +31,7 @@ export const CONVENTIONS_END = "<!-- balladeer:conventions:end -->";
|
|
|
31
31
|
* instructions marker instead. A bump here with no change to the block would
|
|
32
32
|
* rewrite twelve repositories to say exactly what they already said.
|
|
33
33
|
*/
|
|
34
|
-
export const CONVENTIONS_VERSION =
|
|
34
|
+
export const CONVENTIONS_VERSION = 18;
|
|
35
35
|
/** Both spellings: the stable marker, and the versioned one version 1 wrote. */
|
|
36
36
|
const START_MARKER = /<!-- balladeer:conventions:start(?: v(\d{1,4}))? -->/g;
|
|
37
37
|
const END_MARKER = /<!-- balladeer:conventions:end -->/g;
|
package/dist/copy.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export declare const RETRIEVAL_SCOPE = "### Reading what is already agreed\n\nRe
|
|
|
69
69
|
* plane's `/agent` page carries the same bytes under its own constant, and a
|
|
70
70
|
* packaging test compares them.
|
|
71
71
|
*/
|
|
72
|
-
export declare const SESSION_BEHAVIORS = "### Three things to offer without being asked\n\nAt the start of any session in this repository, before you plan anything, ask list_promises for the\npromises nothing is checking yet that belong to the person you are working with: `unverified` true\nand `mine` true, which is one bounded read rather than the catalog. If it returns any, say so in one\nline and offer to build their verifiers now. Name each one by its one-sentence claim rather than by\nan id, so the person can see which behavior is unguarded. Ask the same tool for\n`brokenSinceLastSeen` true as well, and where that returns any, say in one line that those promises\nbroke since they last looked, name each by its claim, and offer to fix them. Then wait for their\nanswer. The offer is the whole of it, and never start building or repairing one because nobody said\nno.\n\nTheirs, and nobody else's. `mine` keeps the promises this person owns or agreed to, and a teammate's\nunguarded promise is that teammate's to hear about: a session that opens by reading out other\npeople's unfinished work reads as an audit of them. `brokenSinceLastSeen` is that person's own by\nconstruction and needs no `mine` beside it. Drop `mine` when this person asks what the rest of the\nteam has promised, and say whose promises you are showing them when you do.\n\nWhen you have proposed promises, show them here as well as there. Put each proposal in the\nconversation in full: its one-sentence claim, who it is for, when it applies and what must then be\ntrue, the numbered cases that must keep working and the ones that must be caught, and every question\nyou left open. Then offer to finish the agreement from here, by asking for a sign-off, giving them\nthe page it returns and taking the one-time code that page shows them, so the browser is needed only\nfor signing. Give them the review link in the same message too, because some people would rather\nread it there and edit it before they agree.\nA promise whose meaning is agreed and which nothing is checking yet is one you can finish. When\nsomebody gives you a promise id, run `
|
|
72
|
+
export declare const SESSION_BEHAVIORS = "### Three things to offer without being asked\n\nAt the start of any session in this repository, before you plan anything, ask list_promises for the\npromises nothing is checking yet that belong to the person you are working with: `unverified` true\nand `mine` true, which is one bounded read rather than the catalog. If it returns any, say so in one\nline and offer to build their verifiers now. Name each one by its one-sentence claim rather than by\nan id, so the person can see which behavior is unguarded. Ask the same tool for\n`brokenSinceLastSeen` true as well, and where that returns any, say in one line that those promises\nbroke since they last looked, name each by its claim, and offer to fix them. Then wait for their\nanswer. The offer is the whole of it, and never start building or repairing one because nobody said\nno.\n\nTheirs, and nobody else's. `mine` keeps the promises this person owns or agreed to, and a teammate's\nunguarded promise is that teammate's to hear about: a session that opens by reading out other\npeople's unfinished work reads as an audit of them. `brokenSinceLastSeen` is that person's own by\nconstruction and needs no `mine` beside it. Drop `mine` when this person asks what the rest of the\nteam has promised, and say whose promises you are showing them when you do.\n\nWhen you have proposed promises, show them here as well as there. Put each proposal in the\nconversation in full: its one-sentence claim, who it is for, when it applies and what must then be\ntrue, the numbered cases that must keep working and the ones that must be caught, and every question\nyou left open. Then offer to finish the agreement from here, by asking for a sign-off, giving them\nthe page it returns and taking the one-time code that page shows them, so the browser is needed only\nfor signing. Give them the review link in the same message too, because some people would rather\nread it there and edit it before they agree.\nA promise whose meaning is agreed and which nothing is checking yet is one you can finish. When\nsomebody gives you a promise id, run `balladeer status <promise id>`, or expand it with get_promise.\nIf it comes back agreed with nothing checking it, say so in one line and offer to prepare and build\nits verifier now. Only if they say yes: `prepare_qualification` mints the one-time setup, or\n`balladeer prepare <promise id>` mints it and writes it where the sealed run reads it. You need no\nsign-off for that and there is no code to ask anybody for, because agreeing the meaning was their\nact and building the check that proves it is yours. Then build the verifier, seal it, push, and tell\nthem protection starts by itself when that run qualifies. The offer is the whole of it: never\nprepare one because nobody said no.";
|
|
73
73
|
/**
|
|
74
74
|
* The capture rules that do not change: when to say nothing, what a yes is
|
|
75
75
|
* worth, and the two promises Balladeer refuses.
|
|
@@ -150,7 +150,7 @@ export declare const FETCH_BUDGET = "Fetch by id, and only for an id the person
|
|
|
150
150
|
* tool guidance and the control plane's `/agent` page carry the same bytes
|
|
151
151
|
* under their own constants, and a packaging test compares all three.
|
|
152
152
|
*/
|
|
153
|
-
export declare const SEALED_FILES = "### Files that are sealed, and the one reason to edit one\n\nEvery file under .continuity/promises/ is sealed. The promise that owns that directory records the\nexact bytes of each file in it, so editing one, adding one there, renaming one or deleting one\nbreaks the seal. A promise whose seal is broken stops being checked, and it stays that way until a\nnamed person qualifies it again, which is their afternoon rather than your commit. Nothing in there\nis ordinary source: keep it out of refactors, formatting runs and dependency upgrades.\n\nFind out which directories are sealed before you plan an edit, not after.
|
|
153
|
+
export declare const SEALED_FILES = "### Files that are sealed, and the one reason to edit one\n\nEvery file under .continuity/promises/ is sealed. The promise that owns that directory records the\nexact bytes of each file in it, so editing one, adding one there, renaming one or deleting one\nbreaks the seal. A promise whose seal is broken stops being checked, and it stays that way until a\nnamed person qualifies it again, which is their afternoon rather than your commit. Nothing in there\nis ordinary source: keep it out of refactors, formatting runs and dependency upgrades.\n\nFind out which directories are sealed before you plan an edit, not after. balladeer status lists\nthem, and list_promises names a promise's sealed directory on its row once a verifier is bound to\nit. Then, before you push, run balladeer check-seals. It prints nothing and exits zero when your\nchange touches no seal, and names the promise, its owner and its page when your change would break\none.\n\nThe one reason to edit a sealed file is to repair a verifier that can no longer run: something it\nimports moved, or the language it is written in changed under it. Never edit one to make a failing\ncheck pass. A check going red is the promise doing its job, and the repair for that belongs in the\nbehavior it protects. A repair is not finished until the promise is sealed again with the runner\nthis repository is pinned to. get_promise_setup carries that exact seal command, and\nballadeer check-seals prints it beside any promise it names.";
|
|
154
154
|
/**
|
|
155
155
|
* Which promises a change touches, measured rather than guessed.
|
|
156
156
|
*
|
|
@@ -167,7 +167,7 @@ export declare const SEALED_FILES = "### Files that are sealed, and the one reas
|
|
|
167
167
|
* Three carriers say it, byte for byte, and a packaging test holds them
|
|
168
168
|
* together.
|
|
169
169
|
*/
|
|
170
|
-
export declare const TOUCH_MAP_GUIDANCE = "Prefer the measurement over the markers. Where this repository has a touch map, `
|
|
170
|
+
export declare const TOUCH_MAP_GUIDANCE = "Prefer the measurement over the markers. Where this repository has a touch map, `balladeer affected\n<paths...>` answers which promises ran the files in front of you, out of what each verifier actually\nexecuted the last time `balladeer touch-map` measured it. Both commands run on this machine and\nsend Balladeer nothing, so you may name any path in the change.\n\nAn answer marked stale was measured against a verifier that has changed since, so read it as the\nlast thing anybody measured rather than as fact, and offer to run `balladeer touch-map` again.\nWhere there is no map, where it does not name your paths, or where it reports a promise it could not\nmeasure, fall back to the paths on the index rows. The map narrows which promises are worth\nfetching. It never widens what you may read: the catalog is still never enumerated, and the fetch\nbudget is unchanged.\n\nWhen a retrieval receipt asks why you read a row, the answer for one the map named is `touch`.";
|
|
171
171
|
/**
|
|
172
172
|
* The one paragraph that turns retrieval from a thing we argue about into a
|
|
173
173
|
* thing we measure.
|
|
@@ -186,14 +186,14 @@ export declare const TOUCH_MAP_GUIDANCE = "Prefer the measurement over the marke
|
|
|
186
186
|
* Three carriers say it, byte for byte, and a packaging test holds them
|
|
187
187
|
* together.
|
|
188
188
|
*/
|
|
189
|
-
export declare const SESSION_STAMP = "
|
|
189
|
+
export declare const SESSION_STAMP = "Use the installed `balladeer` command after setup reports CLI readiness. If it is missing from\nPATH (including an older 1.0.4 setup), use `npx -y balladeer@latest <command>` for that invocation\nand read the current /agent instructions for installation or repair. Do not claim that an older\nrepository's instructions were refreshed. CLI availability does not prove that this coding host\nloaded the native MCP tools or that a credential is authorized. The optional session stamp below\nis separate from all three.\n\nRun `balladeer session` when you start work here and pass the id it prints to every promise read\nyou make, as `session`. It is the same id for the whole of one piece of work, and it costs nothing:\na read without it is answered exactly the same way.\n\nIf local file permissions refuse that session write, no session stamp was saved. Your saved\nconnection is unchanged; this does not prove its current server authorization. Continue\nalready-authorized MCP reads, coding and explicitly requested capture without the optional\n`session` field. Do not invent an ID, add a session trailer, or record an unsaved stamp. Do not\nbroaden filesystem access or move credentials to retry this write. A missing credential or server\nauthorization refusal is different: follow that refusal. Capture still needs the person's request\nor accepted offer, and human meaning approval is unchanged.\n\nIf a session ID was saved, near the end of the work call `mark_promise_used` with the ids you actually cited\nor acted on. Naming none is a fine answer. It is the only way anybody finds out which rows were\nworth sending you, because reading a promise and then ignoring it is not the same as using it.\n\nIf a session ID was saved, write `Balladeer-Session: <the id>` into the commit message or pull-request body you produce,\nand run `balladeer session --record` once the commit exists. That is what lets a check that goes red\nnext week be read back against what Balladeer told you before you started. Your commit message never\nleaves your machine: only the session id and the commit SHA are sent.";
|
|
190
190
|
/**
|
|
191
191
|
* The conventions block written between markers into this repository's agent
|
|
192
192
|
* instructions file. Fixed text, compared byte for byte by a test, because a
|
|
193
193
|
* block that drifts per run would rewrite a customer's committed file on every
|
|
194
194
|
* setup and the diff would say nothing.
|
|
195
195
|
*/
|
|
196
|
-
export declare const CONVENTIONS_BLOCK = "## Balladeer promises\n\nBefore planning work in this repository, read the promises this team has already approved through\nthe Balladeer MCP server. They are the behaviors a named person has agreed the software keeps, so\nyour plan has to hold them, not just read them.\n\n### Reading what is already agreed\n\nRetrieve by promise, and only when this session has a reason to. One repository here can hold a\nthousand agreed promises, and a plan built from whatever survived a truncated catalog read is worse\nthan a plan built from none of it, because nothing tells you which half went missing.\n\nWhen a person gives you a promise id, expand exactly that one with get_promise and stop there. Every\npromise page carries a control that copies its id, so an id is what a person hands you when they\nmean a particular promise. Ask for one rather than searching for what they meant.\n\nConsult list_promises in two situations and no others. The person asks what this repository has\npromised, in which case page the index they asked for. Or the change you are about to make touches\npaths that carry promises, in which case give those paths to list_promises: it answers with the\npromises whose scope overlaps them, closest first, and with the few that name no path and so cover\nthe whole repository. That answer is a selection rather than a page and does not continue with a\ncursor, so when the total beside it is larger than what you were handed, narrow the paths rather\nthan asking for more.\n\nWhen you do not yet know which paths you are about to touch, do not call the index at all. Work from\nids until you do, because a page you did not ask a question of is not about your change, and reading\none as though it were is how a plan quietly misses the promise it breaks.\n\nTo learn which paths those are, read `paths` on a list_promises answer you asked for\nwithout paths of your own. It is the set of\nrepository paths this repository's promises are scoped to, deduplicated and bounded, with\n`pathsTruncated` saying whether there were more than the answer carries. Compare the files you are\nabout to change against it. Nothing matching means there is nothing here to read, and saying so is a\nbetter answer than a page of promises about somewhere else.\n\nNever call get_promise_context at the start of a session. It answers the markers you give it, and\nbefore you know what you are changing there are no markers to give: what comes back is a slice of\nthe catalog chosen by nothing. Call it once the work is in front of you, with that work's markers.\n\nEvery one of these reads is bounded and none of them returns the whole catalog. Read the total\nbeside the rows and the sentence in `scope` that says what the total is a total of, and when the\ntotal is larger than what you were handed, page or narrow rather than planning as though you had\nseen everything.\n\nEach index row carries what it takes to rule that promise out without fetching it: the repository,\nthe paths it covers, its one-sentence claim, what protects it and why, and when anything last\nchecked it.\n\nPrefer the measurement over the markers. Where this repository has a touch map, `npx -y balladeer@latest affected\n<paths...>` answers which promises ran the files in front of you, out of what each verifier actually\nexecuted the last time `npx -y balladeer@latest touch-map` measured it. Both commands run on this machine and\nsend Balladeer nothing, so you may name any path in the change.\n\nAn answer marked stale was measured against a verifier that has changed since, so read it as the\nlast thing anybody measured rather than as fact, and offer to run `npx -y balladeer@latest touch-map` again.\nWhere there is no map, where it does not name your paths, or where it reports a promise it could not\nmeasure, fall back to the paths on the index rows. The map narrows which promises are worth\nfetching. It never widens what you may read: the catalog is still never enumerated, and the fetch\nbudget is unchanged.\n\nWhen a retrieval receipt asks why you read a row, the answer for one the map named is `touch`.\n\nFetch by id, and only for an id the person gave you or an index row whose paths match the change in\nfront of you. Never enumerate the catalog. Never chain one fetch into the next to see the whole of\nsomething: when the rows do not settle it, narrow the filter rather than expanding another promise.\n\n### When to say nothing, and what a yes is worth\n\nMost rules are said in passing, in the middle of something else. Somebody saying one has not asked\nyou to record anything, so propose nothing and start no interview. When you do ask, it is one line\nappended to the end of the reply you were already going to give, never a message of its own, never\nasked twice about the same rule, and nothing is proposed until they say yes. A no ends it, and that\nrule is not raised again for the rest of the conversation.\n\nSilence is per conversation rather than per message. Once a conversation is one of these, you ask\nnothing for the rest of it, however good the rule sounds:\n\n- A question, or working out how something already behaves. Nothing is filed and nothing is offered.\n- A refactor. Nobody predicts a promise from a rewrite. Offer the promises this area already carries\n that nothing is checking yet, once, and then wait. Ask nothing about new ones.\n- A change to wording alone. Wording somebody may change again tomorrow is not a rule.\n- An incident, while it is still being fixed. Ask nothing at all until the fix is merged or they say\n it is done, however good tonight's failing case would be.\n- Somebody still weighing options. A decision nobody has made yet is not a rule.\n- An exploration or spike. It ends in nothing or in a plan, and its sentences sound like rules and\n are not.\n- Reading somebody else's change, while you are still reading it. Nothing is offered until they\n give a verdict.\n\nThe words to ask in, the moment to offer, and the shape a proposal takes are not in this file. The\nBalladeer server sends them at the start of every session, and its copy is the current one: read\nwhat it sent this session rather than what this file remembers.\n\n### Two promises Balladeer cannot keep\n\nA promise about speed needs three things before it is a promise at all: a number, a percentile, and\nwhere it is measured. \"The quote page answers in under one second at p95, measured in production at\npeak load\" is one Balladeer can keep. \"The quote page has to be fast\" is not. Say this, and ask for\nthe part that is missing rather than filing it:\n\n\"I can keep that once it has a number, a percentile, and a place it is measured. Without those it is\na wish, not a promise.\"\n\nBalladeer refuses one without all three and names which of them is missing. When all three are\nthere, write the measurement method into the promise, and tell them plainly that it reads agreed and\nunprotected until a test that actually measures it exists.\n\nA promise about how the team works is not something the software does, so no check can ever catch\nit. \"We must provide a low-friction capture experience\" is one of these. File nothing and say:\n\n\"That is a promise about how we work, not something the software does that a check can fail.\nBalladeer only keeps promises a check can catch. If a customer would notice something when this\nslips, say that and I will keep that instead.\"\n\nThen take the customer-visible half if they give you one, and file that instead.\n\nRun `npx -y balladeer@latest session` when you start work here and pass the id it prints to every promise read\nyou make, as `session`. It is the same id for the whole of one piece of work, and it costs nothing:\na read without it is answered exactly the same way.\n\nIf local file permissions refuse that session write, no session stamp was saved. Your saved\nconnection is unchanged; this does not prove its current server authorization. Continue\nalready-authorized MCP reads, coding and explicitly requested capture without the optional\n`session` field. Do not invent an ID, add a session trailer, or record an unsaved stamp. Do not\nbroaden filesystem access or move credentials to retry this write. A missing credential or server\nauthorization refusal is different: follow that refusal. Capture still needs the person's request\nor accepted offer, and human meaning approval is unchanged.\n\nIf a session ID was saved, near the end of the work call `mark_promise_used` with the ids you actually cited\nor acted on. Naming none is a fine answer. It is the only way anybody finds out which rows were\nworth sending you, because reading a promise and then ignoring it is not the same as using it.\n\nIf a session ID was saved, write `Balladeer-Session: <the id>` into the commit message or pull-request body you produce,\nand run `npx -y balladeer@latest session --record` once the commit exists. That is what lets a check that goes red\nnext week be read back against what Balladeer told you before you started. Your commit message never\nleaves your machine: only the session id and the commit SHA are sent.\n\n### When somebody asks you to protect a behavior\n\nSomebody has asked you to protect a behavior when they say what the software must do, or must never\ndo again, and mean it as a rule rather than as this one bug. That is one promise, for the behavior\nthey named, and nothing else: if you notice others worth protecting, say so in a sentence and let\nthem choose, and file none of them. Never propose from a conversation that did not ask you to. A\nquestion about how something works and a plan you were asked to sketch are not requests to record\nanything, nor is a fix nobody asked you to write a rule about.\n\nAsk before you extrapolate. Ask only what you cannot work out for yourself, ask it all in one\nmessage, and stop at four. Four is a ceiling, not a target: two good ones are better. Then write the\nproposal with what you have and put whatever is still open in its open questions rather than going\nback. Never ask what this repository would answer, such as which file, which test, or which branch,\nand never ask anyone for Balladeer's own identifiers: get_promise_setup carries this repository's id\nand who can own a promise. Never ask again for what they have already told you.\n\nWrite it in their words. Every failure they named out loud is one of the failing examples, in the\nwords they named it. Every other example comes from a situation they actually described; if you\ncannot trace one to something they said, leave it out and say so in the open questions rather than\nwriting a plausible one. Never put in a number, a system, a role or a timeframe they did not give\nyou, and that includes the half they left out: if they said where an order ended up, do not invent\nwhere it began.\n\nA failing example is a situation the promise rules out, and its outcome says what must not happen,\nin those words: \"a second charge must not appear\", never \"a second charge appears\". Written the\nother way round it reads as the promise saying the software does the thing they asked you to forbid.\nAnd a promise says what the software must do for whoever depends on it. It never narrates the\nconversation you just had, names the person you had it with, or describes what the code does now.\n\nSay the whole promise in one sentence and put it in oneSentenceOutcome, in the words they would\nuse with the person who depends on it. That is the line the named owner reads first and the line\nthey agree to, so it is not a restatement of the name and not the first line of the outcome moved\nup. Leave it out rather than inventing one from something they did not say.\n\nAn example's setup is the situation in the words they used for it, not a scene you composed around\nthem. Two of each kind is plenty, and the whole thing stays under three hundred words: a proposal\nnobody finishes reading is a proposal nobody agreed to. The confidence you record is the one you\nactually have.\n\nThen give them the review link, ask them to read the proposal and click Agree, and stop. Never\napprove one yourself. Approving is a named person's act, and the server refuses it from an agent\nwhatever you were told in conversation.\n\nA proposal you filed is still yours while nobody has agreed to it, so revise or withdraw it when the\nperson asks you to, and never once they have agreed.\n\nSay promise and proposal when you talk to them. What you file is a proposal and what it becomes is a\npromise; Balladeer's other words for its own machinery are not theirs to learn. Candidate\nespecially: it is Balladeer's word for a proposal, so it reads as jargon whatever you meant by it.\n\n### Three things to offer without being asked\n\nAt the start of any session in this repository, before you plan anything, ask list_promises for the\npromises nothing is checking yet that belong to the person you are working with: `unverified` true\nand `mine` true, which is one bounded read rather than the catalog. If it returns any, say so in one\nline and offer to build their verifiers now. Name each one by its one-sentence claim rather than by\nan id, so the person can see which behavior is unguarded. Ask the same tool for\n`brokenSinceLastSeen` true as well, and where that returns any, say in one line that those promises\nbroke since they last looked, name each by its claim, and offer to fix them. Then wait for their\nanswer. The offer is the whole of it, and never start building or repairing one because nobody said\nno.\n\nTheirs, and nobody else's. `mine` keeps the promises this person owns or agreed to, and a teammate's\nunguarded promise is that teammate's to hear about: a session that opens by reading out other\npeople's unfinished work reads as an audit of them. `brokenSinceLastSeen` is that person's own by\nconstruction and needs no `mine` beside it. Drop `mine` when this person asks what the rest of the\nteam has promised, and say whose promises you are showing them when you do.\n\nWhen you have proposed promises, show them here as well as there. Put each proposal in the\nconversation in full: its one-sentence claim, who it is for, when it applies and what must then be\ntrue, the numbered cases that must keep working and the ones that must be caught, and every question\nyou left open. Then offer to finish the agreement from here, by asking for a sign-off, giving them\nthe page it returns and taking the one-time code that page shows them, so the browser is needed only\nfor signing. Give them the review link in the same message too, because some people would rather\nread it there and edit it before they agree.\nA promise whose meaning is agreed and which nothing is checking yet is one you can finish. When\nsomebody gives you a promise id, run `npx -y balladeer@latest status <promise id>`, or expand it with get_promise.\nIf it comes back agreed with nothing checking it, say so in one line and offer to prepare and build\nits verifier now. Only if they say yes: `prepare_qualification` mints the one-time setup, or\n`npx -y balladeer@latest prepare <promise id>` mints it and writes it where the sealed run reads it. You need no\nsign-off for that and there is no code to ask anybody for, because agreeing the meaning was their\nact and building the check that proves it is yours. Then build the verifier, seal it, push, and tell\nthem protection starts by itself when that run qualifies. The offer is the whole of it: never\nprepare one because nobody said no.\n\n### When somebody says \"tell me about\" one\n\nAn id is how a person points at something here, and every promise page and every proposal page\ncarries one. A promise id starts with prom_ and a proposal id starts with cand_: expand a promise\nwith get_promise and a proposal with get_proposal, and read neither of them out as a list of fields.\nSay in four or five sentences what it is for, who it is for, when it applies and what must then be\ntrue. Then say where it stands: a promise is agreed, and either protected or not yet checked by\nanything; a proposal is agreed by nobody and waiting on the person it names.\n\nClarify ambiguity that materially changes the behavior during capture, within the existing\nquestion budget; never invent an answer.\n\nOrdinary open questions record uncertainty; they do not prevent the named owner from agreeing to\nthe behavior as written. Agreement does not answer them or add an unstated guarantee. Separate\ncatalog-conflict questions can hold agreement until the owner rules on the stated conflict. Do not\ncall a proposal unready just because it has ordinary open questions. Offer to discuss them if\nuseful; if the person wants to, take them one at a time in the order they come back. For each ordinary question,\nsay what it decides in their words rather than in the question's; give your\nrecommendation and the reason you hold it, drawn from this repository and from the promise itself; and stop there. When\nthey answer, record what they said with resolve_question, in their own words where they gave you\nany, and where their answer changes the promise, follow it with update_proposal, add_case or\nremove_case and tell them what you changed. Never invent an answer or clear uncertainty because\nthey chose to agree. None of that agrees to anything: the named owner agrees, in their own browser\nor through a sign-off you carry, and the questions they answered stay on the proposal in their name.\n\nThe same rule governs every question you leave open in the first place. A question that names a gap\nand stops is a note, and nobody can answer a note. Say what answering it decides, in the words a\ncustomer would use, and carry your own best guess with the reason behind it, so that the shortest\ntrue answer is yes. For example: \"Decides: whether a worker that is running but reconciling nothing\ncounts as an outage this promise covers. Best guess: yes, because the promise is about somebody\nhearing before a customer does, and a wedged worker is invisible to every check this repository has.\nSay yes, or tell me otherwise.\" Balladeer refuses a question filed without both halves and says\nwhich one is missing.\n\n### Files that are sealed, and the one reason to edit one\n\nEvery file under .continuity/promises/ is sealed. The promise that owns that directory records the\nexact bytes of each file in it, so editing one, adding one there, renaming one or deleting one\nbreaks the seal. A promise whose seal is broken stops being checked, and it stays that way until a\nnamed person qualifies it again, which is their afternoon rather than your commit. Nothing in there\nis ordinary source: keep it out of refactors, formatting runs and dependency upgrades.\n\nFind out which directories are sealed before you plan an edit, not after. npx -y balladeer@latest status lists\nthem, and list_promises names a promise's sealed directory on its row once a verifier is bound to\nit. Then, before you push, run npx -y balladeer@latest check-seals. It prints nothing and exits zero when your\nchange touches no seal, and names the promise, its owner and its page when your change would break\none.\n\nThe one reason to edit a sealed file is to repair a verifier that can no longer run: something it\nimports moved, or the language it is written in changed under it. Never edit one to make a failing\ncheck pass. A check going red is the promise doing its job, and the repair for that belongs in the\nbehavior it protects. A repair is not finished until the promise is sealed again with the runner\nthis repository is pinned to. get_promise_setup carries that exact seal command, and\nnpx -y balladeer@latest check-seals prints it beside any promise it names.\n\n### The rest of a promise's life\nWhen a promise is obsolete, finished, deliberately off for a while, or owned by the wrong person,\npropose the change and hand them the promise page. Deciding is theirs.\n\nYou can finish one of those acts here, and only one way. Ask for a sign-off with\nrequest_owner_signoff, give them the page it returns, and ask for the one-time code that page shows\nthem. Then call the act's own tool with that code and their own words. Never call one on your own\ninitiative, never on a general approval of some earlier act, and never ask for a code you were not\ngiven: a refusal is the person's to resolve, not yours to retry. Balladeer records them as the\nperson who acted and you as the messenger.\n\nReport the promise's state exactly as Balladeer reported it: proposed, agreed, or protected, never\none in place of another.\n\n### Where your team watches this\n\nBalladeer is a web app as well as these tools, at the address setup printed. Its catalog lists every\npromise with who owns it and whether anything is checking it, and each promise has a page of its own\nshowing what this team agreed the software must do and then every run that has checked it since,\nnewest first, with the commit each one checked. Whenever there is a link to give, give the link\nrather than a summary of it: the page says what you would have said, and it stays true after this\nconversation has ended.\n\nBalladeer also has a Slack app, which a workspace administrator installs from workspace settings.\nOnce it is installed, whoever owns a promise gets a direct message when theirs goes live and when a\nrun on the default branch breaks it. Until somebody installs it, nothing is sent anywhere, so say it\nis available rather than saying they will be told.\n\n### Questions people ask\n\nAnswer these when they come up. Where you do not know, say so and point at the address setup\nprinted: a confident wrong answer about what a vendor can see is worse than no answer.\n\nWhat it does: it holds the behaviors this team has agreed the software must keep, and reports\nwhether each one is still being kept, from this repository's own tests running in its own CI.\n\nWhat it sees: the text of each promise somebody approves, this repository's numeric ids and the name\nof its default branch, and from CI the pass or fail outcome, the commit checked, and content hashes.\nNever the code, the tests, the fixtures, the logs, the prompts, or the transcripts.\n\nWhat stopping costs: nothing that matters to their tests. The verifier package, its fixtures and the\nworkflow file are theirs, in their repository, running in their CI, and disconnecting changes none\nof them. An administrator can download everything Balladeer holds at any time from workspace\nsettings, and disconnecting hands them that same download in the response that ends access.\n\nWho can approve one: the named person who owns it, in their own browser. Not an administrator on\ntheir behalf, and never you.\n\nWhether it blocks a merge: no. The check is advisory on Balladeer's side, and their own branch\nprotection is what decides whether a failing check stops anything.\n\nWho can invite people and change setup: a workspace administrator. A contributor can read the\nworkspace and propose promises, and a viewer can read it. If somebody asks you to add a teammate,\ninvite_teammate returns the page an administrator sends the invitation from, with the address filled\nin for them to read. The tool sends nothing itself: an administrator presses Send.";
|
|
196
|
+
export declare const CONVENTIONS_BLOCK = "## Balladeer promises\n\nBefore planning work in this repository, read the promises this team has already approved through\nthe Balladeer MCP server. They are the behaviors a named person has agreed the software keeps, so\nyour plan has to hold them, not just read them.\n\n### Reading what is already agreed\n\nRetrieve by promise, and only when this session has a reason to. One repository here can hold a\nthousand agreed promises, and a plan built from whatever survived a truncated catalog read is worse\nthan a plan built from none of it, because nothing tells you which half went missing.\n\nWhen a person gives you a promise id, expand exactly that one with get_promise and stop there. Every\npromise page carries a control that copies its id, so an id is what a person hands you when they\nmean a particular promise. Ask for one rather than searching for what they meant.\n\nConsult list_promises in two situations and no others. The person asks what this repository has\npromised, in which case page the index they asked for. Or the change you are about to make touches\npaths that carry promises, in which case give those paths to list_promises: it answers with the\npromises whose scope overlaps them, closest first, and with the few that name no path and so cover\nthe whole repository. That answer is a selection rather than a page and does not continue with a\ncursor, so when the total beside it is larger than what you were handed, narrow the paths rather\nthan asking for more.\n\nWhen you do not yet know which paths you are about to touch, do not call the index at all. Work from\nids until you do, because a page you did not ask a question of is not about your change, and reading\none as though it were is how a plan quietly misses the promise it breaks.\n\nTo learn which paths those are, read `paths` on a list_promises answer you asked for\nwithout paths of your own. It is the set of\nrepository paths this repository's promises are scoped to, deduplicated and bounded, with\n`pathsTruncated` saying whether there were more than the answer carries. Compare the files you are\nabout to change against it. Nothing matching means there is nothing here to read, and saying so is a\nbetter answer than a page of promises about somewhere else.\n\nNever call get_promise_context at the start of a session. It answers the markers you give it, and\nbefore you know what you are changing there are no markers to give: what comes back is a slice of\nthe catalog chosen by nothing. Call it once the work is in front of you, with that work's markers.\n\nEvery one of these reads is bounded and none of them returns the whole catalog. Read the total\nbeside the rows and the sentence in `scope` that says what the total is a total of, and when the\ntotal is larger than what you were handed, page or narrow rather than planning as though you had\nseen everything.\n\nEach index row carries what it takes to rule that promise out without fetching it: the repository,\nthe paths it covers, its one-sentence claim, what protects it and why, and when anything last\nchecked it.\n\nPrefer the measurement over the markers. Where this repository has a touch map, `balladeer affected\n<paths...>` answers which promises ran the files in front of you, out of what each verifier actually\nexecuted the last time `balladeer touch-map` measured it. Both commands run on this machine and\nsend Balladeer nothing, so you may name any path in the change.\n\nAn answer marked stale was measured against a verifier that has changed since, so read it as the\nlast thing anybody measured rather than as fact, and offer to run `balladeer touch-map` again.\nWhere there is no map, where it does not name your paths, or where it reports a promise it could not\nmeasure, fall back to the paths on the index rows. The map narrows which promises are worth\nfetching. It never widens what you may read: the catalog is still never enumerated, and the fetch\nbudget is unchanged.\n\nWhen a retrieval receipt asks why you read a row, the answer for one the map named is `touch`.\n\nFetch by id, and only for an id the person gave you or an index row whose paths match the change in\nfront of you. Never enumerate the catalog. Never chain one fetch into the next to see the whole of\nsomething: when the rows do not settle it, narrow the filter rather than expanding another promise.\n\n### When to say nothing, and what a yes is worth\n\nMost rules are said in passing, in the middle of something else. Somebody saying one has not asked\nyou to record anything, so propose nothing and start no interview. When you do ask, it is one line\nappended to the end of the reply you were already going to give, never a message of its own, never\nasked twice about the same rule, and nothing is proposed until they say yes. A no ends it, and that\nrule is not raised again for the rest of the conversation.\n\nSilence is per conversation rather than per message. Once a conversation is one of these, you ask\nnothing for the rest of it, however good the rule sounds:\n\n- A question, or working out how something already behaves. Nothing is filed and nothing is offered.\n- A refactor. Nobody predicts a promise from a rewrite. Offer the promises this area already carries\n that nothing is checking yet, once, and then wait. Ask nothing about new ones.\n- A change to wording alone. Wording somebody may change again tomorrow is not a rule.\n- An incident, while it is still being fixed. Ask nothing at all until the fix is merged or they say\n it is done, however good tonight's failing case would be.\n- Somebody still weighing options. A decision nobody has made yet is not a rule.\n- An exploration or spike. It ends in nothing or in a plan, and its sentences sound like rules and\n are not.\n- Reading somebody else's change, while you are still reading it. Nothing is offered until they\n give a verdict.\n\nThe words to ask in, the moment to offer, and the shape a proposal takes are not in this file. The\nBalladeer server sends them at the start of every session, and its copy is the current one: read\nwhat it sent this session rather than what this file remembers.\n\n### Two promises Balladeer cannot keep\n\nA promise about speed needs three things before it is a promise at all: a number, a percentile, and\nwhere it is measured. \"The quote page answers in under one second at p95, measured in production at\npeak load\" is one Balladeer can keep. \"The quote page has to be fast\" is not. Say this, and ask for\nthe part that is missing rather than filing it:\n\n\"I can keep that once it has a number, a percentile, and a place it is measured. Without those it is\na wish, not a promise.\"\n\nBalladeer refuses one without all three and names which of them is missing. When all three are\nthere, write the measurement method into the promise, and tell them plainly that it reads agreed and\nunprotected until a test that actually measures it exists.\n\nA promise about how the team works is not something the software does, so no check can ever catch\nit. \"We must provide a low-friction capture experience\" is one of these. File nothing and say:\n\n\"That is a promise about how we work, not something the software does that a check can fail.\nBalladeer only keeps promises a check can catch. If a customer would notice something when this\nslips, say that and I will keep that instead.\"\n\nThen take the customer-visible half if they give you one, and file that instead.\n\nUse the installed `balladeer` command after setup reports CLI readiness. If it is missing from\nPATH (including an older 1.0.4 setup), use `npx -y balladeer@latest <command>` for that invocation\nand read the current /agent instructions for installation or repair. Do not claim that an older\nrepository's instructions were refreshed. CLI availability does not prove that this coding host\nloaded the native MCP tools or that a credential is authorized. The optional session stamp below\nis separate from all three.\n\nRun `balladeer session` when you start work here and pass the id it prints to every promise read\nyou make, as `session`. It is the same id for the whole of one piece of work, and it costs nothing:\na read without it is answered exactly the same way.\n\nIf local file permissions refuse that session write, no session stamp was saved. Your saved\nconnection is unchanged; this does not prove its current server authorization. Continue\nalready-authorized MCP reads, coding and explicitly requested capture without the optional\n`session` field. Do not invent an ID, add a session trailer, or record an unsaved stamp. Do not\nbroaden filesystem access or move credentials to retry this write. A missing credential or server\nauthorization refusal is different: follow that refusal. Capture still needs the person's request\nor accepted offer, and human meaning approval is unchanged.\n\nIf a session ID was saved, near the end of the work call `mark_promise_used` with the ids you actually cited\nor acted on. Naming none is a fine answer. It is the only way anybody finds out which rows were\nworth sending you, because reading a promise and then ignoring it is not the same as using it.\n\nIf a session ID was saved, write `Balladeer-Session: <the id>` into the commit message or pull-request body you produce,\nand run `balladeer session --record` once the commit exists. That is what lets a check that goes red\nnext week be read back against what Balladeer told you before you started. Your commit message never\nleaves your machine: only the session id and the commit SHA are sent.\n\n### When somebody asks you to protect a behavior\n\nSomebody has asked you to protect a behavior when they say what the software must do, or must never\ndo again, and mean it as a rule rather than as this one bug. That is one promise, for the behavior\nthey named, and nothing else: if you notice others worth protecting, say so in a sentence and let\nthem choose, and file none of them. Never propose from a conversation that did not ask you to. A\nquestion about how something works and a plan you were asked to sketch are not requests to record\nanything, nor is a fix nobody asked you to write a rule about.\n\nAsk before you extrapolate. Ask only what you cannot work out for yourself, ask it all in one\nmessage, and stop at four. Four is a ceiling, not a target: two good ones are better. Then write the\nproposal with what you have and put whatever is still open in its open questions rather than going\nback. Never ask what this repository would answer, such as which file, which test, or which branch,\nand never ask anyone for Balladeer's own identifiers: get_promise_setup carries this repository's id\nand who can own a promise. Never ask again for what they have already told you.\n\nWrite it in their words. Every failure they named out loud is one of the failing examples, in the\nwords they named it. Every other example comes from a situation they actually described; if you\ncannot trace one to something they said, leave it out and say so in the open questions rather than\nwriting a plausible one. Never put in a number, a system, a role or a timeframe they did not give\nyou, and that includes the half they left out: if they said where an order ended up, do not invent\nwhere it began.\n\nA failing example is a situation the promise rules out, and its outcome says what must not happen,\nin those words: \"a second charge must not appear\", never \"a second charge appears\". Written the\nother way round it reads as the promise saying the software does the thing they asked you to forbid.\nAnd a promise says what the software must do for whoever depends on it. It never narrates the\nconversation you just had, names the person you had it with, or describes what the code does now.\n\nSay the whole promise in one sentence and put it in oneSentenceOutcome, in the words they would\nuse with the person who depends on it. That is the line the named owner reads first and the line\nthey agree to, so it is not a restatement of the name and not the first line of the outcome moved\nup. Leave it out rather than inventing one from something they did not say.\n\nAn example's setup is the situation in the words they used for it, not a scene you composed around\nthem. Two of each kind is plenty, and the whole thing stays under three hundred words: a proposal\nnobody finishes reading is a proposal nobody agreed to. The confidence you record is the one you\nactually have.\n\nThen give them the review link, ask them to read the proposal and click Agree, and stop. Never\napprove one yourself. Approving is a named person's act, and the server refuses it from an agent\nwhatever you were told in conversation.\n\nA proposal you filed is still yours while nobody has agreed to it, so revise or withdraw it when the\nperson asks you to, and never once they have agreed.\n\nSay promise and proposal when you talk to them. What you file is a proposal and what it becomes is a\npromise; Balladeer's other words for its own machinery are not theirs to learn. Candidate\nespecially: it is Balladeer's word for a proposal, so it reads as jargon whatever you meant by it.\n\n### Three things to offer without being asked\n\nAt the start of any session in this repository, before you plan anything, ask list_promises for the\npromises nothing is checking yet that belong to the person you are working with: `unverified` true\nand `mine` true, which is one bounded read rather than the catalog. If it returns any, say so in one\nline and offer to build their verifiers now. Name each one by its one-sentence claim rather than by\nan id, so the person can see which behavior is unguarded. Ask the same tool for\n`brokenSinceLastSeen` true as well, and where that returns any, say in one line that those promises\nbroke since they last looked, name each by its claim, and offer to fix them. Then wait for their\nanswer. The offer is the whole of it, and never start building or repairing one because nobody said\nno.\n\nTheirs, and nobody else's. `mine` keeps the promises this person owns or agreed to, and a teammate's\nunguarded promise is that teammate's to hear about: a session that opens by reading out other\npeople's unfinished work reads as an audit of them. `brokenSinceLastSeen` is that person's own by\nconstruction and needs no `mine` beside it. Drop `mine` when this person asks what the rest of the\nteam has promised, and say whose promises you are showing them when you do.\n\nWhen you have proposed promises, show them here as well as there. Put each proposal in the\nconversation in full: its one-sentence claim, who it is for, when it applies and what must then be\ntrue, the numbered cases that must keep working and the ones that must be caught, and every question\nyou left open. Then offer to finish the agreement from here, by asking for a sign-off, giving them\nthe page it returns and taking the one-time code that page shows them, so the browser is needed only\nfor signing. Give them the review link in the same message too, because some people would rather\nread it there and edit it before they agree.\nA promise whose meaning is agreed and which nothing is checking yet is one you can finish. When\nsomebody gives you a promise id, run `balladeer status <promise id>`, or expand it with get_promise.\nIf it comes back agreed with nothing checking it, say so in one line and offer to prepare and build\nits verifier now. Only if they say yes: `prepare_qualification` mints the one-time setup, or\n`balladeer prepare <promise id>` mints it and writes it where the sealed run reads it. You need no\nsign-off for that and there is no code to ask anybody for, because agreeing the meaning was their\nact and building the check that proves it is yours. Then build the verifier, seal it, push, and tell\nthem protection starts by itself when that run qualifies. The offer is the whole of it: never\nprepare one because nobody said no.\n\n### When somebody says \"tell me about\" one\n\nAn id is how a person points at something here, and every promise page and every proposal page\ncarries one. A promise id starts with prom_ and a proposal id starts with cand_: expand a promise\nwith get_promise and a proposal with get_proposal, and read neither of them out as a list of fields.\nSay in four or five sentences what it is for, who it is for, when it applies and what must then be\ntrue. Then say where it stands: a promise is agreed, and either protected or not yet checked by\nanything; a proposal is agreed by nobody and waiting on the person it names.\n\nClarify ambiguity that materially changes the behavior during capture, within the existing\nquestion budget; never invent an answer.\n\nOrdinary open questions record uncertainty; they do not prevent the named owner from agreeing to\nthe behavior as written. Agreement does not answer them or add an unstated guarantee. Separate\ncatalog-conflict questions can hold agreement until the owner rules on the stated conflict. Do not\ncall a proposal unready just because it has ordinary open questions. Offer to discuss them if\nuseful; if the person wants to, take them one at a time in the order they come back. For each ordinary question,\nsay what it decides in their words rather than in the question's; give your\nrecommendation and the reason you hold it, drawn from this repository and from the promise itself; and stop there. When\nthey answer, record what they said with resolve_question, in their own words where they gave you\nany, and where their answer changes the promise, follow it with update_proposal, add_case or\nremove_case and tell them what you changed. Never invent an answer or clear uncertainty because\nthey chose to agree. None of that agrees to anything: the named owner agrees, in their own browser\nor through a sign-off you carry, and the questions they answered stay on the proposal in their name.\n\nThe same rule governs every question you leave open in the first place. A question that names a gap\nand stops is a note, and nobody can answer a note. Say what answering it decides, in the words a\ncustomer would use, and carry your own best guess with the reason behind it, so that the shortest\ntrue answer is yes. For example: \"Decides: whether a worker that is running but reconciling nothing\ncounts as an outage this promise covers. Best guess: yes, because the promise is about somebody\nhearing before a customer does, and a wedged worker is invisible to every check this repository has.\nSay yes, or tell me otherwise.\" Balladeer refuses a question filed without both halves and says\nwhich one is missing.\n\n### Files that are sealed, and the one reason to edit one\n\nEvery file under .continuity/promises/ is sealed. The promise that owns that directory records the\nexact bytes of each file in it, so editing one, adding one there, renaming one or deleting one\nbreaks the seal. A promise whose seal is broken stops being checked, and it stays that way until a\nnamed person qualifies it again, which is their afternoon rather than your commit. Nothing in there\nis ordinary source: keep it out of refactors, formatting runs and dependency upgrades.\n\nFind out which directories are sealed before you plan an edit, not after. balladeer status lists\nthem, and list_promises names a promise's sealed directory on its row once a verifier is bound to\nit. Then, before you push, run balladeer check-seals. It prints nothing and exits zero when your\nchange touches no seal, and names the promise, its owner and its page when your change would break\none.\n\nThe one reason to edit a sealed file is to repair a verifier that can no longer run: something it\nimports moved, or the language it is written in changed under it. Never edit one to make a failing\ncheck pass. A check going red is the promise doing its job, and the repair for that belongs in the\nbehavior it protects. A repair is not finished until the promise is sealed again with the runner\nthis repository is pinned to. get_promise_setup carries that exact seal command, and\nballadeer check-seals prints it beside any promise it names.\n\n### The rest of a promise's life\nWhen a promise is obsolete, finished, deliberately off for a while, or owned by the wrong person,\npropose the change and hand them the promise page. Deciding is theirs.\n\nYou can finish one of those acts here, and only one way. Ask for a sign-off with\nrequest_owner_signoff, give them the page it returns, and ask for the one-time code that page shows\nthem. Then call the act's own tool with that code and their own words. Never call one on your own\ninitiative, never on a general approval of some earlier act, and never ask for a code you were not\ngiven: a refusal is the person's to resolve, not yours to retry. Balladeer records them as the\nperson who acted and you as the messenger.\n\nReport the promise's state exactly as Balladeer reported it: proposed, agreed, or protected, never\none in place of another.\n\n### Where your team watches this\n\nBalladeer is a web app as well as these tools, at the address setup printed. Its catalog lists every\npromise with who owns it and whether anything is checking it, and each promise has a page of its own\nshowing what this team agreed the software must do and then every run that has checked it since,\nnewest first, with the commit each one checked. Whenever there is a link to give, give the link\nrather than a summary of it: the page says what you would have said, and it stays true after this\nconversation has ended.\n\nBalladeer also has a Slack app, which a workspace administrator installs from workspace settings.\nOnce it is installed, whoever owns a promise gets a direct message when theirs goes live and when a\nrun on the default branch breaks it. Until somebody installs it, nothing is sent anywhere, so say it\nis available rather than saying they will be told.\n\n### Questions people ask\n\nAnswer these when they come up. Where you do not know, say so and point at the address setup\nprinted: a confident wrong answer about what a vendor can see is worse than no answer.\n\nWhat it does: it holds the behaviors this team has agreed the software must keep, and reports\nwhether each one is still being kept, from this repository's own tests running in its own CI.\n\nWhat it sees: the text of each promise somebody approves, this repository's numeric ids and the name\nof its default branch, and from CI the pass or fail outcome, the commit checked, and content hashes.\nNever the code, the tests, the fixtures, the logs, the prompts, or the transcripts.\n\nWhat stopping costs: nothing that matters to their tests. The verifier package, its fixtures and the\nworkflow file are theirs, in their repository, running in their CI, and disconnecting changes none\nof them. An administrator can download everything Balladeer holds at any time from workspace\nsettings, and disconnecting hands them that same download in the response that ends access.\n\nWho can approve one: the named person who owns it, in their own browser. Not an administrator on\ntheir behalf, and never you.\n\nWhether it blocks a merge: no. The check is advisory on Balladeer's side, and their own branch\nprotection is what decides whether a failing check stops anything.\n\nWho can invite people and change setup: a workspace administrator. A contributor can read the\nworkspace and propose promises, and a viewer can read it. If somebody asks you to add a teammate,\ninvite_teammate returns the page an administrator sends the invitation from, with the address filled\nin for them to read. The tool sends nothing itself: an administrator presses Send.";
|
|
197
197
|
/**
|
|
198
198
|
* How an agent turns a repository that has just been connected into a catalog a
|
|
199
199
|
* person can read. Setup's last step prints it, and the control plane's `/agent`
|
package/dist/copy.js
CHANGED
|
@@ -179,10 +179,10 @@ the page it returns and taking the one-time code that page shows them, so the br
|
|
|
179
179
|
for signing. Give them the review link in the same message too, because some people would rather
|
|
180
180
|
read it there and edit it before they agree.
|
|
181
181
|
A promise whose meaning is agreed and which nothing is checking yet is one you can finish. When
|
|
182
|
-
somebody gives you a promise id, run \`
|
|
182
|
+
somebody gives you a promise id, run \`balladeer status <promise id>\`, or expand it with get_promise.
|
|
183
183
|
If it comes back agreed with nothing checking it, say so in one line and offer to prepare and build
|
|
184
184
|
its verifier now. Only if they say yes: \`prepare_qualification\` mints the one-time setup, or
|
|
185
|
-
\`
|
|
185
|
+
\`balladeer prepare <promise id>\` mints it and writes it where the sealed run reads it. You need no
|
|
186
186
|
sign-off for that and there is no code to ask anybody for, because agreeing the meaning was their
|
|
187
187
|
act and building the check that proves it is yours. Then build the verifier, seal it, push, and tell
|
|
188
188
|
them protection starts by itself when that run qualifies. The offer is the whole of it: never
|
|
@@ -357,9 +357,9 @@ breaks the seal. A promise whose seal is broken stops being checked, and it stay
|
|
|
357
357
|
named person qualifies it again, which is their afternoon rather than your commit. Nothing in there
|
|
358
358
|
is ordinary source: keep it out of refactors, formatting runs and dependency upgrades.
|
|
359
359
|
|
|
360
|
-
Find out which directories are sealed before you plan an edit, not after.
|
|
360
|
+
Find out which directories are sealed before you plan an edit, not after. balladeer status lists
|
|
361
361
|
them, and list_promises names a promise's sealed directory on its row once a verifier is bound to
|
|
362
|
-
it. Then, before you push, run
|
|
362
|
+
it. Then, before you push, run balladeer check-seals. It prints nothing and exits zero when your
|
|
363
363
|
change touches no seal, and names the promise, its owner and its page when your change would break
|
|
364
364
|
one.
|
|
365
365
|
|
|
@@ -368,7 +368,7 @@ imports moved, or the language it is written in changed under it. Never edit one
|
|
|
368
368
|
check pass. A check going red is the promise doing its job, and the repair for that belongs in the
|
|
369
369
|
behavior it protects. A repair is not finished until the promise is sealed again with the runner
|
|
370
370
|
this repository is pinned to. get_promise_setup carries that exact seal command, and
|
|
371
|
-
|
|
371
|
+
balladeer check-seals prints it beside any promise it names.`;
|
|
372
372
|
/**
|
|
373
373
|
* Which promises a change touches, measured rather than guessed.
|
|
374
374
|
*
|
|
@@ -385,13 +385,13 @@ npx -y balladeer@latest check-seals prints it beside any promise it names.`;
|
|
|
385
385
|
* Three carriers say it, byte for byte, and a packaging test holds them
|
|
386
386
|
* together.
|
|
387
387
|
*/
|
|
388
|
-
export const TOUCH_MAP_GUIDANCE = `Prefer the measurement over the markers. Where this repository has a touch map, \`
|
|
388
|
+
export const TOUCH_MAP_GUIDANCE = `Prefer the measurement over the markers. Where this repository has a touch map, \`balladeer affected
|
|
389
389
|
<paths...>\` answers which promises ran the files in front of you, out of what each verifier actually
|
|
390
|
-
executed the last time \`
|
|
390
|
+
executed the last time \`balladeer touch-map\` measured it. Both commands run on this machine and
|
|
391
391
|
send Balladeer nothing, so you may name any path in the change.
|
|
392
392
|
|
|
393
393
|
An answer marked stale was measured against a verifier that has changed since, so read it as the
|
|
394
|
-
last thing anybody measured rather than as fact, and offer to run \`
|
|
394
|
+
last thing anybody measured rather than as fact, and offer to run \`balladeer touch-map\` again.
|
|
395
395
|
Where there is no map, where it does not name your paths, or where it reports a promise it could not
|
|
396
396
|
measure, fall back to the paths on the index rows. The map narrows which promises are worth
|
|
397
397
|
fetching. It never widens what you may read: the catalog is still never enumerated, and the fetch
|
|
@@ -416,7 +416,14 @@ When a retrieval receipt asks why you read a row, the answer for one the map nam
|
|
|
416
416
|
* Three carriers say it, byte for byte, and a packaging test holds them
|
|
417
417
|
* together.
|
|
418
418
|
*/
|
|
419
|
-
export const SESSION_STAMP = `
|
|
419
|
+
export const SESSION_STAMP = `Use the installed \`balladeer\` command after setup reports CLI readiness. If it is missing from
|
|
420
|
+
PATH (including an older 1.0.4 setup), use \`npx -y balladeer@latest <command>\` for that invocation
|
|
421
|
+
and read the current /agent instructions for installation or repair. Do not claim that an older
|
|
422
|
+
repository's instructions were refreshed. CLI availability does not prove that this coding host
|
|
423
|
+
loaded the native MCP tools or that a credential is authorized. The optional session stamp below
|
|
424
|
+
is separate from all three.
|
|
425
|
+
|
|
426
|
+
Run \`balladeer session\` when you start work here and pass the id it prints to every promise read
|
|
420
427
|
you make, as \`session\`. It is the same id for the whole of one piece of work, and it costs nothing:
|
|
421
428
|
a read without it is answered exactly the same way.
|
|
422
429
|
|
|
@@ -433,7 +440,7 @@ or acted on. Naming none is a fine answer. It is the only way anybody finds out
|
|
|
433
440
|
worth sending you, because reading a promise and then ignoring it is not the same as using it.
|
|
434
441
|
|
|
435
442
|
If a session ID was saved, write \`Balladeer-Session: <the id>\` into the commit message or pull-request body you produce,
|
|
436
|
-
and run \`
|
|
443
|
+
and run \`balladeer session --record\` once the commit exists. That is what lets a check that goes red
|
|
437
444
|
next week be read back against what Balladeer told you before you started. Your commit message never
|
|
438
445
|
leaves your machine: only the session id and the commit SHA are sent.`;
|
|
439
446
|
/**
|
package/dist/currency.js
CHANGED
|
@@ -59,7 +59,7 @@ export function noteServerVersion(headers) {
|
|
|
59
59
|
const line = advice === null ? "" : bounded(advice);
|
|
60
60
|
notice =
|
|
61
61
|
line === ""
|
|
62
|
-
? `A newer Balladeer is available (${newest}; this is ${CLI_VERSION}). Run setup --refresh,
|
|
62
|
+
? `A newer Balladeer is available (${newest}; this is ${CLI_VERSION}). Run npx -y balladeer@latest setup --refresh to update the command and instructions, then restart this session.`
|
|
63
63
|
: line;
|
|
64
64
|
}
|
|
65
65
|
/** The one line this run prints about itself, or nothing when it is current. */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Environment = NodeJS.ProcessEnv;
|
|
2
|
+
export type InstallOptions = {
|
|
3
|
+
environment: Environment;
|
|
4
|
+
/** An exact archive is injected only by installed-package tests, never a CLI flag. */
|
|
5
|
+
packageSpec?: string;
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
};
|
|
8
|
+
export type InstallResult = {
|
|
9
|
+
status: "current" | "installed";
|
|
10
|
+
command: string;
|
|
11
|
+
restartRequired: boolean;
|
|
12
|
+
manualPathRequired?: boolean;
|
|
13
|
+
version: string;
|
|
14
|
+
};
|
|
15
|
+
export declare class InstallError extends Error {
|
|
16
|
+
}
|
|
17
|
+
export declare function ensureInstalled(options: InstallOptions): InstallResult;
|
|
18
|
+
export declare function runInstall(options: InstallOptions & {
|
|
19
|
+
json: boolean;
|
|
20
|
+
write: (text: string) => void;
|
|
21
|
+
allowPartial?: boolean;
|
|
22
|
+
}): number;
|
|
23
|
+
export {};
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { CLI_VERSION } from "./wire.js";
|
|
6
|
+
export class InstallError extends Error {
|
|
7
|
+
}
|
|
8
|
+
function commandOnPath(name, env) {
|
|
9
|
+
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
10
|
+
// Match the shell: empty and relative PATH entries refer to this cwd.
|
|
11
|
+
const resolvedDirectory = resolve(directory || ".");
|
|
12
|
+
const path = join(resolvedDirectory, name);
|
|
13
|
+
// npm exec prepends transient package bins. They are not a durable install.
|
|
14
|
+
if (name.startsWith("balladeer") && resolvedDirectory.split(/[\\/]/).includes("node_modules"))
|
|
15
|
+
continue;
|
|
16
|
+
try {
|
|
17
|
+
accessSync(path, constants.X_OK);
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* next directory */
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function execute(command, args, env) {
|
|
27
|
+
return execFileSync(command, args, {
|
|
28
|
+
env,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
31
|
+
timeout: 120_000,
|
|
32
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
/** Inspect package ownership before executing a command from PATH. */
|
|
36
|
+
function packageVersion(command, env) {
|
|
37
|
+
let entry = realpathSync(command);
|
|
38
|
+
if (basename(entry) === "volta-shim") {
|
|
39
|
+
const volta = commandOnPath("volta", env);
|
|
40
|
+
if (!volta)
|
|
41
|
+
return undefined;
|
|
42
|
+
try {
|
|
43
|
+
entry = realpathSync(execute(volta, ["which", "balladeer"], env));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const root = dirname(dirname(entry));
|
|
50
|
+
try {
|
|
51
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
52
|
+
if (manifest.name !== "balladeer" ||
|
|
53
|
+
manifest.bin?.balladeer !== "dist/cli.js" ||
|
|
54
|
+
realpathSync(join(root, "dist/cli.js")) !== entry)
|
|
55
|
+
return undefined;
|
|
56
|
+
return manifest.version;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function ownedWritable(path) {
|
|
63
|
+
let ancestor = path;
|
|
64
|
+
while (!existsSync(ancestor) && dirname(ancestor) !== ancestor)
|
|
65
|
+
ancestor = dirname(ancestor);
|
|
66
|
+
try {
|
|
67
|
+
const info = statSync(ancestor);
|
|
68
|
+
if (process.getuid && info.uid !== process.getuid())
|
|
69
|
+
return false;
|
|
70
|
+
accessSync(ancestor, constants.W_OK);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function refuseSymlinkPath(path, home) {
|
|
78
|
+
let part = resolve(path);
|
|
79
|
+
while (part !== resolve(home) && dirname(part) !== part) {
|
|
80
|
+
if (existsSync(part) && lstatSync(part).isSymbolicLink())
|
|
81
|
+
throw new InstallError("The personal install location contains a symbolic link. Choose a normal writable home directory before installing.");
|
|
82
|
+
part = dirname(part);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const START = "# >>> Balladeer command PATH >>>";
|
|
86
|
+
const END = "# <<< Balladeer command PATH <<<";
|
|
87
|
+
function shellProfiles(env, bin) {
|
|
88
|
+
const home = env.HOME;
|
|
89
|
+
if (!home || !isAbsolute(home))
|
|
90
|
+
throw new InstallError("A writable absolute HOME is required for personal installation.");
|
|
91
|
+
const shell = basename(env.SHELL ?? "");
|
|
92
|
+
const loginBash = [".bash_profile", ".bash_login", ".profile"].find((name) => existsSync(join(home, name))) ??
|
|
93
|
+
".profile";
|
|
94
|
+
const filenames = shell === "zsh"
|
|
95
|
+
? [".zshrc", ".zprofile"]
|
|
96
|
+
: shell === "bash"
|
|
97
|
+
? [".bashrc", loginBash]
|
|
98
|
+
: shell === "sh"
|
|
99
|
+
? [".profile"]
|
|
100
|
+
: undefined;
|
|
101
|
+
if (!filenames)
|
|
102
|
+
return [];
|
|
103
|
+
return filenames.map((filename) => {
|
|
104
|
+
const path = join(home, filename);
|
|
105
|
+
refuseSymlinkPath(path, home);
|
|
106
|
+
if (!ownedWritable(path))
|
|
107
|
+
throw new InstallError(`Cannot update ${path}. Allow access to this file, then repeat the install command. No pairing has started.`);
|
|
108
|
+
const old = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
109
|
+
const start = old.indexOf(START), end = old.indexOf(END);
|
|
110
|
+
if (start < 0 !== end < 0 ||
|
|
111
|
+
(start >= 0 &&
|
|
112
|
+
(end < start ||
|
|
113
|
+
old.indexOf(START, start + START.length) >= 0 ||
|
|
114
|
+
old.indexOf(END, end + END.length) >= 0)))
|
|
115
|
+
throw new InstallError("The Balladeer PATH block is incomplete or duplicated. Repair that block before repeating install.");
|
|
116
|
+
const quoted = `'${bin.replaceAll("'", "'\\''")}'`;
|
|
117
|
+
const block = `${START}\nexport PATH=${quoted}:"$PATH"\n${END}`;
|
|
118
|
+
const text = start >= 0
|
|
119
|
+
? old.slice(0, start) + block + old.slice(end + END.length)
|
|
120
|
+
: old + (old.endsWith("\n") || !old ? "" : "\n") + block + "\n";
|
|
121
|
+
return { path, text, previous: old };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export function ensureInstalled(options) {
|
|
125
|
+
const env = options.environment;
|
|
126
|
+
const platform = options.platform ?? process.platform;
|
|
127
|
+
if (platform === "win32")
|
|
128
|
+
throw new InstallError("Automatic command installation currently supports macOS and Linux. On Windows, use npm install --global balladeer@latest, then verify balladeer --version. No files or pairing were changed.");
|
|
129
|
+
const current = commandOnPath("balladeer", env);
|
|
130
|
+
if (current) {
|
|
131
|
+
const version = packageVersion(current, env);
|
|
132
|
+
if (version === undefined)
|
|
133
|
+
throw new InstallError(`Another command already owns ${current}. Keep it intact and resolve that command conflict before installing Balladeer.`);
|
|
134
|
+
const newer = /^\d+\.\d+\.\d+$/.test(version) &&
|
|
135
|
+
version
|
|
136
|
+
.split(".")
|
|
137
|
+
.map(Number)
|
|
138
|
+
.some((part, index, parts) => part > Number(CLI_VERSION.split(".")[index]) &&
|
|
139
|
+
parts
|
|
140
|
+
.slice(0, index)
|
|
141
|
+
.every((value, before) => value === Number(CLI_VERSION.split(".")[before])));
|
|
142
|
+
if (version === CLI_VERSION || newer) {
|
|
143
|
+
if (execute(current, ["--version"], env) !== version)
|
|
144
|
+
throw new InstallError("The installed Balladeer command does not run the expected version. Repair the installation before pairing.");
|
|
145
|
+
return { status: "current", command: current, restartRequired: false, version };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const npm = commandOnPath("npm", env);
|
|
149
|
+
if (!npm)
|
|
150
|
+
throw new InstallError("npm is required to install the Balladeer command. Install Node 22 with npm, then repeat setup.");
|
|
151
|
+
const cache = mkdtempSync(join(tmpdir(), "balladeer-install-cache-"));
|
|
152
|
+
const installEnv = { ...env, npm_config_cache: cache };
|
|
153
|
+
try {
|
|
154
|
+
let prefix;
|
|
155
|
+
try {
|
|
156
|
+
prefix = execute(npm, ["prefix", "--global"], installEnv);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw new InstallError("Cannot read npm's global install location. Check npm permissions and repeat install; no pairing has started.");
|
|
160
|
+
}
|
|
161
|
+
if (!isAbsolute(prefix))
|
|
162
|
+
throw new InstallError("npm returned an invalid global install location.");
|
|
163
|
+
let fallback = false;
|
|
164
|
+
let profiles;
|
|
165
|
+
if (![prefix, join(prefix, "bin"), join(prefix, "lib/node_modules")].every(ownedWritable)) {
|
|
166
|
+
if (!env.HOME || !isAbsolute(env.HOME))
|
|
167
|
+
throw new InstallError("npm's global location is not writable. Choose a writable npm prefix on PATH, then repeat install.");
|
|
168
|
+
prefix = join(env.HOME, ".local", "share", "balladeer", "npm");
|
|
169
|
+
refuseSymlinkPath(prefix, env.HOME);
|
|
170
|
+
if (!ownedWritable(prefix))
|
|
171
|
+
throw new InstallError("The personal Balladeer install location is not writable. Allow access and repeat install; no pairing has started.");
|
|
172
|
+
profiles = shellProfiles(env, join(prefix, "bin"));
|
|
173
|
+
fallback = true;
|
|
174
|
+
}
|
|
175
|
+
const bin = join(prefix, "bin");
|
|
176
|
+
const target = join(bin, "balladeer");
|
|
177
|
+
if (existsSync(target) && packageVersion(target, env) === undefined)
|
|
178
|
+
throw new InstallError("The npm destination already contains another command named balladeer. Nothing was overwritten.");
|
|
179
|
+
const packageRoot = join(prefix, "lib/node_modules/balladeer");
|
|
180
|
+
if (existsSync(packageRoot) && !existsSync(target))
|
|
181
|
+
throw new InstallError("The npm destination contains an incomplete Balladeer package. Repair it explicitly before setup; nothing was overwritten.");
|
|
182
|
+
const args = [
|
|
183
|
+
"install",
|
|
184
|
+
"--global",
|
|
185
|
+
"--ignore-scripts",
|
|
186
|
+
"--no-audit",
|
|
187
|
+
"--no-fund",
|
|
188
|
+
...(fallback ? ["--prefix", prefix] : []),
|
|
189
|
+
options.packageSpec ?? `balladeer@${CLI_VERSION}`,
|
|
190
|
+
];
|
|
191
|
+
try {
|
|
192
|
+
execute(npm, args, installEnv);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
throw new InstallError("Balladeer could not be installed. Check network and write access to npm's install location, then repeat `npx -y balladeer@latest install`. No pairing has started.");
|
|
196
|
+
}
|
|
197
|
+
// Resolve the version-manager's command first, or the conventional prefix bin.
|
|
198
|
+
let installed = commandOnPath("balladeer", env);
|
|
199
|
+
if (!installed || packageVersion(installed, env) !== CLI_VERSION)
|
|
200
|
+
installed = existsSync(target) ? target : undefined;
|
|
201
|
+
if (!installed ||
|
|
202
|
+
packageVersion(installed, env) !== CLI_VERSION ||
|
|
203
|
+
execute(installed, ["--version"], env) !== CLI_VERSION)
|
|
204
|
+
throw new InstallError("npm finished, but the exact Balladeer command could not be verified. No pairing has started.");
|
|
205
|
+
let restartRequired = commandOnPath("balladeer", env) !== installed;
|
|
206
|
+
if (restartRequired && !profiles)
|
|
207
|
+
profiles = shellProfiles(env, dirname(installed));
|
|
208
|
+
if (profiles) {
|
|
209
|
+
for (const profile of profiles) {
|
|
210
|
+
const temporary = `${profile.path}.balladeer-${process.pid}.tmp`;
|
|
211
|
+
try {
|
|
212
|
+
refuseSymlinkPath(profile.path, env.HOME);
|
|
213
|
+
const currentText = existsSync(profile.path) ? readFileSync(profile.path, "utf8") : "";
|
|
214
|
+
if (currentText !== profile.previous)
|
|
215
|
+
throw new InstallError("Your shell configuration changed during installation. The command is installed, but PATH was not updated; repeat install after reviewing that file.");
|
|
216
|
+
mkdirSync(dirname(profile.path), { recursive: true });
|
|
217
|
+
writeFileSync(temporary, profile.text, {
|
|
218
|
+
mode: existsSync(profile.path) ? statSync(profile.path).mode & 0o777 : 0o600,
|
|
219
|
+
flag: "wx",
|
|
220
|
+
});
|
|
221
|
+
renameSync(temporary, profile.path);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
rmSync(temporary, { force: true });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
restartRequired = true;
|
|
228
|
+
}
|
|
229
|
+
if (restartRequired)
|
|
230
|
+
env.PATH = `${dirname(installed)}${delimiter}${env.PATH ?? ""}`;
|
|
231
|
+
return {
|
|
232
|
+
status: "installed",
|
|
233
|
+
command: installed,
|
|
234
|
+
restartRequired,
|
|
235
|
+
manualPathRequired: profiles?.length === 0,
|
|
236
|
+
version: CLI_VERSION,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
rmSync(cache, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export function runInstall(options) {
|
|
244
|
+
try {
|
|
245
|
+
const result = ensureInstalled(options);
|
|
246
|
+
const message = result.manualPathRequired
|
|
247
|
+
? `Balladeer is installed at ${result.command}. Add ${dirname(result.command)} to your shell PATH and reopen the terminal/coding agent. Automatic PATH editing is unavailable for this shell; this setup process can use the command now.`
|
|
248
|
+
: result.restartRequired
|
|
249
|
+
? "Balladeer is installed for this user. Reopen your terminal and coding agent to load the new PATH. This setup process can use the command now."
|
|
250
|
+
: `Balladeer ${result.version} is available as the bare balladeer command.`;
|
|
251
|
+
options.write(options.json
|
|
252
|
+
? JSON.stringify({
|
|
253
|
+
step: "install",
|
|
254
|
+
...result,
|
|
255
|
+
status: result.manualPathRequired ? "path_pending" : result.status,
|
|
256
|
+
message,
|
|
257
|
+
}) + "\n"
|
|
258
|
+
: message + "\n");
|
|
259
|
+
return result.manualPathRequired && !options.allowPartial ? 4 : 0;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const message = error instanceof InstallError
|
|
263
|
+
? error.message
|
|
264
|
+
: "Installation could not access a required local file or command. Allow the reported installer operation and retry; no pairing has started.";
|
|
265
|
+
options.write(options.json
|
|
266
|
+
? JSON.stringify({ step: "install", status: "blocked", message }) + "\n"
|
|
267
|
+
: message + "\n");
|
|
268
|
+
return 4;
|
|
269
|
+
}
|
|
270
|
+
}
|
package/dist/release.d.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* this repository builds is not on the registry at all. So this command never
|
|
7
|
-
* writes a registry invocation as a literal. It asks the server, which reads
|
|
8
|
-
* the publication from one setting, and falls back to the checkout form
|
|
9
|
-
* whenever it has not been told otherwise. A sentence that names a command a
|
|
10
|
-
* person cannot run is worse than no sentence at all.
|
|
11
|
-
*
|
|
12
|
-
* What that setting no longer decides is the specifier. Nothing this product
|
|
13
|
-
* hands a customer pins a version any more: a pinned line is what left legacy
|
|
14
|
-
* customers running a months-old command, because the line in their config and
|
|
15
|
-
* the line on their screen both named the version that was newest the day they
|
|
16
|
-
* ran setup. The tag is resolved against the registry on every invocation, so a
|
|
17
|
-
* publish reaches a machine at its next session start.
|
|
2
|
+
* Installed copies can always name their own durable command even when the
|
|
3
|
+
* server omits publication metadata. A source checkout retains its explicit
|
|
4
|
+
* build path until publication is known. Bootstrap and upgrade instructions
|
|
5
|
+
* use npx @latest separately; this helper describes ordinary command use.
|
|
18
6
|
*/
|
|
19
7
|
/** What a person runs from the root of a built checkout. */
|
|
20
8
|
export declare const CHECKOUT_COMMAND = "node packages/cli/dist/cli.js";
|
package/dist/release.js
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import { dirname, join, resolve } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* this repository builds is not on the registry at all. So this command never
|
|
9
|
-
* writes a registry invocation as a literal. It asks the server, which reads
|
|
10
|
-
* the publication from one setting, and falls back to the checkout form
|
|
11
|
-
* whenever it has not been told otherwise. A sentence that names a command a
|
|
12
|
-
* person cannot run is worse than no sentence at all.
|
|
13
|
-
*
|
|
14
|
-
* What that setting no longer decides is the specifier. Nothing this product
|
|
15
|
-
* hands a customer pins a version any more: a pinned line is what left legacy
|
|
16
|
-
* customers running a months-old command, because the line in their config and
|
|
17
|
-
* the line on their screen both named the version that was newest the day they
|
|
18
|
-
* ran setup. The tag is resolved against the registry on every invocation, so a
|
|
19
|
-
* publish reaches a machine at its next session start.
|
|
4
|
+
* Installed copies can always name their own durable command even when the
|
|
5
|
+
* server omits publication metadata. A source checkout retains its explicit
|
|
6
|
+
* build path until publication is known. Bootstrap and upgrade instructions
|
|
7
|
+
* use npx @latest separately; this helper describes ordinary command use.
|
|
20
8
|
*/
|
|
21
9
|
/** What a person runs from the root of a built checkout. */
|
|
22
10
|
export const CHECKOUT_COMMAND = "node packages/cli/dist/cli.js";
|
|
@@ -30,6 +18,8 @@ export const PUBLISHED_SPECIFIER = "balladeer@latest";
|
|
|
30
18
|
* version to name.
|
|
31
19
|
*/
|
|
32
20
|
export function commandPrefix(publishedVersion) {
|
|
21
|
+
if (runningFromRegistryInstall())
|
|
22
|
+
return "balladeer";
|
|
33
23
|
return publishedVersion === null || publishedVersion === undefined
|
|
34
24
|
? CHECKOUT_COMMAND
|
|
35
25
|
: `npx -y ${PUBLISHED_SPECIFIER}`;
|
package/dist/wire.d.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export declare const CLI_VERSION = "1.0.
|
|
8
|
+
export declare const CLI_VERSION = "1.0.5";
|
|
9
9
|
export declare const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export declare const CLIENT_HEADER = "x-balladeer-client";
|
|
11
|
-
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.
|
|
11
|
+
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.5";
|
|
12
12
|
export declare const DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
13
13
|
export type DelegatedScope = "repository:enroll" | "agent:issue" | "ci:connect" | "workspace:invite" | "candidate:propose";
|
|
14
14
|
export declare const DELEGATED_SCOPES: readonly DelegatedScope[];
|
package/dist/wire.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export const CLI_VERSION = "1.0.
|
|
8
|
+
export const CLI_VERSION = "1.0.5";
|
|
9
9
|
export const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export const CLIENT_HEADER = "x-balladeer-client";
|
|
11
11
|
export const CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
|