@bluewombat/slots 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,447 @@
1
+ # Slots
2
+
3
+ The Planner, Builder, and Gate commands that ship with mason. A Project points
4
+ its config at one of these paths; nothing here is imported.
5
+
6
+ ```json
7
+ "planner": ["node", "./node_modules/@bluewombat/slots/planners/one-subtask.mjs"],
8
+ "builder": {
9
+ "producer": {
10
+ "cmd": [
11
+ "node", "./node_modules/@bluewombat/slots/builders/producer.mjs",
12
+ "--prompt-file", "./build.md",
13
+ "--",
14
+ "node", "./node_modules/@bluewombat/slots/agents/claude.mjs"
15
+ ],
16
+ "timeoutMs": 600000
17
+ },
18
+ "repair": {
19
+ "cmd": [
20
+ "node", "./node_modules/@bluewombat/slots/builders/repair.mjs",
21
+ "--prompt-file", "./repair.md",
22
+ "--",
23
+ "node", "./node_modules/@bluewombat/slots/agents/claude.mjs"
24
+ ],
25
+ "timeoutMs": 600000
26
+ },
27
+ "gates": {
28
+ "defaultTimeoutMs": 120000,
29
+ "gates": [
30
+ {
31
+ "id": "workspace-changed",
32
+ "argv": ["node", "./node_modules/@bluewombat/slots/gates/workspace-changed.mjs"]
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ ```
38
+
39
+ A slot is an opaque command: it reads argv and writes one JSON line on stdout.
40
+ These are examples of that contract, not a required layer — a shell script that
41
+ prints the right line is a Gate. What every slot written in Node would otherwise
42
+ write again lives in [`@bluewombat/slot-kit`](../../host/slot-kit/README.md);
43
+ these all use it. How Host wires a slot into a run:
44
+ [`@bluewombat/runtime`](../../host/runtime/README.md).
45
+
46
+ ## Contents
47
+
48
+ - [Catalogue](#catalogue)
49
+ - [Agents](#agents)
50
+ - [Builders](#builders)
51
+ - [The prompt](#the-prompt)
52
+ - [The repair prompt](#the-repair-prompt)
53
+ - [The fix prompt](#the-fix-prompt)
54
+ - [The rules](#the-rules)
55
+ - [Transcripts](#transcripts)
56
+ - [Gates](#gates)
57
+ - [Planners](#planners)
58
+
59
+ ## Catalogue
60
+
61
+ | Slot | Kind | Answers |
62
+ | --------------------------------- | ------- | -------------------------------------------------------------------- |
63
+ | `agents/cursor.mjs` | Agent | Runs Cursor CLI with a filled prompt |
64
+ | `agents/claude.mjs` | Agent | Runs Claude Code with a filled prompt |
65
+ | `builders/producer.mjs` | Builder | First pass of a Subtask: fill `{{task}}`, then spawn the agent |
66
+ | `builders/repair.mjs` | Builder | A Gate refused the Subtask: fill `{{task}}` and `{{report}}` |
67
+ | `assembly/fix.mjs` | Builder | A judgement refused the assembled feature: fill `{{task}}` and `{{report}}` |
68
+ | `gates/workspace-changed.mjs` | Gate | The Attempt left an uncommitted change |
69
+ | `gates/parent-clean.mjs` | Gate | Isolator's Parent working files did not move |
70
+ | `gates/sensitive-path.mjs` | Gate | No path matching a glob was touched |
71
+ | `gates/ci-green.mjs` | Gate | The work line's own checks came back green |
72
+ | `planners/one-subtask.mjs` | Planner | Bootstrap: one Subtask that is the FeatureStandard itself |
73
+ | `publishers/git.mjs` | Publisher | Pushes the feature's branch to the work line's own remote |
74
+ | `refreshers/git.mjs` | Refresher | Fast-forwards the work line copy onto what the Authority holds |
75
+ | `planners/producer.mjs` | Planner | An agent CLI splits the FeatureStandard |
76
+ | `messages/conventional.mjs` | Message | `<type>: <title>` for the feature's fold, no agent; `--scope`, `--type` |
77
+ | `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the feature's subject and body under `--rules-file` |
78
+
79
+ ## Agents
80
+
81
+ A vendor CLI is not a Builder. `agents/cursor.mjs` and `agents/claude.mjs` take
82
+ a prompt that is **already filled**, run the vendor, write a transcript if asked,
83
+ and print the serialized run on stdout. They do not know `--intention` or
84
+ `--report`. A role slot after `--` is what names one:
85
+
86
+ ```json
87
+ "--",
88
+ "node", "./node_modules/@bluewombat/slots/agents/cursor.mjs",
89
+ "--transcript-dir", "./.mason/transcripts"
90
+ ```
91
+
92
+ `cursor.mjs` needs `cursor-agent` on PATH (`cursor-agent login`); `claude.mjs`
93
+ needs `claude`, signed in once.
94
+
95
+ | Option | Meaning |
96
+ | --------------------------- | ------------------------------------------------------------------ |
97
+ | `--prompt TEXT` | The filled prompt. Mutually exclusive with `--prompt-file` |
98
+ | `--prompt-file PATH` | The same text, from a file. No interpolation |
99
+ | `--bin PATH` | The CLI to run. Default: the vendor's own name, on PATH |
100
+ | `--model NAME` | Model for that run |
101
+ | `--output-format FMT` | `json` (default), `text`, or `stream-json` |
102
+ | `--permission-mode MODE` | `claude.mjs` only. Default `bypassPermissions` |
103
+ | `--transcript-dir PATH` | Write one file per turn under here. Off unless given |
104
+ | `--transcript-part NAME` | `prompt`, `stdout`, `stderr`, `timing`. Repeatable. Default: all |
105
+ | `--agent-arg VALUE` | Appended to the CLI argv, before the prompt. Repeatable |
106
+ | `--id` / `--attempt` / `--context` | How a transcript is filed. Forwarded by the role |
107
+
108
+ A global npm install belongs to one Node version: after `nvm use`, a `claude`
109
+ installed under another version is off PATH. Give `--bin` the absolute path, or
110
+ install it under the version the run uses.
111
+
112
+ The vendor's output streams to stderr as it comes. A CLI that cannot start,
113
+ whose sign-in is gone, or that refuses the invocation is `fail-blocking` once
114
+ the role classifies the run — an unusable CLI is not worth another Attempt. The
115
+ agent failing at the work is `fail-retryable`. An agent that declines in prose
116
+ still exits 0: that is what `workspace-changed` is for.
117
+
118
+ The outcome is read from the CLI's own result object, not from its `subtype`: a
119
+ `claude -p` run whose sign-in had expired reported `"subtype": "success"` beside
120
+ `"is_error": true`.
121
+
122
+ Do not pass `--worktree`: Isolator already isolated the workspace.
123
+
124
+ ## Builders
125
+
126
+ A role slot fills **its** placeholders and spawns the agent command after `--`.
127
+ `--prompt-file` and `--rules-file` belong here. Vendor flags belong after `--`.
128
+
129
+ `--report` on retry is what chooses `builders/repair.mjs`; the first-pass
130
+ producer refuses it.
131
+
132
+ ### The prompt
133
+
134
+ The shipped prompt speaks to an agent that only knows the directory it woke up
135
+ in: it names no part of mason, because none of it is visible from there. What
136
+ changes from one Project to the next is that text, not the agent, so it is a
137
+ template file on the role:
138
+
139
+ ```json
140
+ "builder": {
141
+ "producer": {
142
+ "cmd": [
143
+ "node", "./node_modules/@bluewombat/slots/builders/producer.mjs",
144
+ "--prompt-file", "./mason-prompt.md",
145
+ "--",
146
+ "node", "./node_modules/@bluewombat/slots/agents/cursor.mjs"
147
+ ],
148
+ "timeoutMs": 600000
149
+ }
150
+ }
151
+ ```
152
+
153
+ A path that names a file next to the config resolves against the config; any
154
+ other relative path resolves inside the Subtask workspace, so a template
155
+ committed in the Project travels with it.
156
+
157
+ Each role owns a closed set. An unknown name, or a required name the template
158
+ never places, is `fail-blocking`. `{{done_when}}` is `Done when:` plus the
159
+ definition of done, and vanishes whole when there is none — which is every
160
+ assembly, so `assembly/fix` does not fill it at all.
161
+
162
+ | Role | Required | Also fills |
163
+ | ----------------------- | ------------------------ | ------------------------------------------------------------------ |
164
+ | `builders/producer` | `{{task}}`, `{{rules}}` | `{{done_when}}`, `{{id}}`, `{{attempt}}` |
165
+ | `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{refused_by}}`, `{{done_when}}`, `{{id}}`, `{{attempt}}` |
166
+ | `assembly/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
167
+
168
+ A first-pass command that receives `--report` is `fail-blocking`: that report
169
+ belongs to the repair command. A repair or fix command without `--report` is the same.
170
+
171
+ ### The repair prompt
172
+
173
+ A pass a Gate refused is a different job from a first pass: not "do this" but
174
+ "this was refused, resolve it". So it is a different role — `builders/repair.mjs`
175
+ — with its own template. The agent after `--` can be the same.
176
+
177
+ A repair template must place `{{report}}` — one that never says what was wrong is
178
+ a run spent on nothing. A command that arrives with a report and no
179
+ `--prompt-file` of its own falls back to the shipped repair template.
180
+
181
+ ### The fix prompt
182
+
183
+ A judgement of the assembled feature that refused it is not a Subtask retry.
184
+ The units already passed. `assembly/fix.mjs` tells the agent to change what the
185
+ refusal names and leave the rest. Same placeholders as repair except
186
+ `{{done_when}}`, which an assembly has nothing to fill.
187
+
188
+ ### The rules
189
+
190
+ `{{rules}}` holds the bounds of the slot: one directory, work left uncommitted,
191
+ nothing pushed. A producing template must place it — one that leaves it out is
192
+ `fail-blocking` before any agent runs, naming the file. Without the bounds the
193
+ Gates are what says no, which is slower, reaches the tracker, and spends a run.
194
+
195
+ `--rules-file` replaces them with the Project's own. The last three are not a
196
+ matter of taste: the Gates read the working tree of the Subtask directory, so an
197
+ agent that commits, stashes or writes elsewhere leaves them nothing to see and
198
+ the pass is refused for having done nothing. Replacing the rules does not lift
199
+ that; it only stops saying it out loud.
200
+
201
+ ### Transcripts
202
+
203
+ The journal films the system: which phase ran, what each Gate answered. It
204
+ cannot film the turn itself — the prompt and the agent's answer exist nowhere
205
+ but inside the slot, and by the time a result reaches Host it is one word. So a
206
+ loop that misbehaves cannot be read back, only guessed at.
207
+
208
+ `--transcript-dir` writes one Markdown file per turn: the invocation, the prompt
209
+ as sent, stdout, stderr, and how long it took. Files are filed under the Feature
210
+ the Task belongs to, named for the Subtask and the Attempt. It belongs on the
211
+ **agent** command, after `--`.
212
+
213
+ ```json
214
+ "builder": {
215
+ "producer": {
216
+ "cmd": [
217
+ "node", "./node_modules/@bluewombat/slots/builders/producer.mjs",
218
+ "--",
219
+ "node", "./node_modules/@bluewombat/slots/agents/cursor.mjs",
220
+ "--transcript-dir", "./.mason/transcripts"
221
+ ],
222
+ "timeoutMs": 600000
223
+ }
224
+ }
225
+ ```
226
+
227
+ ```
228
+ .mason/transcripts/github-owner-repo-34/s1-attempt-2-20260908T134501Z.md
229
+ ```
230
+
231
+ Off unless the directory is given, and that is not caution for its own sake: a
232
+ transcript is the Project's own material — its code, its conventions, its
233
+ failures — written to disk in the clear. `--transcript-part` narrows what is
234
+ kept; `timing` alone costs nothing and says nothing about the Project. Whatever
235
+ directory is chosen belongs in `.gitignore`.
236
+
237
+ Failing to write a transcript never fails the run: it is a record of the turn,
238
+ not part of it.
239
+
240
+ ## Gates
241
+
242
+ Every Gate answers with one JSON object on stdout — `{"verdict":"pass"}`, or
243
+ `fail-retryable` / `fail-blocking` with a `report`. Which sequence a Gate belongs
244
+ in, and what mason appends to its argv, is in
245
+ [`@bluewombat/runtime`](../../host/runtime/README.md).
246
+
247
+ ```json
248
+ "builder": { "gates": {
249
+ "defaultTimeoutMs": 120000,
250
+ "gates": [
251
+ {
252
+ "id": "workspace-changed",
253
+ "argv": ["node", "./node_modules/@bluewombat/slots/gates/workspace-changed.mjs"]
254
+ },
255
+ {
256
+ "id": "parent-clean",
257
+ "argv": ["node", "./node_modules/@bluewombat/slots/gates/parent-clean.mjs"]
258
+ },
259
+ {
260
+ "id": "sensitive-path",
261
+ "argv": [
262
+ "node",
263
+ "./node_modules/@bluewombat/slots/gates/sensitive-path.mjs",
264
+ "**/.env",
265
+ "**/secrets/**"
266
+ ]
267
+ }
268
+ ]
269
+ } }
270
+ ```
271
+
272
+ `workspace-changed` fail-retries when the workspace has no uncommitted change —
273
+ the Attempt produced nothing, or committed and hid it. It takes no option. Put
274
+ it first: every Gate after it then judges work that exists. It passes at
275
+ `--stage assembly`, where "nothing changed" is a correct outcome rather than a
276
+ producer to send back.
277
+
278
+ `parent-clean` fail-blocks if Isolator's Parent working files changed
279
+ (`--parent DIR`, or the other git worktrees of this Child).
280
+
281
+ `sensitive-path` fail-retries if the workspace changed a path matching a glob.
282
+ `*` is one segment, `**` any depth.
283
+
284
+ `ci-green` reads the work line's own checks on what this workspace published,
285
+ and answers on them. It reads and nothing else — it does not push, does not open
286
+ a pull request, does not merge. Whoever put the work in front of the checks did
287
+ that before the Gate ran; a workspace with nothing published is `fail-blocking`,
288
+ because no Attempt of that Task can change it.
289
+
290
+ ```json
291
+ "assembly": { "gates": {
292
+ "defaultTimeoutMs": 900000,
293
+ "gates": [
294
+ {
295
+ "id": "ci-green",
296
+ "argv": [
297
+ "node",
298
+ "./node_modules/@bluewombat/slots/gates/ci-green.mjs",
299
+ "--token-env",
300
+ "MASON_GITHUB_TOKEN",
301
+ "--require-checks"
302
+ ]
303
+ }
304
+ ]
305
+ } }
306
+ ```
307
+
308
+ It waits while checks are running, so give it a ceiling of its own. On a red
309
+ check the report carries the failing job's log, cut at the error the runner
310
+ marked — a check name and the word "failure" tell the next Attempt nothing it
311
+ can act on. Right after a push the checks may not be created yet, and an empty
312
+ list reads as a pass: on a repository with CI, give it `--require-checks`, and
313
+ it waits for a check to appear instead. `--token-env` is required and names
314
+ the variable holding a token that can read the checks — the same one the
315
+ manager's `tokenEnv` names. It has no default on purpose: `GITHUB_TOKEN` is
316
+ what `gh` reads ahead of its own login, so an operator must not export it,
317
+ and a Gate that fell back to it refused only at the first assembly of a real
318
+ run. `--remote` and `--poll-ms` are there too. It talks to GitHub, and knows
319
+ nothing about `@bluewombat/manager-github`: a Project may run either one
320
+ without the other.
321
+
322
+ `sensitive-path`, `workspace-changed` and `ci-green` need a git workspace, which
323
+ is what Isolator makes when WorkLineStable is a git tree. They fail-block on a
324
+ workspace Isolator had to copy.
325
+
326
+ ## Planners
327
+
328
+ `one-subtask.mjs` is the bootstrap Planner: one Subtask that is the
329
+ FeatureStandard itself. It is what `mason init` writes into a fresh config, and
330
+ it takes no option.
331
+
332
+ `planners/producer.mjs` hands the split to an agent CLI. It reads the project
333
+ before answering and writes nothing into it. `--prompt-file` and `--read` belong
334
+ on the role; the vendor is named after `--`, the same way a Builder names it.
335
+
336
+ ```json
337
+ "planner": {
338
+ "cmd": [
339
+ "node", "./node_modules/@bluewombat/slots/planners/producer.mjs",
340
+ "--read", "./work-line-stable",
341
+ "--",
342
+ "node", "./node_modules/@bluewombat/slots/agents/cursor.mjs"
343
+ ],
344
+ "timeoutMs": 600000
345
+ }
346
+ ```
347
+
348
+ | Option | Meaning |
349
+ | ------------------------ | ---------------------------------------------------------------- |
350
+ | `--read PATH` | The project the agent may read before splitting. Default: cwd |
351
+ | `--prompt-file PATH` | Prompt template. Must place `{{intention}}` and `{{out}}` |
352
+
353
+ `--bin`, `--model`, `--transcript-dir` and `--agent-arg` are the agent's, after
354
+ `--`. Optional placeholder: `{{max_units}}`.
355
+
356
+ A Plan it cannot use — a cycle, a dangling dependency, more Subtasks than
357
+ `--max-units` — is this slot's own failure: it exits non-zero so the Breakdown is
358
+ retried. A FeatureStandard that cannot be split is a refusal on stdout, which
359
+ is a verdict on the intention instead.
360
+
361
+ ## Publishers
362
+
363
+ The slot that puts an assembled feature where the Authority can read it. It runs
364
+ **in the work line**, and mason appends `--id`, `--ref` — the name Isolator gave
365
+ the feature — and `--target`, the work line it is offered to.
366
+
367
+ `git.mjs` pushes that name to the work line's own remote. A plain push, no force
368
+ of any kind: Integrator only ever adds history, so republishing after a refusal
369
+ is a fast-forward. A rejection means the name moved under us, which is the one
370
+ case worth refusing rather than overwriting.
371
+
372
+ It takes no option, and the remote is `origin`. After the Authority folds, Host
373
+ reads the work line back from `origin` on its own, with no way to learn what this
374
+ slot was told — so a `--remote` here would publish to one place while the refresh
375
+ read another. It comes back when that half is a slot too.
376
+
377
+ The answer is one JSON object on stdout, like every slot:
378
+
379
+ ```json
380
+ { "ref": "issue/feature-42" }
381
+ { "outcome": "refused", "reason": "the remote rejected the push" }
382
+ ```
383
+
384
+ The reference is opaque above this line — a branch, a directory, a URL. Whatever
385
+ the Publisher answers is what the FeatureManager is told to judge, so a Project
386
+ that places its work some other way writes its own Publisher and changes nothing
387
+ else. A refusal stops the Submission and travels back to the tracker; a slot that
388
+ could not run at all exits non-zero and says why on stderr.
389
+
390
+ ## Refreshers
391
+
392
+ The other half of the Publisher's seam: what the Authority accepted has moved the
393
+ reference work line, and the copy this system works in has not. Left behind, the
394
+ next Isolation starts from a version that no longer exists — so this runs before
395
+ anything else in a pass. mason appends `--target`.
396
+
397
+ `git.mjs` fetches that branch from `origin` and fast-forwards onto it. Fast-forward
398
+ only: this system never rewrites that line, and a copy that cannot fast-forward has
399
+ been written to by somebody else. A plain directory has no reference held anywhere
400
+ else, so it answers `ok` with nothing to do.
401
+
402
+ ```json
403
+ { "ok": true }
404
+ { "outcome": "refused", "reason": "<copy> has diverged from origin/main: …\nPut it back with: git -C <copy> reset --hard origin/main\nor delete the directory and start again; it is fetched afresh." }
405
+ ```
406
+
407
+ A refusal stops the pass: nothing starts, and the next one tries again. The
408
+ reason is written for the person who reads `mason run`'s trace: the copy is the
409
+ system's, nothing in it is worth keeping, and the reason says the one command
410
+ that puts it back.
411
+
412
+ ## Stack
413
+
414
+ Plain `.mjs`, run by Node. No build step. Repository stack:
415
+ [docs/DEVELOPMENT.md](../../../docs/DEVELOPMENT.md).
416
+
417
+ ## Commands (development)
418
+
419
+ | Task | Command |
420
+ | --------- | ---------------- |
421
+ | Lint | `npm run lint` |
422
+ | Format | `npm run format` |
423
+ | Typecheck | `npm run tsc` |
424
+ | Test | `npm test` |
425
+
426
+ Every slot is tested by spawning it, the way mason does — `test/` holds those
427
+ suites and `fixtures/` the stand-in agent CLIs they run against.
428
+
429
+ The slots are plain `.mjs`, so the path in a Project config is the file in this
430
+ repository and nothing is built. `tsc` still checks them: `checkJs` reads the
431
+ kit's types through the imports, and a helper that takes an argument names its
432
+ type in JSDoc.
433
+
434
+ ## Layout
435
+
436
+ ```
437
+ README.md this file
438
+ agents/ vendor CLIs: filled prompt in, serialized run out
439
+ builders/ first-pass and repair roles for a Subtask
440
+ assembly/ the same two roles for the assembled feature
441
+ lib/ what the shipped templates say, shared by the roles
442
+ gates/ checks on the workspace, and on the work line
443
+ planners/ FeatureStandard in, a Plan out
444
+ messages/ what a commit says: a rule, or an agent under the team's guideline
445
+ test/ one suite per kind; each spawns the real command
446
+ fixtures/ fake agent CLIs the Builder and Planner suites run against
447
+ ```
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ AGENT_OPTIONS,
6
+ findExecutable,
7
+ listOption,
8
+ parseOptions,
9
+ readAgentPrompt,
10
+ runAgent,
11
+ serializeRun,
12
+ textOption,
13
+ transcriptFor,
14
+ } from "@bluewombat/slot-kit";
15
+
16
+ // Claude Code as a vendor agent. The prompt is already filled: this command
17
+ // does not interpolate. Own options only — a role slot strips Implementer's
18
+ // flags before spawning this.
19
+ // --prompt TEXT / --prompt-file PATH the filled prompt. One of the two
20
+ // --bin PATH the Claude CLI. Default: claude, found on PATH
21
+ // --model NAME alias (opus, sonnet) or full name
22
+ // --output-format FMT json (default), text, or stream-json
23
+ // --permission-mode MODE bypassPermissions (default), acceptEdits, …
24
+ // --transcript-dir PATH write one file per turn. Off by default
25
+ // --transcript-part NAME prompt, stdout, stderr, timing. Repeatable
26
+ // --agent-arg VALUE appended before the prompt. Repeatable
27
+ // --id / --attempt / --context how a transcript is filed
28
+
29
+ const NAME = "Claude Code";
30
+ const CLI_NAMES = ["claude"];
31
+ const INSTALL_DIRS = [join(homedir(), ".local/bin"), join(homedir(), ".claude/bin")];
32
+ const OPTIONS = {
33
+ ...AGENT_OPTIONS,
34
+ "--permission-mode": {
35
+ choices: ["bypassPermissions", "acceptEdits", "auto", "dontAsk", "manual", "plan"],
36
+ fallback: "bypassPermissions",
37
+ },
38
+ };
39
+
40
+ const cwd = process.cwd();
41
+
42
+ const parsed = parseOptions(process.argv.slice(2), OPTIONS, NAME);
43
+ if (!parsed.ok) {
44
+ refuse(parsed.reason);
45
+ }
46
+ const options = parsed.values;
47
+
48
+ const loaded = readAgentPrompt(options);
49
+ if (!loaded.ok) {
50
+ refuse(loaded.reason);
51
+ }
52
+ const prompt = loaded.prompt;
53
+
54
+ const bin = textOption(options, "bin") ?? findExecutable(CLI_NAMES, INSTALL_DIRS);
55
+ if (bin === undefined) {
56
+ refuse(
57
+ "Claude Code `claude` is not on PATH. Install it and run `claude` once to sign in, or pass --bin. A global npm install belongs to one Node version: after `nvm use`, give --bin the absolute path.",
58
+ );
59
+ }
60
+
61
+ const model = textOption(options, "model");
62
+ const args = [
63
+ "-p",
64
+ "--permission-mode",
65
+ /** @type {string} */ (options.permissionMode),
66
+ "--output-format",
67
+ /** @type {string} */ (options.outputFormat),
68
+ ...(model === undefined ? [] : ["--model", model]),
69
+ ...listOption(options, "agentArg"),
70
+ prompt,
71
+ ];
72
+
73
+ const transcript = transcriptFor(process.argv, options, {
74
+ slot: NAME,
75
+ bin,
76
+ args,
77
+ prompt,
78
+ });
79
+ const run = await runAgent({ file: bin, args, cwd });
80
+ transcript.write(run);
81
+ answer(run, bin);
82
+
83
+ /** @param {string} reason @returns {never} */
84
+ function refuse(reason) {
85
+ process.stdout.write(`${JSON.stringify({ error: reason })}\n`);
86
+ process.exit(1);
87
+ }
88
+
89
+ /**
90
+ * @param {import("@bluewombat/slot-kit").AgentRun} run
91
+ * @param {string} binPath
92
+ * @returns {never}
93
+ */
94
+ function answer(run, binPath) {
95
+ process.stdout.write(`${JSON.stringify(serializeRun(run, { name: NAME, bin: binPath }))}\n`);
96
+ process.exit(0);
97
+ }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ AGENT_OPTIONS,
6
+ findExecutable,
7
+ listOption,
8
+ parseOptions,
9
+ readAgentPrompt,
10
+ runAgent,
11
+ serializeRun,
12
+ textOption,
13
+ transcriptFor,
14
+ } from "@bluewombat/slot-kit";
15
+
16
+ // Cursor CLI as a vendor agent. The prompt is already filled: this command
17
+ // does not interpolate. Own options only — a role slot strips Implementer's
18
+ // flags before spawning this.
19
+ // --prompt TEXT / --prompt-file PATH the filled prompt. One of the two
20
+ // --bin PATH the Cursor CLI. Default: cursor-agent, found
21
+ // --model NAME
22
+ // --output-format FMT json (default), text, or stream-json
23
+ // --transcript-dir PATH write one file per turn. Off by default
24
+ // --transcript-part NAME prompt, stdout, stderr, timing. Repeatable
25
+ // --agent-arg VALUE appended before the prompt. Repeatable
26
+ // --id / --attempt / --context how a transcript is filed
27
+
28
+ const NAME = "Cursor CLI";
29
+ const CLI_NAMES = ["cursor-agent", "agent"];
30
+ const INSTALL_DIRS = [join(homedir(), ".local/bin"), join(homedir(), ".cursor/bin")];
31
+
32
+ const cwd = process.cwd();
33
+
34
+ const parsed = parseOptions(process.argv.slice(2), AGENT_OPTIONS, NAME);
35
+ if (!parsed.ok) {
36
+ refuse(parsed.reason);
37
+ }
38
+ const options = parsed.values;
39
+
40
+ const loaded = readAgentPrompt(options);
41
+ if (!loaded.ok) {
42
+ refuse(loaded.reason);
43
+ }
44
+ const prompt = loaded.prompt;
45
+
46
+ const bin = textOption(options, "bin") ?? findExecutable(CLI_NAMES, INSTALL_DIRS);
47
+ if (bin === undefined) {
48
+ refuse(
49
+ "Cursor CLI `cursor-agent` is not on PATH. Install it and run `cursor-agent login`, or pass --bin.",
50
+ );
51
+ }
52
+
53
+ const model = textOption(options, "model");
54
+ const args = [
55
+ "-p",
56
+ "--force",
57
+ "--trust",
58
+ "--workspace",
59
+ cwd,
60
+ "--sandbox",
61
+ "disabled",
62
+ "--output-format",
63
+ /** @type {string} */ (options.outputFormat),
64
+ ...(model === undefined ? [] : ["--model", model]),
65
+ ...listOption(options, "agentArg"),
66
+ prompt,
67
+ ];
68
+
69
+ const transcript = transcriptFor(process.argv, options, {
70
+ slot: NAME,
71
+ bin,
72
+ args,
73
+ prompt,
74
+ });
75
+ const run = await runAgent({ file: bin, args, cwd });
76
+ transcript.write(run);
77
+ answer(run, bin);
78
+
79
+ /** @param {string} reason @returns {never} */
80
+ function refuse(reason) {
81
+ process.stdout.write(`${JSON.stringify({ error: reason })}\n`);
82
+ process.exit(1);
83
+ }
84
+
85
+ /**
86
+ * @param {import("@bluewombat/slot-kit").AgentRun} run
87
+ * @param {string} binPath
88
+ * @returns {never}
89
+ */
90
+ function answer(run, binPath) {
91
+ process.stdout.write(`${JSON.stringify(serializeRun(run, { name: NAME, bin: binPath }))}\n`);
92
+ process.exit(0);
93
+ }