@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 +447 -0
- package/agents/claude.mjs +97 -0
- package/agents/cursor.mjs +93 -0
- package/assembly/fix.mjs +102 -0
- package/builders/producer.mjs +85 -0
- package/builders/repair.mjs +103 -0
- package/gates/ci-green.mjs +319 -0
- package/gates/parent-clean.mjs +168 -0
- package/gates/sensitive-path.mjs +105 -0
- package/gates/workspace-changed.mjs +56 -0
- package/lib/fill.mjs +17 -0
- package/messages/conventional.mjs +68 -0
- package/messages/git-producer.mjs +151 -0
- package/package.json +58 -0
- package/planners/one-subtask.mjs +21 -0
- package/planners/producer.mjs +226 -0
- package/publishers/git.mjs +61 -0
- package/refreshers/git.mjs +75 -0
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
|
+
}
|