@bluewombat/slots 0.8.0 → 0.10.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 +190 -122
- package/agents/agent-plugins.mjs +52 -14
- package/agents/claude.mjs +3 -3
- package/agents/cursor.mjs +1 -1
- package/builders/e2e.mjs +124 -0
- package/builders/producer.mjs +6 -6
- package/builders/repair.mjs +4 -3
- package/builders/tests.mjs +139 -0
- package/feature/fix.mjs +134 -0
- package/feature/validate.mjs +205 -0
- package/gates/ci-green.mjs +17 -6
- package/gates/e2e.mjs +538 -0
- package/gates/gitignore-leak.mjs +10 -9
- package/gates/project-checks.mjs +128 -0
- package/gates/tests-frozen.mjs +96 -0
- package/gates/tests-red.mjs +227 -0
- package/gates/workspace-changed.mjs +2 -1
- package/gates/workspace-unchanged.mjs +90 -0
- package/lib/fill.mjs +0 -6
- package/lib/junit.mjs +122 -0
- package/lib/output-tail.mjs +70 -0
- package/messages/conventional.mjs +1 -1
- package/messages/git-producer.mjs +2 -2
- package/package.json +9 -10
- package/publishers/git.mjs +1 -1
- package/refreshers/git.mjs +52 -2
- package/{assembly → ticket}/fix.mjs +5 -5
- package/{assembly → ticket}/validate.mjs +11 -13
- package/planners/one-subtask.mjs +0 -21
- package/planners/producer.mjs +0 -233
package/README.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# Slots
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The Builder, Gate, and publishing commands that ship with mason. A Project points
|
|
4
4
|
its config at one of these paths; nothing here is imported.
|
|
5
5
|
|
|
6
6
|
```yaml
|
|
7
|
-
planner:
|
|
8
|
-
cmd: [node, ./node_modules/@bluewombat/slots/planners/one-subtask.mjs]
|
|
9
7
|
builder:
|
|
10
8
|
producer:
|
|
11
9
|
cmd:
|
|
@@ -52,29 +50,35 @@ these all use it. How Host wires a slot into a run:
|
|
|
52
50
|
- [The rules](#the-rules)
|
|
53
51
|
- [Transcripts](#transcripts)
|
|
54
52
|
- [Gates](#gates)
|
|
55
|
-
- [Planners](#planners)
|
|
56
53
|
|
|
57
54
|
## Catalogue
|
|
58
55
|
|
|
59
|
-
| Slot
|
|
60
|
-
|
|
|
61
|
-
| `agents/cursor.mjs`
|
|
62
|
-
| `agents/claude.mjs`
|
|
63
|
-
| `builders/
|
|
64
|
-
| `builders/
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `gates/
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
56
|
+
| Slot | Kind | Answers |
|
|
57
|
+
| ------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `agents/cursor.mjs` | Agent | Runs Cursor CLI with a filled prompt |
|
|
59
|
+
| `agents/claude.mjs` | Agent | Runs Claude Code with a filled prompt |
|
|
60
|
+
| `builders/tests.mjs` | Builder | The tests of the task, written before its code: one test per case of its "Tests" section (or per criterion when it has none), stubs for a missing interface, each test proved passable by a throwaway implementation in the stubs, then left failing. |
|
|
61
|
+
| `builders/e2e.mjs` | Builder | The Ticket that holds a Feature's e2e acceptance suite: one test per scenario of the task, named after its id, and the manifest `e2e/scenarios.json`; nothing outside the suite folder, the suite never run. One command for the first pass and a refused one |
|
|
62
|
+
| `builders/producer.mjs` | Builder | First pass of the unit of work: fill `{{task}}`, then spawn the agent |
|
|
63
|
+
| `builders/repair.mjs` | Builder | A Gate refused the unit: fill `{{task}}` and `{{report}}` |
|
|
64
|
+
| `ticket/fix.mjs` | Builder | A judgement refused the assembled ticket: fill `{{task}}` and `{{report}}` |
|
|
65
|
+
| `feature/validate.mjs` | Builder | Read-only review of the whole feature against its request: fill `{{task}}`; same verdict line as `ticket/validate.mjs` |
|
|
66
|
+
| `feature/fix.mjs` | Builder | A judgement refused the whole feature: fill `{{task}}` and `{{report}}` |
|
|
67
|
+
| `ticket/validate.mjs` | Builder | Read-only review of the assembled work: fill `{{task}}`; answers `MASON_VERDICT: VALIDATED` or `MASON_VERDICT: REFUSED: …` |
|
|
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/workspace-unchanged.mjs` | Gate | A read-only pass changed nothing at all (a file modified, deleted or added) |
|
|
72
|
+
| `gates/gitignore-leak.mjs` | Gate | The ticket's commits add nothing its own `.gitignore` excludes |
|
|
73
|
+
| `gates/project-checks.mjs` | Gate | One command of the Project's own (typecheck, tests, lint, build) exits 0 in the workspace |
|
|
74
|
+
| `gates/tests-red.mjs` | Gate | The tests just written fail, and not on a missing import or a syntax error (`--cmd` with `{files}`, `--test-glob`, `--not-red-when`). |
|
|
75
|
+
| `gates/tests-frozen.mjs` | Gate | A test file held by the "Tests: " checkpoint was not modified, deleted or renamed (`--test-glob`, `--subject-prefix`) |
|
|
76
|
+
| `gates/e2e.mjs` | Gate | The Feature's acceptance suite, run once on the assembled line: the Project's `--up` / `--ready` / `--run` / `--down`, a JUnit report compared with the manifest of scenario ids, each failing test retried once. |
|
|
77
|
+
| `gates/ci-green.mjs` | Gate | The work line's own checks came back green |
|
|
78
|
+
| `publishers/git.mjs` | Publisher | Pushes the ticket's branch to the work line's own remote |
|
|
79
|
+
| `refreshers/git.mjs` | Refresher | Fast-forwards the work line copy onto what the Authority holds |
|
|
80
|
+
| `messages/conventional.mjs` | Message | `<type>: <title>` for the ticket's fold, no agent; `--scope`, `--type` |
|
|
81
|
+
| `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the ticket's subject and body under `--rules-file` |
|
|
78
82
|
|
|
79
83
|
## Agents
|
|
80
84
|
|
|
@@ -105,26 +109,26 @@ lowers a risk and confines nothing (RESERVATIONS F32). `claude.mjs`
|
|
|
105
109
|
needs `claude`, signed in once, and refuses those two flags: Claude is not
|
|
106
110
|
isolated yet, and starting it would keep the operator's own config.
|
|
107
111
|
|
|
108
|
-
| Option | Meaning
|
|
109
|
-
| ---------------------------------- |
|
|
110
|
-
| `--prompt TEXT` | The filled prompt. Mutually exclusive with `--prompt-file`
|
|
111
|
-
| `--prompt-file PATH` | The same text, from a file. No interpolation
|
|
112
|
-
| `--bin PATH` | The CLI to run. Default: the vendor's own name, on PATH
|
|
113
|
-
| `--model NAME` | Model for that run
|
|
114
|
-
| `--output-format FMT` | `stream-json` (default), `json`, or `text`. Skill extraction needs `stream-json`
|
|
115
|
-
| `--permission-mode MODE` | `claude.mjs` only. Default `bypassPermissions`
|
|
116
|
-
| `--transcript-dir PATH` | Write one file per turn under here. Off unless given
|
|
117
|
-
| `--transcript-part NAME` | `prompt`, `stdout`, `stderr`, `timing`. Repeatable. Default: all
|
|
118
|
-
| `--agent-arg VALUE` | Appended to the CLI argv, before the prompt. Repeatable
|
|
119
|
-
| `--agent-home DIR` | Isolate the vendor process: its `HOME` is this directory. Cursor only; Claude refuses
|
|
120
|
-
| `--api-key-env NAME` | With `--agent-home`, read `process.env[NAME]` for the vendor key. Never the value itself
|
|
121
|
-
| `--pass-env NAME` | With `--agent-home`, also hand the vendor this environment variable. Repeatable
|
|
122
|
-
| `--deny-shell NAME` | With `--agent-home`, also refuse this command to the vendor (`gh`, `vercel`…). `ssh` and `scp` always are. Repeatable
|
|
123
|
-
| `--plugin-dir DIR` | A plugin folder of the operator's (a Mason home's `ai-plugins/core/*`). Repeatable, loaded first
|
|
124
|
-
| `--profile
|
|
125
|
-
| `--plugins-out DIR` | Where
|
|
126
|
-
| `--deck-bin PATH` | The deck CLI. Default: the `@pinkpuma/deck` this package depends on
|
|
127
|
-
| `--id` / `--attempt` / `--context` | How a transcript is filed. Forwarded by the role
|
|
112
|
+
| Option | Meaning |
|
|
113
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `--prompt TEXT` | The filled prompt. Mutually exclusive with `--prompt-file` |
|
|
115
|
+
| `--prompt-file PATH` | The same text, from a file. No interpolation |
|
|
116
|
+
| `--bin PATH` | The CLI to run. Default: the vendor's own name, on PATH |
|
|
117
|
+
| `--model NAME` | Model for that run |
|
|
118
|
+
| `--output-format FMT` | `stream-json` (default), `json`, or `text`. Skill extraction needs `stream-json` |
|
|
119
|
+
| `--permission-mode MODE` | `claude.mjs` only. Default `bypassPermissions` |
|
|
120
|
+
| `--transcript-dir PATH` | Write one file per turn under here. Off unless given |
|
|
121
|
+
| `--transcript-part NAME` | `prompt`, `stdout`, `stderr`, `timing`. Repeatable. Default: all |
|
|
122
|
+
| `--agent-arg VALUE` | Appended to the CLI argv, before the prompt. Repeatable |
|
|
123
|
+
| `--agent-home DIR` | Isolate the vendor process: its `HOME` is this directory. Cursor only; Claude refuses |
|
|
124
|
+
| `--api-key-env NAME` | With `--agent-home`, read `process.env[NAME]` for the vendor key. Never the value itself |
|
|
125
|
+
| `--pass-env NAME` | With `--agent-home`, also hand the vendor this environment variable. Repeatable |
|
|
126
|
+
| `--deny-shell NAME` | With `--agent-home`, also refuse this command to the vendor (`gh`, `vercel`…). `ssh` and `scp` always are. Repeatable |
|
|
127
|
+
| `--plugin-dir DIR` | A plugin folder of the operator's (a Mason home's `ai-plugins/core/*`). Repeatable, loaded first |
|
|
128
|
+
| `--app-deck-profile PATH` | The app's deck profile for this role, as a path in the checkout: `[<app dir>/].ai/profiles/<name>`. Cursor only; Claude refuses |
|
|
129
|
+
| `--plugins-out DIR` | Where the profile is built, one folder per profile. Required with `--app-deck-profile`; never the workspace |
|
|
130
|
+
| `--deck-bin PATH` | The deck CLI. Default: the `@pinkpuma/deck` this package depends on |
|
|
131
|
+
| `--id` / `--attempt` / `--context` | How a transcript is filed. Forwarded by the role |
|
|
128
132
|
|
|
129
133
|
### Plugins
|
|
130
134
|
|
|
@@ -132,7 +136,7 @@ An agent starts with plugin folders, each passed to the vendor as
|
|
|
132
136
|
`--plugin-dir`: first the `--plugin-dir` folders, in order, then what the
|
|
133
137
|
project's [deck](https://github.com/tilap/pinkpuma-deck) profile resolves to.
|
|
134
138
|
|
|
135
|
-
`--profile builder` runs `deck materialize -C <
|
|
139
|
+
`--app-deck-profile .ai/profiles/builder` runs `deck materialize -C <app dir> --profile builder --out
|
|
136
140
|
<plugins-out>/builder --json` before the vendor starts. The working directory
|
|
137
141
|
is the checkout the agent works on, so a role gets what the project committed
|
|
138
142
|
at that commit — the base `ai-plugin.json` and `.ai/profiles/builder/`, as their
|
|
@@ -142,11 +146,11 @@ no committed lock pins, and the wrapper then refuses before the vendor starts
|
|
|
142
146
|
role refuses the same way until the project is fixed; `mason doctor` says so
|
|
143
147
|
first.
|
|
144
148
|
|
|
145
|
-
| The project has
|
|
146
|
-
|
|
|
147
|
-
| `.ai/profiles/<name>/ai-plugin.json`
|
|
148
|
-
| only a base `ai-plugin.json`
|
|
149
|
-
| no deck file at all
|
|
149
|
+
| The project has | The agent gets |
|
|
150
|
+
| ------------------------------------ | ------------------------------- |
|
|
151
|
+
| `.ai/profiles/<name>/ai-plugin.json` | the base and that profile |
|
|
152
|
+
| only a base `ai-plugin.json` | the base, and a note on stderr |
|
|
153
|
+
| no deck file at all | the `--plugin-dir` folders only |
|
|
150
154
|
|
|
151
155
|
A non-empty `.ai/profiles/<name>/prompt.md` is appended to the filled prompt
|
|
152
156
|
under `## Project instructions for this role`: what the project wants from that
|
|
@@ -161,7 +165,7 @@ deck's cache (`~/.cache/pinkpuma-deck`, or `DECK_CACHE`) has no lock, and
|
|
|
161
165
|
`--plugins-out` holds one folder per profile: both rely on a Mason run starting
|
|
162
166
|
one agent at a time, which it does.
|
|
163
167
|
|
|
164
|
-
`claude.mjs` takes `--plugin-dir` but refuses `--profile`: not isolated, Claude
|
|
168
|
+
`claude.mjs` takes `--plugin-dir` but refuses `--app-deck-profile`: not isolated, Claude
|
|
165
169
|
also loads the operator's plugins and the project's `.claude/settings.json`, so
|
|
166
170
|
a role's profile would not be all it gets.
|
|
167
171
|
|
|
@@ -169,12 +173,12 @@ The agent CLI prints **one JSON line** — a `SerializedRun` from
|
|
|
169
173
|
`@bluewombat/slot-kit` — with the vendor's raw streams plus the extras the
|
|
170
174
|
wrapper fills when it can:
|
|
171
175
|
|
|
172
|
-
| Field
|
|
173
|
-
|
|
|
174
|
-
| `skills`
|
|
175
|
-
| `usage`
|
|
176
|
-
| `plugins` | Absent when no plugin folder was loaded. Otherwise each folder: `id`, `path`, `from` (`local` or `deck`), and `integrity` (deck's, `null` for a local one)
|
|
177
|
-
| `tools`
|
|
176
|
+
| Field | Meaning |
|
|
177
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
178
|
+
| `skills` | `null` = unknown (not stream-json, or unparseable). `[]` = looked, none used. Otherwise skill names in order |
|
|
179
|
+
| `usage` | `null` = unknown. Otherwise token counts (`input`, `output`, `cacheRead`, `cacheWrite`) and `costUsd` (`null` when the vendor did not report a cost) |
|
|
180
|
+
| `plugins` | Absent when no plugin folder was loaded. Otherwise each folder: `id`, `path`, `from` (`local` or `deck`), and `integrity` (deck's, `null` for a local one) |
|
|
181
|
+
| `tools` | `null` = unknown (not stream-json, or unparseable). Otherwise what the agent did with its tools: `calls` per tool name, `busyMs` (time with a tool running; `null` when the stream has no times, as Claude's), and the lists `web` (pages fetched, searches, URLs a shell command named), `mcp` (`server/tool`, plus `getMcpTools` when the agent only listed a server's tools), `subagents` (by description), `outside` (paths named outside the working directory, skill files excepted) |
|
|
178
182
|
|
|
179
183
|
Cursor extracts skills from `stream-json` `readToolCall` paths ending in
|
|
180
184
|
`/skills/<name>/SKILL.md`. Claude extracts them from `tool_use` blocks named
|
|
@@ -237,20 +241,20 @@ builder:
|
|
|
237
241
|
```
|
|
238
242
|
|
|
239
243
|
A path that names a file next to the config resolves against the config; any
|
|
240
|
-
other relative path resolves inside the
|
|
244
|
+
other relative path resolves inside the workspace, so a template
|
|
241
245
|
committed in the Project travels with it.
|
|
242
246
|
|
|
243
247
|
Each role owns a closed set. An unknown name, or a required name the template
|
|
244
|
-
never places, is `fail-blocking`.
|
|
245
|
-
definition of done, and vanishes whole when there is none — which is every
|
|
246
|
-
assembly, so `assembly/fix` does not fill it at all.
|
|
248
|
+
never places, is `fail-blocking`.
|
|
247
249
|
|
|
248
|
-
| Role | Required | Also fills
|
|
249
|
-
| ------------------- | ------------------------------------- |
|
|
250
|
-
| `builders/producer` | `{{task}}`, `{{rules}}` | `{{history}}`, `{{
|
|
251
|
-
| `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
250
|
+
| Role | Required | Also fills |
|
|
251
|
+
| ------------------- | ------------------------------------- | -------------------------------------------------------- |
|
|
252
|
+
| `builders/producer` | `{{task}}`, `{{rules}}` | `{{history}}`, `{{id}}`, `{{attempt}}` |
|
|
253
|
+
| `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
254
|
+
| `ticket/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
255
|
+
| `feature/validate` | `{{task}}`, `{{rules}}` | `{{id}}`, `{{attempt}}` |
|
|
256
|
+
| `feature/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
257
|
+
| `ticket/validate` | `{{task}}`, `{{rules}}` | `{{id}}`, `{{attempt}}` |
|
|
254
258
|
|
|
255
259
|
A first-pass command that receives `--report` is `fail-blocking`: that report
|
|
256
260
|
belongs to the repair command. A repair or fix command without `--report` is the same.
|
|
@@ -258,7 +262,7 @@ belongs to the repair command. A repair or fix command without `--report` is the
|
|
|
258
262
|
`{{history}}` is what the Task went through before this Attempt, oldest first (the
|
|
259
263
|
refusal it came with, each Attempt, each Gate's answer; see Implementer's SPECS §
|
|
260
264
|
History). It is empty on a first Attempt and vanishes whole then, so a template
|
|
261
|
-
that does not place it
|
|
265
|
+
that does not place it simply omits it.
|
|
262
266
|
|
|
263
267
|
### The way out
|
|
264
268
|
|
|
@@ -267,7 +271,7 @@ given is not enough to act on, it changes nothing and ends its final message wit
|
|
|
267
271
|
one line, `MASON_BLOCKED: <what is missing>`. The slot reads the last such line
|
|
268
272
|
in the agent's final message, and answers `fail-blocking` with that reason, so the
|
|
269
273
|
Task stops as `escalated` and a person reads it instead of another Attempt
|
|
270
|
-
inventing a change. This is the same kind of signal as `
|
|
274
|
+
inventing a change. This is the same kind of signal as `ticket/validate`'s
|
|
271
275
|
`MASON_VERDICT:` line, and it is not tied to any refuser. A clean exit prints
|
|
272
276
|
`{ "said": "<the agent's final message, clipped>" }` on stdout, which the Task
|
|
273
277
|
keeps for its History. A Project's own template should keep a paragraph that
|
|
@@ -285,14 +289,13 @@ a run spent on nothing. A command that arrives with a report and no
|
|
|
285
289
|
|
|
286
290
|
### The fix prompt
|
|
287
291
|
|
|
288
|
-
A judgement of the assembled
|
|
289
|
-
The
|
|
290
|
-
refusal names and leave the rest. Same placeholders as repair
|
|
291
|
-
`{{done_when}}`, which an assembly has nothing to fill.
|
|
292
|
+
A judgement of the assembled work that refused it is not a retry of the unit.
|
|
293
|
+
The unit already passed. `ticket/fix.mjs` tells the agent to change what the
|
|
294
|
+
refusal names and leave the rest. Same placeholders as repair.
|
|
292
295
|
|
|
293
296
|
### The validate prompt
|
|
294
297
|
|
|
295
|
-
`
|
|
298
|
+
`ticket/validate.mjs` is a different kind of Builder: it never writes. It
|
|
296
299
|
fills only `{{task}}` and `{{rules}}` — there is nothing to fix yet, so no
|
|
297
300
|
`{{report}}` — and asks the agent to end its final message with one line:
|
|
298
301
|
`MASON_VERDICT: VALIDATED` or `MASON_VERDICT: REFUSED: <one paragraph>`. Its
|
|
@@ -319,7 +322,7 @@ nothing pushed. A producing template must place it — one that leaves it out is
|
|
|
319
322
|
Gates are what says no, which is slower, reaches the tracker, and spends a run.
|
|
320
323
|
|
|
321
324
|
`--rules-file` replaces them with the Project's own. The last three are not a
|
|
322
|
-
matter of taste: the Gates read the working tree of the
|
|
325
|
+
matter of taste: the Gates read the working tree of the workspace, so an
|
|
323
326
|
agent that commits, stashes or writes elsewhere leaves them nothing to see and
|
|
324
327
|
the pass is refused for having done nothing. Replacing the rules does not lift
|
|
325
328
|
that; it only stops saying it out loud.
|
|
@@ -335,7 +338,7 @@ loop that misbehaves cannot be read back, only guessed at.
|
|
|
335
338
|
as sent, stdout, stderr, how long it took, and — when the agent wrapper passed
|
|
336
339
|
them — `skills`, `usage` and what the tools did (`unknown`, `(none)`, or the
|
|
337
340
|
values). Files are
|
|
338
|
-
filed under the
|
|
341
|
+
filed under the Ticket the Task belongs to, named for the Subtask and the
|
|
339
342
|
Attempt. It belongs on the **agent** command, after `--`.
|
|
340
343
|
|
|
341
344
|
```yaml
|
|
@@ -392,7 +395,7 @@ builder:
|
|
|
392
395
|
`workspace-changed` fail-retries when the workspace has no uncommitted change —
|
|
393
396
|
the Attempt produced nothing, or committed and hid it. It takes no option. Put
|
|
394
397
|
it first: every Gate after it then judges work that exists. It passes at
|
|
395
|
-
`--stage
|
|
398
|
+
`--stage ticket` (or `feature`), where "nothing changed" is a correct outcome rather than a
|
|
396
399
|
producer to send back.
|
|
397
400
|
|
|
398
401
|
`parent-clean` fail-blocks if Isolator's Parent working files changed
|
|
@@ -404,15 +407,58 @@ the workspace — tracked paths, and new ones `.gitignore` does not exclude —
|
|
|
404
407
|
what an install left under an ignored directory (a dependency's own `.github/`)
|
|
405
408
|
is not a change to guard.
|
|
406
409
|
|
|
407
|
-
`
|
|
410
|
+
`project-checks --cmd COMMAND` runs one command of the Project's own in the workspace
|
|
411
|
+
and answers on its exit code. It is the Gate that makes a machine, not the Builder,
|
|
412
|
+
say that the code typechecks and the tests pass. The command goes through the shell,
|
|
413
|
+
so it is written as the Project writes it; it knows no stack.
|
|
414
|
+
The report carries the end of the output, cleaned as it arrives: escape codes removed, a line redrawn with `\r` reduced to its last state, and spinner or progress lines (`[⣷] Pulling schema...`) dropped, so thousands of redraws cannot push the real error out of the tail. `tests-red` reads its output the same way.
|
|
415
|
+
|
|
416
|
+
```yaml
|
|
417
|
+
- id: typecheck
|
|
418
|
+
timeoutMs: 300000
|
|
419
|
+
argv: [node, ./node_modules/@bluewombat/slots/gates/project-checks.mjs, --cmd, "pnpm typecheck"]
|
|
420
|
+
- id: tests
|
|
421
|
+
timeoutMs: 600000
|
|
422
|
+
argv: [node, ./node_modules/@bluewombat/slots/gates/project-checks.mjs, --cmd, "pnpm test"]
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
One command per entry: the report then names the check that failed, and the first red
|
|
426
|
+
one stops the sequence before a slower one starts. Put the cheap ones first.
|
|
427
|
+
|
|
428
|
+
- `0` passes.
|
|
429
|
+
- A non-zero exit is `fail-retryable`: the producer gets the end of the output
|
|
430
|
+
(the last ~12 KB, where the error is), not the start.
|
|
431
|
+
- Exit 126 or 127 (the shell cannot find or run the command) is `fail-blocking`: no
|
|
432
|
+
change to the work fixes it.
|
|
433
|
+
- A run that outlives the Gate's ceiling is stopped, with its whole process group, a few
|
|
434
|
+
seconds before the ceiling (`--timeout-ms`, which Implementer passes), and is
|
|
435
|
+
`fail-blocking` too. A hung check and a ceiling that is too short look the same from
|
|
436
|
+
here, so a person decides. Give a test suite a `timeoutMs` of its own.
|
|
437
|
+
- It sets `CI=true` so a watcher does not wait for a terminal.
|
|
438
|
+
- It runs at both stages. In `ticket.gates` it is the local answer on the assembled
|
|
439
|
+
ticket, so a Project with no CI of its own is still judged by a machine, and one with
|
|
440
|
+
CI gets the answer before waiting for it.
|
|
441
|
+
|
|
442
|
+
The workspace needs its dependencies, which `workLine.warm` installs once on the Feature.
|
|
443
|
+
|
|
444
|
+
`workspace-unchanged` fail-blocks when the workspace holds any change: a file modified,
|
|
445
|
+
deleted, or added that `.gitignore` does not exclude. It is for a pass that must not
|
|
446
|
+
write (`ticket.validate`), whose other Gates each look at a part: `sensitive-path` at
|
|
447
|
+
some globs, `parent-clean` at the Parent. It is meant to run where everything is
|
|
448
|
+
committed (after the align), so whatever git shows was written since. `--allow GLOB`,
|
|
449
|
+
repeatable, lets a path through: use it for what an earlier check leaves behind and the
|
|
450
|
+
Project does not ignore (a coverage report). Blocking, not retryable: what the pass
|
|
451
|
+
wrote would be committed by the next align, so a person looks.
|
|
452
|
+
|
|
453
|
+
`gitignore-leak --base REF` fail-retries if the ticket's commits add a path the
|
|
408
454
|
workspace's own `.gitignore` excludes — a Builder's `node_modules/`, a build's
|
|
409
|
-
`dist/`, a generated file. `REF` is the work line the
|
|
410
|
-
(`main`): only what the
|
|
411
|
-
only what is committed, so a path tracked on purpose before the
|
|
455
|
+
`dist/`, a generated file. `REF` is the work line the ticket is offered to
|
|
456
|
+
(`main`): only what the ticket adds since it left that line is judged, and
|
|
457
|
+
only what is committed, so a path tracked on purpose before the ticket began
|
|
412
458
|
is not blamed on it, and a removal staged but not committed does not pass. The
|
|
413
|
-
report is written for `
|
|
459
|
+
report is written for `ticket.fix`: untrack the paths, or change `.gitignore`
|
|
414
460
|
if tracking one is the point. It passes at `--stage unit`, where no commit
|
|
415
|
-
holds a
|
|
461
|
+
holds a the unit's work yet.
|
|
416
462
|
|
|
417
463
|
It is a second line, not the first: the `isolation-git` fold already keeps an
|
|
418
464
|
ignored path out of the commits it makes. It catches a leak that came some
|
|
@@ -421,7 +467,7 @@ assembly Gate, with an Authority it judges what was published, so it stops the
|
|
|
421
467
|
fold into the work line, not the push.
|
|
422
468
|
|
|
423
469
|
```yaml
|
|
424
|
-
|
|
470
|
+
ticket:
|
|
425
471
|
gates:
|
|
426
472
|
defaultTimeoutMs: 900000
|
|
427
473
|
gates:
|
|
@@ -464,47 +510,69 @@ without the other.
|
|
|
464
510
|
is what Isolator makes when WorkLineStable is a git tree. They fail-block on a
|
|
465
511
|
workspace Isolator had to copy.
|
|
466
512
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
`
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
513
|
+
### The e2e acceptance suite
|
|
514
|
+
|
|
515
|
+
`e2e.mjs` runs a Feature's acceptance suite on the assembled Feature line, from a Feature Gate
|
|
516
|
+
(`feature.validate.gates`). It knows no stack: the Project supplies four commands, run through
|
|
517
|
+
the shell in the workspace, and the Gate starts, waits, runs, compares and always stops.
|
|
518
|
+
|
|
519
|
+
| Option | Meaning |
|
|
520
|
+
| --- | --- |
|
|
521
|
+
| `--up CMD` | Required. Starts the application. It may return once started or keep running; the Gate does not wait for it, it waits for `--ready`. An `--up` that exits non-zero before that is a start failure |
|
|
522
|
+
| `--ready CMD` | Required. Exit 0 when the application answers. Polled every second until `--start-timeout-ms` |
|
|
523
|
+
| `--run CMD` | Required. Runs the suite and writes a JUnit XML report to `--junit` |
|
|
524
|
+
| `--rerun CMD` | Runs the failed scenarios again; `{ids}` is their ids joined with `\|` (a regular expression, `pnpm e2e --grep "{ids}"`). Absent: `--run` again, whole |
|
|
525
|
+
| `--down CMD` | Required. Stops the application. Always run, whatever happened; the process group of `--up` is killed after it |
|
|
526
|
+
| `--manifest PATH` | The scenarios the suite must hold. Default `e2e/scenarios.json`: an array of ids, or `{ "scenarios": [ "E1" \| { "id": "E1", "title": "…" } ] }` |
|
|
527
|
+
| `--junit PATH` | Where `--run` writes its report. Default `e2e/results/junit.xml`. Removed before every run, so an old report is never read |
|
|
528
|
+
| `--server-log PATH` | The application's log, whose tail goes in a red report. Default: what `--up` printed |
|
|
529
|
+
| `--max-scenarios N` | The most scenarios a suite may hold. Default 5 |
|
|
530
|
+
| `--max-run-ms N` | The most the whole Gate may take. Default 600000 (10 minutes), and never more than the Gate's own ceiling less 25 s |
|
|
531
|
+
| `--start-timeout-ms N` | The most `--up` and `--ready` may take together. Default 120000 |
|
|
532
|
+
| `--host HOST` | Handed to every command as `E2E_HOST`. Default `127.0.0.1`; any non-loopback address is refused |
|
|
533
|
+
| `--port N` | Handed to every command as `E2E_PORT`. Absent: the application's own choice |
|
|
534
|
+
|
|
535
|
+
Every command also gets `E2E_DATA_DIR`, an empty directory made for the run and removed after it
|
|
536
|
+
(the data is disposable), and `E2E_RESULTS_DIR`, the folder of `--junit`. A secret is not an
|
|
537
|
+
option of the Gate: the commands read it from the environment mason was started in, and it
|
|
538
|
+
belongs in no file of the repository.
|
|
539
|
+
|
|
540
|
+
A scenario is a manifest id, and every test carries its id in its name (`E2 pays by card`). The
|
|
541
|
+
comparison is exact: an id with no test, a test with no manifest id, and a test the runner
|
|
542
|
+
skipped are failures. A failing test is run once more: a pass then is reported `UNSTABLE`, and
|
|
543
|
+
is not a refusal; failing twice is red. The verdict:
|
|
544
|
+
|
|
545
|
+
| Verdict | When |
|
|
546
|
+
| --- | --- |
|
|
547
|
+
| `pass` | Every scenario ran and passed. The report says how many and which, and which were unstable |
|
|
548
|
+
| `fail-retryable` | Red: the report has, per scenario, its id, the test, the step (`at`), the message, the trace path when the runner saved one, then the last 40 lines of the server log. Screenshots and traces stay on disk beside the report. The suite is frozen; the application is the producer's to repair. Also a runner that crashed before writing a report |
|
|
549
|
+
| `fail-blocking` | What no change to the application fixes: a missing, empty or oversized manifest, an application that does not start or is never ready, a run or a retry that outlives the ceiling, a suite that exits 0 and writes no report, a non-loopback `--host` |
|
|
550
|
+
|
|
551
|
+
The Project's `.gitignore` excludes the results folder (`e2e/results/`): a Gate that guards the
|
|
552
|
+
suite folder (`sensitive-path e2e/**` in `feature.fix.gates`) would read the report as a change.
|
|
476
553
|
|
|
477
554
|
```yaml
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
- --read
|
|
483
|
-
- ./work-line-stable
|
|
484
|
-
- --
|
|
555
|
+
- id: e2e
|
|
556
|
+
skipWhen: skip-test-e2e # Host's: not run for a Feature that carries the label
|
|
557
|
+
timeoutMs: 720000
|
|
558
|
+
argv:
|
|
485
559
|
- node
|
|
486
|
-
- ./node_modules/@bluewombat/slots/
|
|
487
|
-
|
|
560
|
+
- ./node_modules/@bluewombat/slots/gates/e2e.mjs
|
|
561
|
+
- --up
|
|
562
|
+
- pnpm e2e:up
|
|
563
|
+
- --ready
|
|
564
|
+
- pnpm e2e:ready
|
|
565
|
+
- --run
|
|
566
|
+
- pnpm e2e
|
|
567
|
+
- --down
|
|
568
|
+
- pnpm e2e:down
|
|
488
569
|
```
|
|
489
570
|
|
|
490
|
-
| Option | Meaning |
|
|
491
|
-
| -------------------- | ------------------------------------------------------------- |
|
|
492
|
-
| `--read PATH` | The project the agent may read before splitting. Default: cwd |
|
|
493
|
-
| `--prompt-file PATH` | Prompt template. Must place `{{intention}}` and `{{out}}` |
|
|
494
|
-
|
|
495
|
-
`--bin`, `--model`, `--transcript-dir` and `--agent-arg` are the agent's, after
|
|
496
|
-
`--`. Optional placeholder: `{{max_units}}`.
|
|
497
|
-
|
|
498
|
-
A Plan it cannot use — a cycle, a dangling dependency, more Subtasks than
|
|
499
|
-
`--max-units` — is this slot's own failure: it exits non-zero so the Breakdown is
|
|
500
|
-
retried. A FeatureStandard that cannot be split is a refusal on stdout, which
|
|
501
|
-
is a verdict on the intention instead.
|
|
502
|
-
|
|
503
571
|
## Publishers
|
|
504
572
|
|
|
505
|
-
The slot that puts an assembled
|
|
573
|
+
The slot that puts an assembled ticket where the Authority can read it. It runs
|
|
506
574
|
**in the work line**, and mason appends `--id`, `--ref` — the name Isolator gave
|
|
507
|
-
the
|
|
575
|
+
the ticket — and `--target`, the work line it is offered to.
|
|
508
576
|
|
|
509
577
|
`git.mjs` pushes that name to the work line's own remote. A plain push, no force
|
|
510
578
|
of any kind: Integrator only ever adds history, so republishing after a refusal
|
|
@@ -519,12 +587,12 @@ read another. It comes back when that half is a slot too.
|
|
|
519
587
|
The answer is one JSON object on stdout, like every slot:
|
|
520
588
|
|
|
521
589
|
```json
|
|
522
|
-
{ "ref": "issue/
|
|
590
|
+
{ "ref": "issue/ticket-42" }
|
|
523
591
|
{ "outcome": "refused", "reason": "the remote rejected the push" }
|
|
524
592
|
```
|
|
525
593
|
|
|
526
594
|
The reference is opaque above this line — a branch, a directory, a URL. Whatever
|
|
527
|
-
the Publisher answers is what the
|
|
595
|
+
the Publisher answers is what the TicketTracker is told to judge, so a Project
|
|
528
596
|
that places its work some other way writes its own Publisher and changes nothing
|
|
529
597
|
else. A refusal stops the Submission and travels back to the tracker; a slot that
|
|
530
598
|
could not run at all exits non-zero and says why on stderr.
|
|
@@ -582,12 +650,12 @@ type in JSDoc.
|
|
|
582
650
|
```
|
|
583
651
|
README.md this file
|
|
584
652
|
agents/ vendor CLIs: filled prompt in, serialized run out
|
|
585
|
-
builders/ first-pass and repair roles for
|
|
586
|
-
|
|
653
|
+
builders/ first-pass and repair roles for the unit of work, and the e2e Ticket's
|
|
654
|
+
ticket/ fix and validate for an assembled ticket
|
|
655
|
+
feature/ fix for a whole feature
|
|
587
656
|
lib/ what the shipped templates say, shared by the roles
|
|
588
657
|
gates/ checks on the workspace, and on the work line
|
|
589
|
-
planners/ FeatureStandard in, a Plan out
|
|
590
658
|
messages/ what a commit says: a rule, or an agent under the team's guideline
|
|
591
659
|
test/ one suite per kind; each spawns the real command
|
|
592
|
-
fixtures/ fake agent CLIs the Builder
|
|
660
|
+
fixtures/ fake agent CLIs the Builder suites run against
|
|
593
661
|
```
|
package/agents/agent-plugins.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { listOption, textOption } from "@bluewombat/slot-kit";
|
|
6
6
|
|
|
7
7
|
// The plugin folders an agent starts with, shared by every vendor wrapper.
|
|
8
8
|
//
|
|
9
9
|
// --plugin-dir DIR a folder of the operator's own (the Mason home's
|
|
10
10
|
// core plugins). Repeatable, loaded first, in order
|
|
11
|
-
// --
|
|
12
|
-
//
|
|
11
|
+
// --app-deck-profile PATH
|
|
12
|
+
// the app's deck profile for this role, as a path in the
|
|
13
|
+
// checkout: `[<app dir>/].ai/profiles/<name>`. The base
|
|
14
|
+
// and that profile, as the app's locks pin them
|
|
13
15
|
// --plugins-out DIR where the profile is materialized, one folder per
|
|
14
|
-
// profile. Required with --profile: never the
|
|
16
|
+
// profile. Required with --app-deck-profile: never the
|
|
15
17
|
// workspace, whose tree the Gates read
|
|
16
18
|
// --deck-bin PATH the deck CLI. Default: the @pinkpuma/deck this
|
|
17
19
|
// package depends on
|
|
@@ -23,6 +25,37 @@ import { listOption, textOption } from "@bluewombat/slot-kit";
|
|
|
23
25
|
|
|
24
26
|
const PROFILE_NAME = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Where a deck profile is, from the path the config gives: the app folder that
|
|
30
|
+
* holds `.ai/` (empty for the checkout itself) and the profile's name. deck
|
|
31
|
+
* keeps its profiles in `<app>/.ai/profiles/<name>`, so that is the shape; the
|
|
32
|
+
* app folder is what a Project can place elsewhere.
|
|
33
|
+
*
|
|
34
|
+
* The same rule as the config's own reading of the key (`parseAppDeckProfile`
|
|
35
|
+
* in the runtime), written again because a slot depends on neither.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} given
|
|
38
|
+
* @returns {{ ok: true, root: string, name: string } | { ok: false, reason: string }}
|
|
39
|
+
*/
|
|
40
|
+
export function parseAppDeckProfile(given) {
|
|
41
|
+
const matched = /^(?:(.*?)\/)?\.ai\/profiles\/([^/]+)\/?$/.exec(given.trim());
|
|
42
|
+
const name = matched?.[2];
|
|
43
|
+
if (matched === null || name === undefined || !PROFILE_NAME.test(name) || name === "current") {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
reason: `The --app-deck-profile "${given}" is not a deck profile path: [<app dir>/].ai/profiles/<name>.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const root = matched[1] ?? ".";
|
|
50
|
+
if (isAbsolute(root) || root.split("/").includes("..")) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: `The --app-deck-profile "${given}" must stay inside the checkout.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, root, name };
|
|
57
|
+
}
|
|
58
|
+
|
|
26
59
|
/**
|
|
27
60
|
* @typedef {import("@bluewombat/slot-kit").AgentPlugin} AgentPlugin
|
|
28
61
|
* @typedef {{ ok: true, plugins: AgentPlugin[], prompt: string | undefined, notes: string[] }
|
|
@@ -44,29 +77,34 @@ export function loadPlugins(options, cwd) {
|
|
|
44
77
|
}
|
|
45
78
|
plugins.push({ id: pluginId(path), path, from: "local", integrity: null });
|
|
46
79
|
}
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
80
|
+
const given = textOption(options, "appDeckProfile");
|
|
81
|
+
if (given === undefined) {
|
|
49
82
|
return { ok: true, plugins, prompt: undefined, notes: [] };
|
|
50
83
|
}
|
|
51
|
-
|
|
52
|
-
|
|
84
|
+
const located = parseAppDeckProfile(given);
|
|
85
|
+
if (!located.ok) {
|
|
86
|
+
return located;
|
|
53
87
|
}
|
|
88
|
+
const { root, name: profile } = located;
|
|
54
89
|
const out = textOption(options, "pluginsOut");
|
|
55
90
|
if (out === undefined) {
|
|
56
91
|
return {
|
|
57
92
|
ok: false,
|
|
58
|
-
reason: "--profile needs --plugins-out: the profile is built outside the workspace.",
|
|
93
|
+
reason: "--app-deck-profile needs --plugins-out: the profile is built outside the workspace.",
|
|
59
94
|
};
|
|
60
95
|
}
|
|
61
|
-
|
|
96
|
+
// The app is where deck is run: the checkout, or a folder of it that holds
|
|
97
|
+
// its own `.ai/` (an app of a monorepo).
|
|
98
|
+
const appDir = resolve(cwd, root);
|
|
99
|
+
const hasBase = existsSync(join(appDir, "ai-plugin.json"));
|
|
62
100
|
// deck's own rule: a profile is a folder of .ai/profiles that holds a request.
|
|
63
|
-
const hasProfile = existsSync(join(
|
|
101
|
+
const hasProfile = existsSync(join(appDir, ".ai", "profiles", profile, "ai-plugin.json"));
|
|
64
102
|
if (!hasBase && !hasProfile) {
|
|
65
103
|
return {
|
|
66
104
|
ok: true,
|
|
67
105
|
plugins,
|
|
68
106
|
prompt: undefined,
|
|
69
|
-
notes: [`The
|
|
107
|
+
notes: [`The app has no deck setup: no plugin for profile ${given}.`],
|
|
70
108
|
};
|
|
71
109
|
}
|
|
72
110
|
const deck = deckBin(options);
|
|
@@ -77,7 +115,7 @@ export function loadPlugins(options, cwd) {
|
|
|
77
115
|
deck.path,
|
|
78
116
|
"materialize",
|
|
79
117
|
"-C",
|
|
80
|
-
|
|
118
|
+
appDir,
|
|
81
119
|
"--out",
|
|
82
120
|
resolve(cwd, out, profile),
|
|
83
121
|
"--json",
|
|
@@ -104,7 +142,7 @@ export function loadPlugins(options, cwd) {
|
|
|
104
142
|
ok: true,
|
|
105
143
|
plugins,
|
|
106
144
|
prompt: profilePrompt(result.prompt),
|
|
107
|
-
notes: hasProfile ? [] : [`The
|
|
145
|
+
notes: hasProfile ? [] : [`The app has no deck profile ${given}: base plugins only.`],
|
|
108
146
|
};
|
|
109
147
|
}
|
|
110
148
|
|