@bluewombat/slots 0.9.0 → 0.11.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 +205 -66
- package/agents/agent-plugins.mjs +52 -14
- package/agents/claude.mjs +3 -3
- package/agents/cursor.mjs +1 -1
- package/builders/feature-test.mjs +134 -0
- package/builders/producer.mjs +4 -0
- package/builders/repair.mjs +4 -0
- package/feature/fix.mjs +134 -0
- package/feature/validate.mjs +205 -0
- package/gates/base-diff.mjs +276 -0
- package/gates/ci-green.mjs +100 -9
- package/gates/feature-tests.mjs +280 -0
- package/gates/gitignore-leak.mjs +2 -1
- package/gates/project-checks.mjs +128 -0
- package/gates/tests-only.mjs +65 -0
- package/gates/workspace-changed.mjs +2 -1
- package/gates/workspace-unchanged.mjs +90 -0
- package/lib/junit.mjs +122 -0
- package/lib/output-tail.mjs +70 -0
- package/package.json +7 -5
- package/refreshers/git.mjs +35 -1
- package/{assembly → ticket}/fix.mjs +2 -2
- package/{assembly → ticket}/validate.mjs +5 -7
package/README.md
CHANGED
|
@@ -53,23 +53,31 @@ these all use it. How Host wires a slot into a run:
|
|
|
53
53
|
|
|
54
54
|
## Catalogue
|
|
55
55
|
|
|
56
|
-
| Slot
|
|
57
|
-
|
|
|
58
|
-
| `agents/cursor.mjs`
|
|
59
|
-
| `agents/claude.mjs`
|
|
60
|
-
| `builders/
|
|
61
|
-
| `builders/
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `gates/
|
|
68
|
-
| `gates/
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
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/feature-test.mjs` | Builder | The Ticket that holds a Feature's tests: one test per scenario of the task, named after its id, existing tests updated when a scenario changes them, and the manifest `feature-tests/scenarios.json`; tests never run. One command for the first pass and a refused one |
|
|
61
|
+
| `builders/producer.mjs` | Builder | First pass of the unit of work: fill `{{task}}`, then spawn the agent |
|
|
62
|
+
| `builders/repair.mjs` | Builder | A Gate refused the unit: fill `{{task}}` and `{{report}}` |
|
|
63
|
+
| `ticket/fix.mjs` | Builder | A judgement refused the assembled ticket: fill `{{task}}` and `{{report}}` |
|
|
64
|
+
| `feature/validate.mjs` | Builder | Read-only review of the whole feature against its request: fill `{{task}}`; same verdict line as `ticket/validate.mjs` |
|
|
65
|
+
| `feature/fix.mjs` | Builder | A judgement refused the whole feature: fill `{{task}}` and `{{report}}` |
|
|
66
|
+
| `ticket/validate.mjs` | Builder | Read-only review of the assembled work: fill `{{task}}`; answers `MASON_VERDICT: VALIDATED` or `MASON_VERDICT: REFUSED: …` |
|
|
67
|
+
| `gates/workspace-changed.mjs` | Gate | The Attempt left an uncommitted change |
|
|
68
|
+
| `gates/parent-clean.mjs` | Gate | Isolator's Parent working files did not move |
|
|
69
|
+
| `gates/sensitive-path.mjs` | Gate | No path matching a glob was touched |
|
|
70
|
+
| `gates/base-diff.mjs` | Gate | What the work changed since the line (`--base`): no denied path was touched, or a command holds when a path did |
|
|
71
|
+
| `gates/tests-only.mjs` | Gate | Every path the workspace changed matches a glob: the tests, their helpers and the manifest, nothing else |
|
|
72
|
+
| `gates/workspace-unchanged.mjs` | Gate | A read-only pass changed nothing at all (a file modified, deleted or added) |
|
|
73
|
+
| `gates/gitignore-leak.mjs` | Gate | The ticket's commits add nothing its own `.gitignore` excludes |
|
|
74
|
+
| `gates/project-checks.mjs` | Gate | One command of the Project's own (typecheck, tests, lint, build) exits 0 in the workspace |
|
|
75
|
+
| `gates/feature-tests.mjs` | Gate | The Feature's own tests, run once on the assembled line: the Project's `--run` filtered on the ids of the manifest, a JUnit report compared with them. |
|
|
76
|
+
| `gates/ci-green.mjs` | Gate | The work line's own checks came back green |
|
|
77
|
+
| `publishers/git.mjs` | Publisher | Pushes the ticket's branch to the work line's own remote |
|
|
78
|
+
| `refreshers/git.mjs` | Refresher | Fast-forwards the work line copy onto what the Authority holds |
|
|
79
|
+
| `messages/conventional.mjs` | Message | `<type>: <title>` for the ticket's fold, no agent; `--scope`, `--type` |
|
|
80
|
+
| `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the ticket's subject and body under `--rules-file` |
|
|
73
81
|
|
|
74
82
|
## Agents
|
|
75
83
|
|
|
@@ -100,26 +108,26 @@ lowers a risk and confines nothing (RESERVATIONS F32). `claude.mjs`
|
|
|
100
108
|
needs `claude`, signed in once, and refuses those two flags: Claude is not
|
|
101
109
|
isolated yet, and starting it would keep the operator's own config.
|
|
102
110
|
|
|
103
|
-
| Option | Meaning
|
|
104
|
-
| ---------------------------------- |
|
|
105
|
-
| `--prompt TEXT` | The filled prompt. Mutually exclusive with `--prompt-file`
|
|
106
|
-
| `--prompt-file PATH` | The same text, from a file. No interpolation
|
|
107
|
-
| `--bin PATH` | The CLI to run. Default: the vendor's own name, on PATH
|
|
108
|
-
| `--model NAME` | Model for that run
|
|
109
|
-
| `--output-format FMT` | `stream-json` (default), `json`, or `text`. Skill extraction needs `stream-json`
|
|
110
|
-
| `--permission-mode MODE` | `claude.mjs` only. Default `bypassPermissions`
|
|
111
|
-
| `--transcript-dir PATH` | Write one file per turn under here. Off unless given
|
|
112
|
-
| `--transcript-part NAME` | `prompt`, `stdout`, `stderr`, `timing`. Repeatable. Default: all
|
|
113
|
-
| `--agent-arg VALUE` | Appended to the CLI argv, before the prompt. Repeatable
|
|
114
|
-
| `--agent-home DIR` | Isolate the vendor process: its `HOME` is this directory. Cursor only; Claude refuses
|
|
115
|
-
| `--api-key-env NAME` | With `--agent-home`, read `process.env[NAME]` for the vendor key. Never the value itself
|
|
116
|
-
| `--pass-env NAME` | With `--agent-home`, also hand the vendor this environment variable. Repeatable
|
|
117
|
-
| `--deny-shell NAME` | With `--agent-home`, also refuse this command to the vendor (`gh`, `vercel`…). `ssh` and `scp` always are. Repeatable
|
|
118
|
-
| `--plugin-dir DIR` | A plugin folder of the operator's (a Mason home's `ai-plugins/core/*`). Repeatable, loaded first
|
|
119
|
-
| `--profile
|
|
120
|
-
| `--plugins-out DIR` | Where
|
|
121
|
-
| `--deck-bin PATH` | The deck CLI. Default: the `@pinkpuma/deck` this package depends on
|
|
122
|
-
| `--id` / `--attempt` / `--context` | How a transcript is filed. Forwarded by the role
|
|
111
|
+
| Option | Meaning |
|
|
112
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `--prompt TEXT` | The filled prompt. Mutually exclusive with `--prompt-file` |
|
|
114
|
+
| `--prompt-file PATH` | The same text, from a file. No interpolation |
|
|
115
|
+
| `--bin PATH` | The CLI to run. Default: the vendor's own name, on PATH |
|
|
116
|
+
| `--model NAME` | Model for that run |
|
|
117
|
+
| `--output-format FMT` | `stream-json` (default), `json`, or `text`. Skill extraction needs `stream-json` |
|
|
118
|
+
| `--permission-mode MODE` | `claude.mjs` only. Default `bypassPermissions` |
|
|
119
|
+
| `--transcript-dir PATH` | Write one file per turn under here. Off unless given |
|
|
120
|
+
| `--transcript-part NAME` | `prompt`, `stdout`, `stderr`, `timing`. Repeatable. Default: all |
|
|
121
|
+
| `--agent-arg VALUE` | Appended to the CLI argv, before the prompt. Repeatable |
|
|
122
|
+
| `--agent-home DIR` | Isolate the vendor process: its `HOME` is this directory. Cursor only; Claude refuses |
|
|
123
|
+
| `--api-key-env NAME` | With `--agent-home`, read `process.env[NAME]` for the vendor key. Never the value itself |
|
|
124
|
+
| `--pass-env NAME` | With `--agent-home`, also hand the vendor this environment variable. Repeatable |
|
|
125
|
+
| `--deny-shell NAME` | With `--agent-home`, also refuse this command to the vendor (`gh`, `vercel`…). `ssh` and `scp` always are. Repeatable |
|
|
126
|
+
| `--plugin-dir DIR` | A plugin folder of the operator's (a Mason home's `ai-plugins/core/*`). Repeatable, loaded first |
|
|
127
|
+
| `--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 |
|
|
128
|
+
| `--plugins-out DIR` | Where the profile is built, one folder per profile. Required with `--app-deck-profile`; never the workspace |
|
|
129
|
+
| `--deck-bin PATH` | The deck CLI. Default: the `@pinkpuma/deck` this package depends on |
|
|
130
|
+
| `--id` / `--attempt` / `--context` | How a transcript is filed. Forwarded by the role |
|
|
123
131
|
|
|
124
132
|
### Plugins
|
|
125
133
|
|
|
@@ -127,7 +135,7 @@ An agent starts with plugin folders, each passed to the vendor as
|
|
|
127
135
|
`--plugin-dir`: first the `--plugin-dir` folders, in order, then what the
|
|
128
136
|
project's [deck](https://github.com/tilap/pinkpuma-deck) profile resolves to.
|
|
129
137
|
|
|
130
|
-
`--profile builder` runs `deck materialize -C <
|
|
138
|
+
`--app-deck-profile .ai/profiles/builder` runs `deck materialize -C <app dir> --profile builder --out
|
|
131
139
|
<plugins-out>/builder --json` before the vendor starts. The working directory
|
|
132
140
|
is the checkout the agent works on, so a role gets what the project committed
|
|
133
141
|
at that commit — the base `ai-plugin.json` and `.ai/profiles/builder/`, as their
|
|
@@ -137,11 +145,11 @@ no committed lock pins, and the wrapper then refuses before the vendor starts
|
|
|
137
145
|
role refuses the same way until the project is fixed; `mason doctor` says so
|
|
138
146
|
first.
|
|
139
147
|
|
|
140
|
-
| The project has
|
|
141
|
-
|
|
|
142
|
-
| `.ai/profiles/<name>/ai-plugin.json`
|
|
143
|
-
| only a base `ai-plugin.json`
|
|
144
|
-
| no deck file at all
|
|
148
|
+
| The project has | The agent gets |
|
|
149
|
+
| ------------------------------------ | ------------------------------- |
|
|
150
|
+
| `.ai/profiles/<name>/ai-plugin.json` | the base and that profile |
|
|
151
|
+
| only a base `ai-plugin.json` | the base, and a note on stderr |
|
|
152
|
+
| no deck file at all | the `--plugin-dir` folders only |
|
|
145
153
|
|
|
146
154
|
A non-empty `.ai/profiles/<name>/prompt.md` is appended to the filled prompt
|
|
147
155
|
under `## Project instructions for this role`: what the project wants from that
|
|
@@ -156,7 +164,7 @@ deck's cache (`~/.cache/pinkpuma-deck`, or `DECK_CACHE`) has no lock, and
|
|
|
156
164
|
`--plugins-out` holds one folder per profile: both rely on a Mason run starting
|
|
157
165
|
one agent at a time, which it does.
|
|
158
166
|
|
|
159
|
-
`claude.mjs` takes `--plugin-dir` but refuses `--profile`: not isolated, Claude
|
|
167
|
+
`claude.mjs` takes `--plugin-dir` but refuses `--app-deck-profile`: not isolated, Claude
|
|
160
168
|
also loads the operator's plugins and the project's `.claude/settings.json`, so
|
|
161
169
|
a role's profile would not be all it gets.
|
|
162
170
|
|
|
@@ -164,12 +172,12 @@ The agent CLI prints **one JSON line** — a `SerializedRun` from
|
|
|
164
172
|
`@bluewombat/slot-kit` — with the vendor's raw streams plus the extras the
|
|
165
173
|
wrapper fills when it can:
|
|
166
174
|
|
|
167
|
-
| Field
|
|
168
|
-
|
|
|
169
|
-
| `skills`
|
|
170
|
-
| `usage`
|
|
171
|
-
| `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)
|
|
172
|
-
| `tools`
|
|
175
|
+
| Field | Meaning |
|
|
176
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
177
|
+
| `skills` | `null` = unknown (not stream-json, or unparseable). `[]` = looked, none used. Otherwise skill names in order |
|
|
178
|
+
| `usage` | `null` = unknown. Otherwise token counts (`input`, `output`, `cacheRead`, `cacheWrite`) and `costUsd` (`null` when the vendor did not report a cost) |
|
|
179
|
+
| `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) |
|
|
180
|
+
| `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) |
|
|
173
181
|
|
|
174
182
|
Cursor extracts skills from `stream-json` `readToolCall` paths ending in
|
|
175
183
|
`/skills/<name>/SKILL.md`. Claude extracts them from `tool_use` blocks named
|
|
@@ -238,12 +246,14 @@ committed in the Project travels with it.
|
|
|
238
246
|
Each role owns a closed set. An unknown name, or a required name the template
|
|
239
247
|
never places, is `fail-blocking`.
|
|
240
248
|
|
|
241
|
-
| Role | Required | Also fills
|
|
242
|
-
| ------------------- | ------------------------------------- |
|
|
243
|
-
| `builders/producer` | `{{task}}`, `{{rules}}` | `{{history}}`, `{{id}}`, `{{attempt}}`
|
|
244
|
-
| `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}`
|
|
245
|
-
| `
|
|
246
|
-
| `
|
|
249
|
+
| Role | Required | Also fills |
|
|
250
|
+
| ------------------- | ------------------------------------- | -------------------------------------------------------- |
|
|
251
|
+
| `builders/producer` | `{{task}}`, `{{rules}}` | `{{history}}`, `{{id}}`, `{{attempt}}` |
|
|
252
|
+
| `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
253
|
+
| `ticket/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
254
|
+
| `feature/validate` | `{{task}}`, `{{rules}}` | `{{id}}`, `{{attempt}}` |
|
|
255
|
+
| `feature/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
256
|
+
| `ticket/validate` | `{{task}}`, `{{rules}}` | `{{id}}`, `{{attempt}}` |
|
|
247
257
|
|
|
248
258
|
A first-pass command that receives `--report` is `fail-blocking`: that report
|
|
249
259
|
belongs to the repair command. A repair or fix command without `--report` is the same.
|
|
@@ -260,7 +270,7 @@ given is not enough to act on, it changes nothing and ends its final message wit
|
|
|
260
270
|
one line, `MASON_BLOCKED: <what is missing>`. The slot reads the last such line
|
|
261
271
|
in the agent's final message, and answers `fail-blocking` with that reason, so the
|
|
262
272
|
Task stops as `escalated` and a person reads it instead of another Attempt
|
|
263
|
-
inventing a change. This is the same kind of signal as `
|
|
273
|
+
inventing a change. This is the same kind of signal as `ticket/validate`'s
|
|
264
274
|
`MASON_VERDICT:` line, and it is not tied to any refuser. A clean exit prints
|
|
265
275
|
`{ "said": "<the agent's final message, clipped>" }` on stdout, which the Task
|
|
266
276
|
keeps for its History. A Project's own template should keep a paragraph that
|
|
@@ -279,12 +289,12 @@ a run spent on nothing. A command that arrives with a report and no
|
|
|
279
289
|
### The fix prompt
|
|
280
290
|
|
|
281
291
|
A judgement of the assembled work that refused it is not a retry of the unit.
|
|
282
|
-
The unit already passed. `
|
|
292
|
+
The unit already passed. `ticket/fix.mjs` tells the agent to change what the
|
|
283
293
|
refusal names and leave the rest. Same placeholders as repair.
|
|
284
294
|
|
|
285
295
|
### The validate prompt
|
|
286
296
|
|
|
287
|
-
`
|
|
297
|
+
`ticket/validate.mjs` is a different kind of Builder: it never writes. It
|
|
288
298
|
fills only `{{task}}` and `{{rules}}` — there is nothing to fix yet, so no
|
|
289
299
|
`{{report}}` — and asks the agent to end its final message with one line:
|
|
290
300
|
`MASON_VERDICT: VALIDATED` or `MASON_VERDICT: REFUSED: <one paragraph>`. Its
|
|
@@ -384,7 +394,7 @@ builder:
|
|
|
384
394
|
`workspace-changed` fail-retries when the workspace has no uncommitted change —
|
|
385
395
|
the Attempt produced nothing, or committed and hid it. It takes no option. Put
|
|
386
396
|
it first: every Gate after it then judges work that exists. It passes at
|
|
387
|
-
`--stage
|
|
397
|
+
`--stage ticket` (or `feature`), where "nothing changed" is a correct outcome rather than a
|
|
388
398
|
producer to send back.
|
|
389
399
|
|
|
390
400
|
`parent-clean` fail-blocks if Isolator's Parent working files changed
|
|
@@ -396,13 +406,97 @@ the workspace — tracked paths, and new ones `.gitignore` does not exclude —
|
|
|
396
406
|
what an install left under an ignored directory (a dependency's own `.github/`)
|
|
397
407
|
is not a change to guard.
|
|
398
408
|
|
|
409
|
+
`base-diff` is `sensitive-path` against the work line instead of `HEAD`. It takes the
|
|
410
|
+
paths that differ between the base and the workspace — the commits since the workspace
|
|
411
|
+
left it (`BASE...HEAD`) and what is on disk and not committed, an ignored path excluded —
|
|
412
|
+
so it still sees the work after the commit, and answers at the unit, the ticket and the
|
|
413
|
+
recheck (`--stage assembly`). The base is the branch checked out in the **main worktree**
|
|
414
|
+
of the repository the workspace belongs to: Isolation makes every workspace a worktree of
|
|
415
|
+
the work line's copy, and Host keeps that copy on the line the work lands on, a
|
|
416
|
+
Feature's own line while a Feature is open. In a Ticket of a Feature the diff is therefore
|
|
417
|
+
the Ticket's own, not the Feature's so far. `--base REF` names another ref instead; a
|
|
418
|
+
hand-named `main` is the copy's local branch, which Host does not move (the Refresher
|
|
419
|
+
moves the Feature's line), so it can be weeks old. Two uses, alone or together:
|
|
420
|
+
|
|
421
|
+
```yaml
|
|
422
|
+
- id: no-workflow-edit
|
|
423
|
+
argv: [node, ./node_modules/@bluewombat/slots/gates/base-diff.mjs, --deny, ".github/workflows/**"]
|
|
424
|
+
- id: e2e-when-app-changes
|
|
425
|
+
timeoutMs: 600000
|
|
426
|
+
argv:
|
|
427
|
+
- node
|
|
428
|
+
- ./node_modules/@bluewombat/slots/gates/base-diff.mjs
|
|
429
|
+
- --when
|
|
430
|
+
- "src/**"
|
|
431
|
+
- --run
|
|
432
|
+
- "npm run e2e"
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`--deny GLOB` (repeatable) fail-retries when a changed path matches. `--when GLOB`
|
|
436
|
+
(repeatable) with `--run COMMAND` hands the command to `project-checks` when a changed path
|
|
437
|
+
matches (so it is cleaned, bounded by the Gate's ceiling and classified as every command of
|
|
438
|
+
the Project is) and answers as it does; `MASON_BASE` is the base and `MASON_CHANGED_FILES`
|
|
439
|
+
the changed paths one per line. When nothing matches it passes without running it. It
|
|
440
|
+
fail-blocks where there is no base to derive: the workspace is the main worktree itself,
|
|
441
|
+
the main worktree has no branch (detached), a `--base` the workspace lacks, and
|
|
442
|
+
`--stage feature` without `--base` — the Feature's workspace is its line with the target
|
|
443
|
+
merged in, so the line would show the target's work; name the target there.
|
|
444
|
+
|
|
445
|
+
`tests-only` is its counterpart: it fail-retries if the workspace changed a path that matches
|
|
446
|
+
none of the globs (added, modified, deleted or renamed, tracked or new and not ignored). It is
|
|
447
|
+
the Gate of the feature-test Ticket, whose tests sit among the Project's code
|
|
448
|
+
(`**/*.test.ts`), where a list of denied paths cannot say "everything but tests".
|
|
449
|
+
|
|
450
|
+
`project-checks --cmd COMMAND` runs one command of the Project's own in the workspace
|
|
451
|
+
and answers on its exit code. It is the Gate that makes a machine, not the Builder,
|
|
452
|
+
say that the code typechecks and the tests pass. The command goes through the shell,
|
|
453
|
+
so it is written as the Project writes it; it knows no stack.
|
|
454
|
+
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. `feature-tests` reads its output the same way.
|
|
455
|
+
|
|
456
|
+
```yaml
|
|
457
|
+
- id: typecheck
|
|
458
|
+
timeoutMs: 300000
|
|
459
|
+
argv: [node, ./node_modules/@bluewombat/slots/gates/project-checks.mjs, --cmd, "pnpm typecheck"]
|
|
460
|
+
- id: tests
|
|
461
|
+
timeoutMs: 600000
|
|
462
|
+
argv: [node, ./node_modules/@bluewombat/slots/gates/project-checks.mjs, --cmd, "pnpm test"]
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
One command per entry: the report then names the check that failed, and the first red
|
|
466
|
+
one stops the sequence before a slower one starts. Put the cheap ones first.
|
|
467
|
+
|
|
468
|
+
- `0` passes.
|
|
469
|
+
- A non-zero exit is `fail-retryable`: the producer gets the end of the output
|
|
470
|
+
(the last ~12 KB, where the error is), not the start.
|
|
471
|
+
- Exit 126 or 127 (the shell cannot find or run the command) is `fail-blocking`: no
|
|
472
|
+
change to the work fixes it.
|
|
473
|
+
- A run that outlives the Gate's ceiling is stopped, with its whole process group, a few
|
|
474
|
+
seconds before the ceiling (`--timeout-ms`, which Implementer passes), and is
|
|
475
|
+
`fail-blocking` too. A hung check and a ceiling that is too short look the same from
|
|
476
|
+
here, so a person decides. Give a test suite a `timeoutMs` of its own.
|
|
477
|
+
- It sets `CI=true` so a watcher does not wait for a terminal.
|
|
478
|
+
- It runs at both stages. In `ticket.gates` it is the local answer on the assembled
|
|
479
|
+
ticket, so a Project with no CI of its own is still judged by a machine, and one with
|
|
480
|
+
CI gets the answer before waiting for it.
|
|
481
|
+
|
|
482
|
+
The workspace needs its dependencies, which `workLine.warm` installs once on the Feature.
|
|
483
|
+
|
|
484
|
+
`workspace-unchanged` fail-blocks when the workspace holds any change: a file modified,
|
|
485
|
+
deleted, or added that `.gitignore` does not exclude. It is for a pass that must not
|
|
486
|
+
write (`ticket.validate`), whose other Gates each look at a part: `sensitive-path` at
|
|
487
|
+
some globs, `parent-clean` at the Parent. It is meant to run where everything is
|
|
488
|
+
committed (after the align), so whatever git shows was written since. `--allow GLOB`,
|
|
489
|
+
repeatable, lets a path through: use it for what an earlier check leaves behind and the
|
|
490
|
+
Project does not ignore (a coverage report). Blocking, not retryable: what the pass
|
|
491
|
+
wrote would be committed by the next align, so a person looks.
|
|
492
|
+
|
|
399
493
|
`gitignore-leak --base REF` fail-retries if the ticket's commits add a path the
|
|
400
494
|
workspace's own `.gitignore` excludes — a Builder's `node_modules/`, a build's
|
|
401
495
|
`dist/`, a generated file. `REF` is the work line the ticket is offered to
|
|
402
496
|
(`main`): only what the ticket adds since it left that line is judged, and
|
|
403
497
|
only what is committed, so a path tracked on purpose before the ticket began
|
|
404
498
|
is not blamed on it, and a removal staged but not committed does not pass. The
|
|
405
|
-
report is written for `
|
|
499
|
+
report is written for `ticket.fix`: untrack the paths, or change `.gitignore`
|
|
406
500
|
if tracking one is the point. It passes at `--stage unit`, where no commit
|
|
407
501
|
holds a the unit's work yet.
|
|
408
502
|
|
|
@@ -413,7 +507,7 @@ assembly Gate, with an Authority it judges what was published, so it stops the
|
|
|
413
507
|
fold into the work line, not the push.
|
|
414
508
|
|
|
415
509
|
```yaml
|
|
416
|
-
|
|
510
|
+
ticket:
|
|
417
511
|
gates:
|
|
418
512
|
defaultTimeoutMs: 900000
|
|
419
513
|
gates:
|
|
@@ -448,14 +542,58 @@ the variable holding a token that can read the checks — the same one the
|
|
|
448
542
|
manager's `tokenEnv` names. It has no default on purpose: `GITHUB_TOKEN` is
|
|
449
543
|
what `gh` reads ahead of its own login, so an operator must not export it,
|
|
450
544
|
and a Gate that fell back to it refused only at the first assembly of a real
|
|
451
|
-
run. `--remote`, `--poll-ms` and `--api-base` (GitHub Enterprise) are there too.
|
|
545
|
+
run. `--remote`, `--poll-ms` and `--api-base` (GitHub Enterprise) are there too.
|
|
546
|
+
`--require-check NAME` (repeatable) names a check that must exist and have succeeded:
|
|
547
|
+
the check run (or status context) called exactly `NAME`. The general rule counts a
|
|
548
|
+
`skipped` or `neutral` run as green, so an e2e job skipped for want of its secrets lets the
|
|
549
|
+
line through; a required one that settled so is `fail-blocking`: the work cannot make it run, so a person reads it, not a fix agent. One not created yet is
|
|
550
|
+
waited for like `--require-checks`, and at the ceiling the CI did not answer
|
|
551
|
+
(`fail-blocking`). A name is exact: a matrix job is one check per cell (`e2e (chrome)`), and each is listed. It talks to GitHub, and knows
|
|
452
552
|
nothing about `@bluewombat/manager-github`: a Project may run either one
|
|
453
553
|
without the other.
|
|
454
554
|
|
|
455
|
-
`sensitive-path`, `workspace-changed`, `gitignore-leak` and `ci-green` need a git workspace, which
|
|
555
|
+
`sensitive-path`, `base-diff`, `workspace-changed`, `gitignore-leak` and `ci-green` need a git workspace, which
|
|
456
556
|
is what Isolator makes when WorkLineStable is a git tree. They fail-block on a
|
|
457
557
|
workspace Isolator had to copy.
|
|
458
558
|
|
|
559
|
+
### The feature tests
|
|
560
|
+
|
|
561
|
+
`feature-tests.mjs` runs the tests of a Feature on the assembled Feature line, from a Feature Gate
|
|
562
|
+
(`feature.validate.gates`). Only the scenarios of this Feature run, not the Project's whole suite. It
|
|
563
|
+
knows no stack and starts no application: the Project supplies one command, run through the shell
|
|
564
|
+
in the workspace, and the Gate runs it, compares and judges.
|
|
565
|
+
|
|
566
|
+
| Option | Meaning |
|
|
567
|
+
| --- | --- |
|
|
568
|
+
| `--run CMD` | Required. Runs the tests of the Feature and writes a JUnit XML report. `{ids}` is the ids of the manifest joined with `\|` (a regular expression, `pnpm test --grep "{ids}"`), `{junit}` the path of the report |
|
|
569
|
+
| `--manifest PATH` | The scenarios of the Feature. Default `feature-tests/scenarios.json`: an array of ids, or `{ "scenarios": [ "S1" \| { "id": "S1", "title": "…" } ] }` |
|
|
570
|
+
| `--junit PATH` | Where `--run` writes its report. Default `feature-tests/results/junit.xml`. Removed before every run, so an old report is never read |
|
|
571
|
+
| `--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 5 s |
|
|
572
|
+
|
|
573
|
+
A scenario is a manifest id, and every test carries its id in its name (`S2 pays by card`). The
|
|
574
|
+
comparison is exact: an id with no test, a test with no manifest id, and a test the runner
|
|
575
|
+
skipped are failures. There is no retry. The verdict:
|
|
576
|
+
|
|
577
|
+
| Verdict | When |
|
|
578
|
+
| --- | --- |
|
|
579
|
+
| `pass` | Every scenario ran and passed. The report says how many and which |
|
|
580
|
+
| `fail-retryable` | Red: the report has, per scenario, its id, the test, the step (`at`) and the message. Whether the test or the application is wrong is for the caller to say: the feature-test Ticket's author reads the report to fix its tests, and the Feature's fix repairs the application with the tests frozen. Also a runner that crashed before writing a report, and tests that carry no id of the manifest |
|
|
581
|
+
| `fail-blocking` | What no change to the application fixes: a missing, empty or malformed manifest, a run that outlives the ceiling, a run that exits 0 and writes no report |
|
|
582
|
+
|
|
583
|
+
The Project's `.gitignore` excludes the results folder (`feature-tests/results/`): a Gate that guards
|
|
584
|
+
the folder (`sensitive-path feature-tests/**` in `feature.fix.gates`) would read the report as a change.
|
|
585
|
+
|
|
586
|
+
```yaml
|
|
587
|
+
- id: feature-tests
|
|
588
|
+
skipWhen: skip-feature-test # Host's: not run for a Feature that carries the label
|
|
589
|
+
timeoutMs: 720000
|
|
590
|
+
argv:
|
|
591
|
+
- node
|
|
592
|
+
- ./node_modules/@bluewombat/slots/gates/feature-tests.mjs
|
|
593
|
+
- --run
|
|
594
|
+
- pnpm test --grep "{ids}" --reporter=junit --outputFile={junit}
|
|
595
|
+
```
|
|
596
|
+
|
|
459
597
|
## Publishers
|
|
460
598
|
|
|
461
599
|
The slot that puts an assembled ticket where the Authority can read it. It runs
|
|
@@ -538,8 +676,9 @@ type in JSDoc.
|
|
|
538
676
|
```
|
|
539
677
|
README.md this file
|
|
540
678
|
agents/ vendor CLIs: filled prompt in, serialized run out
|
|
541
|
-
builders/ first-pass and repair roles for the unit of work
|
|
542
|
-
|
|
679
|
+
builders/ first-pass and repair roles for the unit of work, and the feature-test Ticket's
|
|
680
|
+
ticket/ fix and validate for an assembled ticket
|
|
681
|
+
feature/ fix for a whole feature
|
|
543
682
|
lib/ what the shipped templates say, shared by the roles
|
|
544
683
|
gates/ checks on the workspace, and on the work line
|
|
545
684
|
messages/ what a commit says: a rule, or an agent under the team's guideline
|
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
|
|
package/agents/claude.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { extrasFromClaude } from "./claude-extras.mjs";
|
|
|
30
30
|
// --agent-home / --api-key-env recognised, then refused. Claude is not
|
|
31
31
|
// isolated yet; starting would keep the operator's config
|
|
32
32
|
// --plugin-dir DIR a plugin folder, repeatable (see agent-plugins.mjs)
|
|
33
|
-
// --profile / --plugins-out / --deck-bin recognised, then refused for the
|
|
33
|
+
// --app-deck-profile / --plugins-out / --deck-bin recognised, then refused for the
|
|
34
34
|
// same reason: Claude would also load the operator's
|
|
35
35
|
// and the project's own plugins, so a role's profile
|
|
36
36
|
// would not be what it gets
|
|
@@ -64,12 +64,12 @@ if (
|
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
if (
|
|
67
|
-
textOption(options, "
|
|
67
|
+
textOption(options, "appDeckProfile") !== undefined ||
|
|
68
68
|
textOption(options, "pluginsOut") !== undefined ||
|
|
69
69
|
textOption(options, "deckBin") !== undefined
|
|
70
70
|
) {
|
|
71
71
|
refuse(
|
|
72
|
-
"Claude Code does not isolate the operator home yet, so a deck profile would not be all it loads. Remove --profile; the vendor was not started.",
|
|
72
|
+
"Claude Code does not isolate the operator home yet, so a deck profile would not be all it loads. Remove --app-deck-profile; the vendor was not started.",
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
package/agents/cursor.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import { extrasFromCursor } from "./cursor-extras.mjs";
|
|
|
36
36
|
// --deny-shell NAME with --agent-home: also refuse this command to the
|
|
37
37
|
// vendor (`gh`, `vercel`…). Repeatable. ssh and scp are
|
|
38
38
|
// always refused
|
|
39
|
-
// --plugin-dir / --profile / --plugins-out / --deck-bin plugin folders,
|
|
39
|
+
// --plugin-dir / --app-deck-profile / --plugins-out / --deck-bin plugin folders,
|
|
40
40
|
// see agent-plugins.mjs
|
|
41
41
|
// --id / --attempt / --context how a transcript is filed
|
|
42
42
|
|