@bluewombat/slots 0.8.0 → 0.9.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 +31 -76
- package/assembly/fix.mjs +3 -3
- package/assembly/validate.mjs +7 -7
- package/builders/producer.mjs +3 -7
- package/builders/repair.mjs +1 -4
- package/gates/gitignore-leak.mjs +8 -8
- package/lib/fill.mjs +0 -6
- package/messages/conventional.mjs +1 -1
- package/messages/git-producer.mjs +2 -2
- package/package.json +4 -7
- package/publishers/git.mjs +1 -1
- package/refreshers/git.mjs +17 -1
- 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,7 +50,6 @@ 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
|
|
|
@@ -60,21 +57,19 @@ these all use it. How Host wires a slot into a run:
|
|
|
60
57
|
| ----------------------------- | --------- | ------------------------------------------------------------------------------------------ |
|
|
61
58
|
| `agents/cursor.mjs` | Agent | Runs Cursor CLI with a filled prompt |
|
|
62
59
|
| `agents/claude.mjs` | Agent | Runs Claude Code with a filled prompt |
|
|
63
|
-
| `builders/producer.mjs` | Builder | First pass of
|
|
64
|
-
| `builders/repair.mjs` | Builder | A Gate refused the
|
|
65
|
-
| `assembly/fix.mjs` | Builder | A judgement refused the assembled
|
|
66
|
-
| `assembly/validate.mjs` | Builder | Read-only review of the assembled
|
|
60
|
+
| `builders/producer.mjs` | Builder | First pass of the unit of work: fill `{{task}}`, then spawn the agent |
|
|
61
|
+
| `builders/repair.mjs` | Builder | A Gate refused the unit: fill `{{task}}` and `{{report}}` |
|
|
62
|
+
| `assembly/fix.mjs` | Builder | A judgement refused the assembled work: fill `{{task}}` and `{{report}}` |
|
|
63
|
+
| `assembly/validate.mjs` | Builder | Read-only review of the assembled work: fill `{{task}}`; answers `MASON_VERDICT: VALIDATED` or `MASON_VERDICT: REFUSED: …` |
|
|
67
64
|
| `gates/workspace-changed.mjs` | Gate | The Attempt left an uncommitted change |
|
|
68
65
|
| `gates/parent-clean.mjs` | Gate | Isolator's Parent working files did not move |
|
|
69
66
|
| `gates/sensitive-path.mjs` | Gate | No path matching a glob was touched |
|
|
70
|
-
| `gates/gitignore-leak.mjs` | Gate | The
|
|
67
|
+
| `gates/gitignore-leak.mjs` | Gate | The ticket's commits add nothing its own `.gitignore` excludes |
|
|
71
68
|
| `gates/ci-green.mjs` | Gate | The work line's own checks came back green |
|
|
72
|
-
| `
|
|
73
|
-
| `publishers/git.mjs` | Publisher | Pushes the feature's branch to the work line's own remote |
|
|
69
|
+
| `publishers/git.mjs` | Publisher | Pushes the ticket's branch to the work line's own remote |
|
|
74
70
|
| `refreshers/git.mjs` | Refresher | Fast-forwards the work line copy onto what the Authority holds |
|
|
75
|
-
| `
|
|
76
|
-
| `messages/
|
|
77
|
-
| `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the feature's subject and body under `--rules-file` |
|
|
71
|
+
| `messages/conventional.mjs` | Message | `<type>: <title>` for the ticket's fold, no agent; `--scope`, `--type` |
|
|
72
|
+
| `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the ticket's subject and body under `--rules-file` |
|
|
78
73
|
|
|
79
74
|
## Agents
|
|
80
75
|
|
|
@@ -237,18 +232,16 @@ builder:
|
|
|
237
232
|
```
|
|
238
233
|
|
|
239
234
|
A path that names a file next to the config resolves against the config; any
|
|
240
|
-
other relative path resolves inside the
|
|
235
|
+
other relative path resolves inside the workspace, so a template
|
|
241
236
|
committed in the Project travels with it.
|
|
242
237
|
|
|
243
238
|
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.
|
|
239
|
+
never places, is `fail-blocking`.
|
|
247
240
|
|
|
248
241
|
| Role | Required | Also fills |
|
|
249
242
|
| ------------------- | ------------------------------------- | ---------------------------------------------------------- |
|
|
250
|
-
| `builders/producer` | `{{task}}`, `{{rules}}` | `{{history}}`, `{{
|
|
251
|
-
| `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{
|
|
243
|
+
| `builders/producer` | `{{task}}`, `{{rules}}` | `{{history}}`, `{{id}}`, `{{attempt}}` |
|
|
244
|
+
| `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
252
245
|
| `assembly/fix` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{id}}`, `{{attempt}}` |
|
|
253
246
|
| `assembly/validate` | `{{task}}`, `{{rules}}` | `{{id}}`, `{{attempt}}` |
|
|
254
247
|
|
|
@@ -258,7 +251,7 @@ belongs to the repair command. A repair or fix command without `--report` is the
|
|
|
258
251
|
`{{history}}` is what the Task went through before this Attempt, oldest first (the
|
|
259
252
|
refusal it came with, each Attempt, each Gate's answer; see Implementer's SPECS §
|
|
260
253
|
History). It is empty on a first Attempt and vanishes whole then, so a template
|
|
261
|
-
that does not place it
|
|
254
|
+
that does not place it simply omits it.
|
|
262
255
|
|
|
263
256
|
### The way out
|
|
264
257
|
|
|
@@ -285,10 +278,9 @@ a run spent on nothing. A command that arrives with a report and no
|
|
|
285
278
|
|
|
286
279
|
### The fix prompt
|
|
287
280
|
|
|
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.
|
|
281
|
+
A judgement of the assembled work that refused it is not a retry of the unit.
|
|
282
|
+
The unit already passed. `assembly/fix.mjs` tells the agent to change what the
|
|
283
|
+
refusal names and leave the rest. Same placeholders as repair.
|
|
292
284
|
|
|
293
285
|
### The validate prompt
|
|
294
286
|
|
|
@@ -319,7 +311,7 @@ nothing pushed. A producing template must place it — one that leaves it out is
|
|
|
319
311
|
Gates are what says no, which is slower, reaches the tracker, and spends a run.
|
|
320
312
|
|
|
321
313
|
`--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
|
|
314
|
+
matter of taste: the Gates read the working tree of the workspace, so an
|
|
323
315
|
agent that commits, stashes or writes elsewhere leaves them nothing to see and
|
|
324
316
|
the pass is refused for having done nothing. Replacing the rules does not lift
|
|
325
317
|
that; it only stops saying it out loud.
|
|
@@ -335,7 +327,7 @@ loop that misbehaves cannot be read back, only guessed at.
|
|
|
335
327
|
as sent, stdout, stderr, how long it took, and — when the agent wrapper passed
|
|
336
328
|
them — `skills`, `usage` and what the tools did (`unknown`, `(none)`, or the
|
|
337
329
|
values). Files are
|
|
338
|
-
filed under the
|
|
330
|
+
filed under the Ticket the Task belongs to, named for the Subtask and the
|
|
339
331
|
Attempt. It belongs on the **agent** command, after `--`.
|
|
340
332
|
|
|
341
333
|
```yaml
|
|
@@ -404,15 +396,15 @@ the workspace — tracked paths, and new ones `.gitignore` does not exclude —
|
|
|
404
396
|
what an install left under an ignored directory (a dependency's own `.github/`)
|
|
405
397
|
is not a change to guard.
|
|
406
398
|
|
|
407
|
-
`gitignore-leak --base REF` fail-retries if the
|
|
399
|
+
`gitignore-leak --base REF` fail-retries if the ticket's commits add a path the
|
|
408
400
|
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
|
|
401
|
+
`dist/`, a generated file. `REF` is the work line the ticket is offered to
|
|
402
|
+
(`main`): only what the ticket adds since it left that line is judged, and
|
|
403
|
+
only what is committed, so a path tracked on purpose before the ticket began
|
|
412
404
|
is not blamed on it, and a removal staged but not committed does not pass. The
|
|
413
405
|
report is written for `assembly.fix`: untrack the paths, or change `.gitignore`
|
|
414
406
|
if tracking one is the point. It passes at `--stage unit`, where no commit
|
|
415
|
-
holds a
|
|
407
|
+
holds a the unit's work yet.
|
|
416
408
|
|
|
417
409
|
It is a second line, not the first: the `isolation-git` fold already keeps an
|
|
418
410
|
ignored path out of the commits it makes. It catches a leak that came some
|
|
@@ -464,47 +456,11 @@ without the other.
|
|
|
464
456
|
is what Isolator makes when WorkLineStable is a git tree. They fail-block on a
|
|
465
457
|
workspace Isolator had to copy.
|
|
466
458
|
|
|
467
|
-
## Planners
|
|
468
|
-
|
|
469
|
-
`one-subtask.mjs` is the bootstrap Planner: one Subtask that is the
|
|
470
|
-
FeatureStandard itself. It is what `mason init` writes into a fresh config, and
|
|
471
|
-
it takes no option.
|
|
472
|
-
|
|
473
|
-
`planners/producer.mjs` hands the split to an agent CLI. It reads the project
|
|
474
|
-
before answering and writes nothing into it. `--prompt-file` and `--read` belong
|
|
475
|
-
on the role; the vendor is named after `--`, the same way a Builder names it.
|
|
476
|
-
|
|
477
|
-
```yaml
|
|
478
|
-
planner:
|
|
479
|
-
cmd:
|
|
480
|
-
- node
|
|
481
|
-
- ./node_modules/@bluewombat/slots/planners/producer.mjs
|
|
482
|
-
- --read
|
|
483
|
-
- ./work-line-stable
|
|
484
|
-
- --
|
|
485
|
-
- node
|
|
486
|
-
- ./node_modules/@bluewombat/slots/agents/cursor.mjs
|
|
487
|
-
timeoutMs: 600000
|
|
488
|
-
```
|
|
489
|
-
|
|
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
459
|
## Publishers
|
|
504
460
|
|
|
505
|
-
The slot that puts an assembled
|
|
461
|
+
The slot that puts an assembled ticket where the Authority can read it. It runs
|
|
506
462
|
**in the work line**, and mason appends `--id`, `--ref` — the name Isolator gave
|
|
507
|
-
the
|
|
463
|
+
the ticket — and `--target`, the work line it is offered to.
|
|
508
464
|
|
|
509
465
|
`git.mjs` pushes that name to the work line's own remote. A plain push, no force
|
|
510
466
|
of any kind: Integrator only ever adds history, so republishing after a refusal
|
|
@@ -519,12 +475,12 @@ read another. It comes back when that half is a slot too.
|
|
|
519
475
|
The answer is one JSON object on stdout, like every slot:
|
|
520
476
|
|
|
521
477
|
```json
|
|
522
|
-
{ "ref": "issue/
|
|
478
|
+
{ "ref": "issue/ticket-42" }
|
|
523
479
|
{ "outcome": "refused", "reason": "the remote rejected the push" }
|
|
524
480
|
```
|
|
525
481
|
|
|
526
482
|
The reference is opaque above this line — a branch, a directory, a URL. Whatever
|
|
527
|
-
the Publisher answers is what the
|
|
483
|
+
the Publisher answers is what the TicketTracker is told to judge, so a Project
|
|
528
484
|
that places its work some other way writes its own Publisher and changes nothing
|
|
529
485
|
else. A refusal stops the Submission and travels back to the tracker; a slot that
|
|
530
486
|
could not run at all exits non-zero and says why on stderr.
|
|
@@ -582,12 +538,11 @@ type in JSDoc.
|
|
|
582
538
|
```
|
|
583
539
|
README.md this file
|
|
584
540
|
agents/ vendor CLIs: filled prompt in, serialized run out
|
|
585
|
-
builders/ first-pass and repair roles for
|
|
586
|
-
assembly/ the same two roles for the assembled
|
|
541
|
+
builders/ first-pass and repair roles for the unit of work
|
|
542
|
+
assembly/ the same two roles for the assembled work
|
|
587
543
|
lib/ what the shipped templates say, shared by the roles
|
|
588
544
|
gates/ checks on the workspace, and on the work line
|
|
589
|
-
planners/ FeatureStandard in, a Plan out
|
|
590
545
|
messages/ what a commit says: a rule, or an agent under the team's guideline
|
|
591
546
|
test/ one suite per kind; each spawns the real command
|
|
592
|
-
fixtures/ fake agent CLIs the Builder
|
|
547
|
+
fixtures/ fake agent CLIs the Builder suites run against
|
|
593
548
|
```
|
package/assembly/fix.mjs
CHANGED
|
@@ -19,9 +19,9 @@ const NAME = "assembly fix";
|
|
|
19
19
|
|
|
20
20
|
const DEFAULT_TEMPLATE = `You are a coding agent working unattended in a checkout of the project.
|
|
21
21
|
|
|
22
|
-
This directory is the assembled
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
This directory is the assembled work. It passed the checks made on the unit
|
|
23
|
+
itself. An earlier judgement of the whole refused it. Do not redo the work.
|
|
24
|
+
Fix what the refusal names.
|
|
25
25
|
|
|
26
26
|
Refused by: {{refused_by}}
|
|
27
27
|
|
package/assembly/validate.mjs
CHANGED
|
@@ -19,11 +19,11 @@ const NAME = "assembly validate";
|
|
|
19
19
|
|
|
20
20
|
const DEFAULT_TEMPLATE = `You are a coding agent working unattended in a checkout of the project.
|
|
21
21
|
|
|
22
|
-
This directory is the assembled
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
This directory is the assembled work. It has already passed the checks made on
|
|
23
|
+
the unit itself. Your job now is different: read the diff it carries against
|
|
24
|
+
the work line it will land on, and judge whether the WHOLE meets what was
|
|
25
|
+
asked. Do not redo the work. Do not change anything — this is a
|
|
26
|
+
read-only review, not a repair.
|
|
27
27
|
|
|
28
28
|
## What was asked
|
|
29
29
|
|
|
@@ -34,7 +34,7 @@ anything — this is a read-only review, not a repair.
|
|
|
34
34
|
End your final message with exactly one line in this form, and nothing after
|
|
35
35
|
it:
|
|
36
36
|
|
|
37
|
-
- \`MASON_VERDICT: VALIDATED\` — the assembled
|
|
37
|
+
- \`MASON_VERDICT: VALIDATED\` — the assembled ticket meets what was asked.
|
|
38
38
|
- \`MASON_VERDICT: REFUSED: <one paragraph>\` — it does not, and the paragraph
|
|
39
39
|
says what is wrong, specifically enough for another agent to fix it without
|
|
40
40
|
redoing your review. Keep the whole paragraph on that one line.
|
|
@@ -162,7 +162,7 @@ function answer(run, bin, name) {
|
|
|
162
162
|
"fail-retryable",
|
|
163
163
|
why.length > 0
|
|
164
164
|
? why
|
|
165
|
-
: "assembly.validate refused the assembled
|
|
165
|
+
: "assembly.validate refused the assembled ticket, with no reason given.",
|
|
166
166
|
);
|
|
167
167
|
process.exit(1);
|
|
168
168
|
}
|
package/builders/producer.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
textOption,
|
|
14
14
|
transcriptArgs,
|
|
15
15
|
} from "@bluewombat/slot-kit";
|
|
16
|
-
import { attemptOf
|
|
16
|
+
import { attemptOf } from "../lib/fill.mjs";
|
|
17
17
|
|
|
18
18
|
const NAME = "builder producer";
|
|
19
19
|
|
|
@@ -22,11 +22,8 @@ const DEFAULT_TEMPLATE = `You are a coding agent working unattended in a checkou
|
|
|
22
22
|
Task:
|
|
23
23
|
{{task}}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
This checkout already includes any earlier subtasks that were folded into the
|
|
28
|
-
feature. Do not rewrite that work. Add or change only what this task names, and
|
|
29
|
-
leave the project's own checks green on the whole tree.
|
|
25
|
+
This checkout is the project as it stands. Add or change only what this task
|
|
26
|
+
names, and leave the project's own checks green on the whole tree.
|
|
30
27
|
|
|
31
28
|
{{history}}
|
|
32
29
|
|
|
@@ -82,7 +79,6 @@ const rendered = fillPrompt(
|
|
|
82
79
|
{
|
|
83
80
|
task: intention,
|
|
84
81
|
history: (take(process.argv, "--history") ?? "").trim(),
|
|
85
|
-
done_when: doneWhen(take(process.argv, "--definition-of-done")),
|
|
86
82
|
rules: rules.value.trim(),
|
|
87
83
|
id: take(process.argv, "--id") ?? "",
|
|
88
84
|
attempt: String(attemptOf(process.argv)),
|
package/builders/repair.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
textOption,
|
|
14
14
|
transcriptArgs,
|
|
15
15
|
} from "@bluewombat/slot-kit";
|
|
16
|
-
import { attemptOf
|
|
16
|
+
import { attemptOf } from "../lib/fill.mjs";
|
|
17
17
|
|
|
18
18
|
const NAME = "builder repair";
|
|
19
19
|
|
|
@@ -38,8 +38,6 @@ This was asked in the first place, and still holds:
|
|
|
38
38
|
|
|
39
39
|
{{task}}
|
|
40
40
|
|
|
41
|
-
{{done_when}}
|
|
42
|
-
|
|
43
41
|
{{history}}
|
|
44
42
|
|
|
45
43
|
## When you cannot act
|
|
@@ -96,7 +94,6 @@ const rendered = fillPrompt(
|
|
|
96
94
|
report,
|
|
97
95
|
refused_by:
|
|
98
96
|
refusedBy.length > 0 ? refusedBy : "the producer of the last attempt, which did not finish",
|
|
99
|
-
done_when: doneWhen(take(process.argv, "--definition-of-done")),
|
|
100
97
|
rules: rules.value.trim(),
|
|
101
98
|
id: take(process.argv, "--id") ?? "",
|
|
102
99
|
attempt: String(attemptOf(process.argv)),
|
package/gates/gitignore-leak.mjs
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { emitVerdict, GATE_FLAGS, ownArgv, stageOf } from "@bluewombat/slot-kit";
|
|
4
4
|
|
|
5
|
-
// Fail-retryable when the
|
|
5
|
+
// Fail-retryable when the ticket's commits add a path the workspace's own
|
|
6
6
|
// .gitignore excludes — a Builder's node_modules/, a build's dist/, a generated
|
|
7
7
|
// file — so it would ship in the Submission.
|
|
8
8
|
//
|
|
9
|
-
// --base REF (required): the work line the
|
|
10
|
-
//
|
|
11
|
-
// tracked on purpose before the
|
|
9
|
+
// --base REF (required): the work line the ticket is offered to. Only what the
|
|
10
|
+
// ticket adds since it left that line is judged, in what is committed: a path
|
|
11
|
+
// tracked on purpose before the ticket began is not this ticket's leak, and
|
|
12
12
|
// a removal staged but not committed has not removed anything yet.
|
|
13
13
|
//
|
|
14
|
-
// A unit only: nothing.
|
|
15
|
-
//
|
|
14
|
+
// A unit only: nothing. The work is uncommitted until a fold records it, so
|
|
15
|
+
// there is no commit of its own here to judge.
|
|
16
16
|
|
|
17
17
|
const cwd = process.cwd();
|
|
18
18
|
const tokens = ownArgv(process.argv, GATE_FLAGS);
|
|
@@ -37,7 +37,7 @@ for (let i = 0; i < tokens.length; i++) {
|
|
|
37
37
|
if (base === undefined) {
|
|
38
38
|
emitVerdict(
|
|
39
39
|
"fail-blocking",
|
|
40
|
-
"gitignore-leak needs --base <ref>: the work line the
|
|
40
|
+
"gitignore-leak needs --base <ref>: the work line the ticket is offered to.",
|
|
41
41
|
);
|
|
42
42
|
process.exit(0);
|
|
43
43
|
}
|
|
@@ -105,7 +105,7 @@ emitVerdict(
|
|
|
105
105
|
[
|
|
106
106
|
`These paths are committed on top of ${base}, and this workspace's .gitignore excludes them.`,
|
|
107
107
|
"Take them out of the commit with git rm -r --cached <path> and keep the files. If the",
|
|
108
|
-
"
|
|
108
|
+
"ticket does mean to track one, change .gitignore so it no longer excludes it:",
|
|
109
109
|
...shown,
|
|
110
110
|
...(rest > 0 ? [`… and ${rest} more`] : []),
|
|
111
111
|
].join("\n"),
|
package/lib/fill.mjs
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { take } from "@bluewombat/slot-kit";
|
|
6
6
|
|
|
7
|
-
/** @param {string | undefined} raw */
|
|
8
|
-
export function doneWhen(raw) {
|
|
9
|
-
const done = (raw ?? "").trim();
|
|
10
|
-
return done.length > 0 ? `Done when:\n${done}` : "";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
7
|
/** @param {readonly string[]} argv */
|
|
14
8
|
export function attemptOf(argv) {
|
|
15
9
|
const parsed = Number.parseInt(take(argv, "--attempt") ?? "", 10);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { emitMessage, MESSAGE_FLAGS, ownArgv, take, writeDiagnostic } from "@bluewombat/slot-kit";
|
|
3
3
|
|
|
4
|
-
// The
|
|
4
|
+
// The ticket's commit in the Conventional Commits shape, from the issue's
|
|
5
5
|
// title and body and with no agent: `<type>: <title>`, the body as written.
|
|
6
6
|
//
|
|
7
7
|
// The type is read off the title's first word — a Project that wants a
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
writeDiagnostic,
|
|
17
17
|
} from "@bluewombat/slot-kit";
|
|
18
18
|
|
|
19
|
-
// The
|
|
19
|
+
// The ticket's commit message — and the pull request's description with it
|
|
20
20
|
// — written by an agent that read the diff, under the Project's own rules.
|
|
21
|
-
// Asked once per
|
|
21
|
+
// Asked once per ticket. A role slot like the builders: it fills a template
|
|
22
22
|
// and hands it to the agent after `--`; the agent answers in a file this slot
|
|
23
23
|
// reads back. `--prompt-file` replaces the template, `--rules-file` the rules
|
|
24
24
|
// — that is where a team's commit guideline goes.
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bluewombat/slots",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Shipped mason slots:
|
|
5
|
+
"description": "Shipped mason slots: Builders, Gates, publishers, and refreshers a Project names in its config.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mason",
|
|
8
8
|
"bluewombat",
|
|
9
9
|
"gate",
|
|
10
|
-
"builder"
|
|
11
|
-
"planner"
|
|
10
|
+
"builder"
|
|
12
11
|
],
|
|
13
12
|
"license": "MIT",
|
|
14
13
|
"homepage": "https://github.com/tilap/bluewombat/tree/main/packages/plugins/slots#readme",
|
|
@@ -19,7 +18,6 @@
|
|
|
19
18
|
"directory": "packages/plugins/slots"
|
|
20
19
|
},
|
|
21
20
|
"exports": {
|
|
22
|
-
"./planners/*": "./planners/*",
|
|
23
21
|
"./builders/*": "./builders/*",
|
|
24
22
|
"./assembly/*": "./assembly/*",
|
|
25
23
|
"./agents/*": "./agents/*",
|
|
@@ -29,7 +27,6 @@
|
|
|
29
27
|
"./messages/*": "./messages/*"
|
|
30
28
|
},
|
|
31
29
|
"files": [
|
|
32
|
-
"planners",
|
|
33
30
|
"builders",
|
|
34
31
|
"assembly",
|
|
35
32
|
"agents",
|
|
@@ -53,7 +50,7 @@
|
|
|
53
50
|
"test": "tsx --test $(find test -name '*.test.ts')"
|
|
54
51
|
},
|
|
55
52
|
"dependencies": {
|
|
56
|
-
"@bluewombat/slot-kit": "^0.
|
|
53
|
+
"@bluewombat/slot-kit": "^0.9.0",
|
|
57
54
|
"@pinkpuma/deck": "1.0.0"
|
|
58
55
|
}
|
|
59
56
|
}
|
package/publishers/git.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
writeDiagnostic,
|
|
12
12
|
} from "@bluewombat/slot-kit";
|
|
13
13
|
|
|
14
|
-
// Put the
|
|
14
|
+
// Put the ticket's work where the Authority can read it, by pushing the name
|
|
15
15
|
// Isolator gave it to the work line's own remote.
|
|
16
16
|
//
|
|
17
17
|
// A plain push, and no force of any kind. Integrator only ever adds history, so
|
package/refreshers/git.mjs
CHANGED
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
// would make the next Isolation start from a version that no longer exists, so
|
|
17
17
|
// this runs before anything else in a pass.
|
|
18
18
|
//
|
|
19
|
+
// The copy moves onto the target line first: a Feature's Tickets are cut from
|
|
20
|
+
// and folded into that Feature's line, not the final branch.
|
|
21
|
+
//
|
|
19
22
|
// Fast-forward only: this system never rewrites that line. A copy that cannot
|
|
20
23
|
// fast-forward has been written to by somebody else, and that is a person's
|
|
21
24
|
// problem, not a merge to attempt.
|
|
@@ -41,7 +44,8 @@ if (!existsSync(join(cwd, ".git"))) {
|
|
|
41
44
|
process.exit(0);
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
// An explicit refspec: a single-branch clone only knows its own branch.
|
|
48
|
+
const fetched = git(["fetch", "origin", `+refs/heads/${target}:refs/remotes/origin/${target}`]);
|
|
45
49
|
if (fetched.status !== 0) {
|
|
46
50
|
emitRefreshRefusal(
|
|
47
51
|
`Cannot fetch origin/${target} from ${cwd}: ${fetched.stderr.trim().slice(-500)}`,
|
|
@@ -49,6 +53,18 @@ if (fetched.status !== 0) {
|
|
|
49
53
|
process.exit(0);
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
const current = git(["symbolic-ref", "--short", "HEAD"]);
|
|
57
|
+
if (current.status !== 0 || current.stdout.trim() !== target) {
|
|
58
|
+
const exists = git(["rev-parse", "--verify", "--quiet", `refs/heads/${target}`]).status === 0;
|
|
59
|
+
const switched = git(
|
|
60
|
+
exists ? ["checkout", target] : ["checkout", "-b", target, `origin/${target}`],
|
|
61
|
+
);
|
|
62
|
+
if (switched.status !== 0) {
|
|
63
|
+
emitRefreshRefusal(`Cannot move ${cwd} onto ${target}: ${switched.stderr.trim().slice(-500)}`);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
const merged = git(["merge", "--ff-only", `origin/${target}`]);
|
|
53
69
|
if (merged.status !== 0) {
|
|
54
70
|
// The one way a copy stops fast-forwarding is a commit made into it that
|
package/planners/one-subtask.mjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Bootstrap Planner: one Subtask that is the FeatureStandard itself.
|
|
4
|
-
* FeatureBreakdown appends --intention, --key, --max-units.
|
|
5
|
-
*/
|
|
6
|
-
import { emitPlan, emitRefusal, take } from "@bluewombat/slot-kit";
|
|
7
|
-
|
|
8
|
-
const intention = (take(process.argv, "--intention") ?? "").trim();
|
|
9
|
-
if (intention.length === 0) {
|
|
10
|
-
emitRefusal("The FeatureStandard has no intention to turn into a Subtask.");
|
|
11
|
-
process.exit(0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
emitPlan([
|
|
15
|
-
{
|
|
16
|
-
id: "A",
|
|
17
|
-
intention,
|
|
18
|
-
definition_of_done: "The intention is realized in the workspace.",
|
|
19
|
-
depends_on: [],
|
|
20
|
-
},
|
|
21
|
-
]);
|
package/planners/producer.mjs
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
checkPlan,
|
|
7
|
-
emitPlan,
|
|
8
|
-
emitRefusal,
|
|
9
|
-
fillPrompt,
|
|
10
|
-
loadPrompt,
|
|
11
|
-
PLANNER_FLAGS,
|
|
12
|
-
parseRole,
|
|
13
|
-
spawnFilled,
|
|
14
|
-
take,
|
|
15
|
-
textOption,
|
|
16
|
-
transcriptArgs,
|
|
17
|
-
writeDiagnostic,
|
|
18
|
-
} from "@bluewombat/slot-kit";
|
|
19
|
-
|
|
20
|
-
const NAME = "planner producer";
|
|
21
|
-
|
|
22
|
-
const DEFAULT_TEMPLATE = `You are splitting one piece of work into an ordered list of subtasks.
|
|
23
|
-
|
|
24
|
-
You are not writing any of it. Do not modify anything in this directory. Read as
|
|
25
|
-
much of it as you need first: the split has to fit the project as it is. Do not
|
|
26
|
-
invent conventions you can discover from the repository.
|
|
27
|
-
|
|
28
|
-
The work:
|
|
29
|
-
{{intention}}
|
|
30
|
-
|
|
31
|
-
Treat that as the source of truth for the desired outcome, not as a finished
|
|
32
|
-
specification. Its wording may be incomplete. Separate what it explicitly
|
|
33
|
-
requires from what the repository or ordinary engineering practice implies.
|
|
34
|
-
Do not invent requirements to make the plan look complete.
|
|
35
|
-
|
|
36
|
-
How the subtasks will be run, which is what makes a good split:
|
|
37
|
-
|
|
38
|
-
- One at a time, never in parallel. Each runs in its own copy of this project,
|
|
39
|
-
and its result is folded back before the next one starts. So a later subtask
|
|
40
|
-
sees the earlier ones' work. If two slices would both change the same path
|
|
41
|
-
(a README, an index or barrel file, a changelog, package.json, or any other
|
|
42
|
-
shared file), make them **one** subtask, or give the second a \`depends_on\` on
|
|
43
|
-
the first and let **only** that second one write the shared path. Do not invent
|
|
44
|
-
a docs-only subtask unless the work asked for documentation.
|
|
45
|
-
- Every subtask is checked by the project's own checks — its tests, its lint —
|
|
46
|
-
on its own. **A subtask must leave the project working.** This is the rule that
|
|
47
|
-
decides your split: never cut by layer. "Add the types", then "add the code",
|
|
48
|
-
then "add the tests" is wrong, because the first two leave the project broken.
|
|
49
|
-
Cut by thin slices that each work end to end. Tests belong in the subtask that
|
|
50
|
-
introduces or changes the behavior they verify.
|
|
51
|
-
- Each subtask is given to an agent with no memory of you and no memory of the
|
|
52
|
-
others. Its intention has to stand alone: no "as discussed above", no knowledge
|
|
53
|
-
of this planning process, no requirement that lives only in the original work
|
|
54
|
-
description. Copy into the subtask everything its agent needs. A pointer at
|
|
55
|
-
existing code is fine if it names the behavior or location clearly enough to
|
|
56
|
-
investigate.
|
|
57
|
-
- Write each intention the way a good commit message is written: one headline
|
|
58
|
-
of at most 72 characters in the imperative ("Add titleCase in
|
|
59
|
-
src/title-case.js"), a blank line, then everything the agent needs. The
|
|
60
|
-
headline is what people see: in the plan on the tracker, in the change list
|
|
61
|
-
of the submission, on the commit.
|
|
62
|
-
- Its definition of done has to be checkable by someone who cannot read your
|
|
63
|
-
mind, and preferably by a test. It describes this subtask's observable result,
|
|
64
|
-
not the whole feature and not an intention.
|
|
65
|
-
- \`depends_on\` means "cannot start before this one is done", and nothing else.
|
|
66
|
-
Sharing a file is a reason not to start side by side: pack those slices into
|
|
67
|
-
one subtask, or make the later one depend on the earlier. The graph describes
|
|
68
|
-
what must land before a merge is safe, not the order you happened to think in.
|
|
69
|
-
Chaining every subtask to the one before it when their files do not overlap
|
|
70
|
-
hides which ones were actually independent.
|
|
71
|
-
|
|
72
|
-
You are choosing between two costs, and both are real.
|
|
73
|
-
|
|
74
|
-
Splitting too finely costs a full agent run per slice, and each slice pays the
|
|
75
|
-
project's whole check sequence again. Do not create a subtask solely to read
|
|
76
|
-
code, decide what to implement, create folders, rename things, add comments,
|
|
77
|
-
write documentation (unless the work asked for it), clean up, or refactor
|
|
78
|
-
speculatively.
|
|
79
|
-
|
|
80
|
-
Not splitting enough costs more, and less visibly. One subtask is one agent run,
|
|
81
|
-
with one budget of attempts and one clock: a subtask too large for that exhausts
|
|
82
|
-
them and the whole piece of work stops. And a subtask that fails goes back to
|
|
83
|
-
nothing, while a subtask that succeeded is never redone. Ten modules in one
|
|
84
|
-
subtask means ten modules redone when the tenth goes wrong.
|
|
85
|
-
|
|
86
|
-
Prefer adapting what the project already does over introducing a parallel
|
|
87
|
-
mechanism. Do not propose an architectural change merely because it might be
|
|
88
|
-
cleaner.
|
|
89
|
-
|
|
90
|
-
So: as few subtasks as the work allows, and none larger than what one competent
|
|
91
|
-
engineer would finish in a single sitting without losing the thread. At most
|
|
92
|
-
{{max_units}}.
|
|
93
|
-
|
|
94
|
-
Write your answer as JSON, and nothing else, to this file:
|
|
95
|
-
|
|
96
|
-
{{out}}
|
|
97
|
-
|
|
98
|
-
It looks like this:
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
"subtasks": [
|
|
102
|
-
{ "id": "s1", "intention": "Headline\\n\\nEverything the agent needs.", "definition_of_done": "...", "depends_on": [] },
|
|
103
|
-
{ "id": "s2", "intention": "Headline\\n\\n...", "definition_of_done": "...", "depends_on": ["s1"] }
|
|
104
|
-
]
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
If the work cannot be split as it is written — contradictory outcomes, a
|
|
108
|
-
critical requirement so ambiguous that the implementation would change with
|
|
109
|
-
the reading, or too little to determine what should be built — write this
|
|
110
|
-
instead, and do not invent a plan to fill the silence. Do not refuse merely
|
|
111
|
-
because some implementation details are unspecified; those come from the
|
|
112
|
-
repository.
|
|
113
|
-
|
|
114
|
-
{ "outcome": "refused", "reason": "..." }
|
|
115
|
-
`;
|
|
116
|
-
|
|
117
|
-
const intention = (take(process.argv, "--intention") ?? "").trim();
|
|
118
|
-
if (intention.length === 0) {
|
|
119
|
-
fail("The Planner received no --intention.");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const parsed = parseRole(process.argv, PLANNER_FLAGS, NAME, { "--read": {} });
|
|
123
|
-
if (!parsed.ok) {
|
|
124
|
-
fail(parsed.reason);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const template = loadPrompt(parsed.options, DEFAULT_TEMPLATE);
|
|
128
|
-
if (!template.ok) {
|
|
129
|
-
fail(template.reason);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const maxUnits = Number.parseInt(take(process.argv, "--max-units") ?? "", 10);
|
|
133
|
-
const scratch = mkdtempSync(join(tmpdir(), "plan-"));
|
|
134
|
-
const out = join(scratch, "plan.json");
|
|
135
|
-
const cwd = textOption(parsed.options, "read") ?? process.cwd();
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const rendered = fillPrompt(
|
|
139
|
-
template.value,
|
|
140
|
-
{
|
|
141
|
-
intention,
|
|
142
|
-
max_units: Number.isInteger(maxUnits) ? String(maxUnits) : "a handful",
|
|
143
|
-
out,
|
|
144
|
-
},
|
|
145
|
-
["intention", "out"],
|
|
146
|
-
textOption(parsed.options, "promptFile") ?? "built-in template",
|
|
147
|
-
);
|
|
148
|
-
if (!rendered.ok) {
|
|
149
|
-
fail(rendered.reason);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const spawned = await spawnFilled(
|
|
153
|
-
parsed.runner,
|
|
154
|
-
rendered.prompt,
|
|
155
|
-
transcriptArgs(process.argv, {
|
|
156
|
-
id: "breakdown",
|
|
157
|
-
context: take(process.argv, "--key"),
|
|
158
|
-
}),
|
|
159
|
-
cwd,
|
|
160
|
-
);
|
|
161
|
-
if (!spawned.ok) {
|
|
162
|
-
fail(spawned.reason);
|
|
163
|
-
}
|
|
164
|
-
if (spawned.run.error !== undefined) {
|
|
165
|
-
fail(`Could not start the agent at ${spawned.about.bin}: ${spawned.run.error.message}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const answer = readAnswer(out);
|
|
169
|
-
if (answer === undefined) {
|
|
170
|
-
fail(`The agent wrote no usable plan to ${out}. ${tail(spawned.run.output)}`);
|
|
171
|
-
}
|
|
172
|
-
if (answer.outcome === "refused") {
|
|
173
|
-
const reason =
|
|
174
|
-
typeof answer.reason === "string" && answer.reason.trim().length > 0
|
|
175
|
-
? answer.reason.trim()
|
|
176
|
-
: "The Planner refused without saying why.";
|
|
177
|
-
emitRefusal(reason);
|
|
178
|
-
process.exit(0);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const checked = checkPlan(answer.subtasks, maxUnits);
|
|
182
|
-
if (!checked.ok) {
|
|
183
|
-
fail(`The agent returned a plan that cannot be used: ${checked.reason}`);
|
|
184
|
-
}
|
|
185
|
-
emitPlan(
|
|
186
|
-
checked.subtasks.map((subtask) => ({
|
|
187
|
-
id: subtask.id,
|
|
188
|
-
intention: subtask.intention,
|
|
189
|
-
definition_of_done: subtask.definition_of_done,
|
|
190
|
-
depends_on: subtask.depends_on,
|
|
191
|
-
})),
|
|
192
|
-
);
|
|
193
|
-
process.exit(0);
|
|
194
|
-
} finally {
|
|
195
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** @param {string} path @returns {Record<string, unknown> | undefined} */
|
|
199
|
-
function readAnswer(path) {
|
|
200
|
-
let text;
|
|
201
|
-
try {
|
|
202
|
-
text = readFileSync(path, "utf8");
|
|
203
|
-
} catch {
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
|
|
207
|
-
for (const candidate of [fenced?.[1], text]) {
|
|
208
|
-
if (candidate === undefined) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
const value = JSON.parse(candidate);
|
|
213
|
-
if (value !== null && typeof value === "object") {
|
|
214
|
-
return value;
|
|
215
|
-
}
|
|
216
|
-
} catch {
|
|
217
|
-
// try the next reading
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** @param {string | undefined} output */
|
|
224
|
-
function tail(output) {
|
|
225
|
-
const trimmed = (output ?? "").trim();
|
|
226
|
-
return trimmed.length === 0 ? "" : trimmed.slice(-1000);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** @param {string} reason @returns {never} */
|
|
230
|
-
function fail(reason) {
|
|
231
|
-
writeDiagnostic(`${reason}\n`);
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|