@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 CHANGED
@@ -1,11 +1,9 @@
1
1
  # Slots
2
2
 
3
- The Planner, Builder, and Gate commands that ship with mason. A Project points
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 a Subtask: fill `{{task}}`, then spawn the agent |
64
- | `builders/repair.mjs` | Builder | A Gate refused the Subtask: fill `{{task}}` and `{{report}}` |
65
- | `assembly/fix.mjs` | Builder | A judgement refused the assembled feature: fill `{{task}}` and `{{report}}` |
66
- | `assembly/validate.mjs` | Builder | Read-only review of the assembled feature: fill `{{task}}`; answers `MASON_VERDICT: VALIDATED` or `MASON_VERDICT: REFUSED: …` |
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 feature's commits add nothing its own `.gitignore` excludes |
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
- | `planners/one-subtask.mjs` | Planner | Bootstrap: one Subtask that is the FeatureStandard itself |
73
- | `publishers/git.mjs` | Publisher | Pushes the feature's branch to the work line's own remote |
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
- | `planners/producer.mjs` | Planner | An agent CLI splits the FeatureStandard |
76
- | `messages/conventional.mjs` | Message | `<type>: <title>` for the feature's fold, no agent; `--scope`, `--type` |
77
- | `messages/git-producer.mjs` | Describer | An agent reads the git diff and writes the feature's subject and body under `--rules-file` |
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 Subtask workspace, so a template
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`. `{{done_when}}` is `Done when:` plus the
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}}`, `{{done_when}}`, `{{id}}`, `{{attempt}}` |
251
- | `builders/repair` | `{{task}}`, `{{report}}`, `{{rules}}` | `{{history}}`, `{{refused_by}}`, `{{done_when}}`, `{{id}}`, `{{attempt}}` |
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 works as before.
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 feature that refused it is not a Subtask retry.
289
- The units already passed. `assembly/fix.mjs` tells the agent to change what the
290
- refusal names and leave the rest. Same placeholders as repair except
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 Subtask directory, so an
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 Feature the Task belongs to, named for the Subtask and 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 feature's commits add a path 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 feature is offered to
410
- (`main`): only what the feature adds since it left that line is judged, and
411
- only what is committed, so a path tracked on purpose before the feature began
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 Subtask's work yet.
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 feature where the Authority can read it. It runs
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 feature — and `--target`, the work line it is offered to.
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/feature-42" }
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 FeatureManager is told to judge, so a Project
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 a Subtask
586
- assembly/ the same two roles for the assembled feature
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 and Planner suites run against
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 feature. The units of work that made it have
23
- already been validated. An earlier judgement of the whole refused it. Do not
24
- redo those units. Fix what the refusal names.
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
 
@@ -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 feature. Every unit of work that makes it up
23
- has already been validated on its own. Your job now is different: read the
24
- diff this feature carries against the work line it will land on, and judge
25
- whether the WHOLE meets what was asked. Do not redo the units. Do not change
26
- anything — this is a read-only review, not a repair.
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 feature meets what was asked.
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 feature, with no reason given.",
165
+ : "assembly.validate refused the assembled ticket, with no reason given.",
166
166
  );
167
167
  process.exit(1);
168
168
  }
@@ -13,7 +13,7 @@ import {
13
13
  textOption,
14
14
  transcriptArgs,
15
15
  } from "@bluewombat/slot-kit";
16
- import { attemptOf, doneWhen } from "../lib/fill.mjs";
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
- {{done_when}}
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)),
@@ -13,7 +13,7 @@ import {
13
13
  textOption,
14
14
  transcriptArgs,
15
15
  } from "@bluewombat/slot-kit";
16
- import { attemptOf, doneWhen } from "../lib/fill.mjs";
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)),
@@ -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 feature's commits add a path the workspace's own
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 feature is offered to. Only what the
10
- // feature adds since it left that line is judged, in what is committed: a path
11
- // tracked on purpose before the feature began is not this feature's leak, and
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. A Subtask's work is uncommitted until a fold records it,
15
- // so there is no commit of its own here to judge.
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 feature is offered to.",
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
- "feature does mean to track one, change .gitignore so it no longer excludes it:",
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 feature's commit in the Conventional Commits shape, from the issue's
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 feature's commit message — and the pull request's description with it
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 feature. A role slot like the builders: it fills a template
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.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
- "description": "Shipped mason slots: Planners, Builders, Gates, publishers, and refreshers a Project names in its config.",
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.8.0",
53
+ "@bluewombat/slot-kit": "^0.9.0",
57
54
  "@pinkpuma/deck": "1.0.0"
58
55
  }
59
56
  }
@@ -11,7 +11,7 @@ import {
11
11
  writeDiagnostic,
12
12
  } from "@bluewombat/slot-kit";
13
13
 
14
- // Put the feature's work where the Authority can read it, by pushing the name
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
@@ -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
- const fetched = git(["fetch", "origin", target]);
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
@@ -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
- ]);
@@ -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
- }