@bluewombat/runtime 0.10.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.
Files changed (84) hide show
  1. package/README.md +121 -90
  2. package/SPECS.md +102 -22
  3. package/dist/cli.js +32 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/agent-shorthand.d.ts +3 -4
  6. package/dist/config/agent-shorthand.d.ts.map +1 -1
  7. package/dist/config/agent-shorthand.js +3 -4
  8. package/dist/config/agent-shorthand.js.map +1 -1
  9. package/dist/config/fixture-yaml.d.ts +8 -0
  10. package/dist/config/fixture-yaml.d.ts.map +1 -0
  11. package/dist/config/fixture-yaml.js +30 -0
  12. package/dist/config/fixture-yaml.js.map +1 -0
  13. package/dist/config/load-config.d.ts.map +1 -1
  14. package/dist/config/load-config.js +88 -101
  15. package/dist/config/load-config.js.map +1 -1
  16. package/dist/config/parse-args.d.ts.map +1 -1
  17. package/dist/config/parse-args.js +3 -4
  18. package/dist/config/parse-args.js.map +1 -1
  19. package/dist/config/types.d.ts +8 -16
  20. package/dist/config/types.d.ts.map +1 -1
  21. package/dist/config/types.js +1 -1
  22. package/dist/config/types.js.map +1 -1
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/loop/features.d.ts +16 -0
  28. package/dist/loop/features.d.ts.map +1 -1
  29. package/dist/loop/features.js +124 -45
  30. package/dist/loop/features.js.map +1 -1
  31. package/dist/loop/open-host.d.ts +10 -0
  32. package/dist/loop/open-host.d.ts.map +1 -1
  33. package/dist/loop/open-host.js +8 -8
  34. package/dist/loop/open-host.js.map +1 -1
  35. package/dist/loop/streams.d.ts +3 -0
  36. package/dist/loop/streams.d.ts.map +1 -1
  37. package/dist/loop/streams.js +2 -1
  38. package/dist/loop/streams.js.map +1 -1
  39. package/dist/loop/tick.d.ts +3 -0
  40. package/dist/loop/tick.d.ts.map +1 -1
  41. package/dist/loop/tick.js.map +1 -1
  42. package/dist/loop/trace.d.ts +1 -0
  43. package/dist/loop/trace.d.ts.map +1 -1
  44. package/dist/loop/trace.js +1 -1
  45. package/dist/loop/trace.js.map +1 -1
  46. package/dist/loop/transformers.d.ts +4 -4
  47. package/dist/loop/transformers.d.ts.map +1 -1
  48. package/dist/loop/transformers.js +11 -22
  49. package/dist/loop/transformers.js.map +1 -1
  50. package/dist/operator/board.d.ts +43 -4
  51. package/dist/operator/board.d.ts.map +1 -1
  52. package/dist/operator/board.js +153 -10
  53. package/dist/operator/board.js.map +1 -1
  54. package/dist/operator/doctor.d.ts.map +1 -1
  55. package/dist/operator/doctor.js +5 -8
  56. package/dist/operator/doctor.js.map +1 -1
  57. package/dist/operator/explain.d.ts.map +1 -1
  58. package/dist/operator/explain.js +6 -11
  59. package/dist/operator/explain.js.map +1 -1
  60. package/dist/operator/incident-dossier.d.ts +154 -0
  61. package/dist/operator/incident-dossier.d.ts.map +1 -0
  62. package/dist/operator/incident-dossier.js +696 -0
  63. package/dist/operator/incident-dossier.js.map +1 -0
  64. package/dist/operator/incident-system.d.ts +48 -0
  65. package/dist/operator/incident-system.d.ts.map +1 -0
  66. package/dist/operator/incident-system.js +152 -0
  67. package/dist/operator/incident-system.js.map +1 -0
  68. package/dist/operator/incident.d.ts +27 -0
  69. package/dist/operator/incident.d.ts.map +1 -0
  70. package/dist/operator/incident.js +220 -0
  71. package/dist/operator/incident.js.map +1 -0
  72. package/dist/operator/init.js +24 -24
  73. package/dist/operator/init.js.map +1 -1
  74. package/dist/operator/live.js +4 -4
  75. package/dist/operator/live.js.map +1 -1
  76. package/dist/operator/log.d.ts +2 -0
  77. package/dist/operator/log.d.ts.map +1 -1
  78. package/dist/operator/log.js +2 -5
  79. package/dist/operator/log.js.map +1 -1
  80. package/dist/operator/restart.d.ts +20 -0
  81. package/dist/operator/restart.d.ts.map +1 -0
  82. package/dist/operator/restart.js +63 -0
  83. package/dist/operator/restart.js.map +1 -0
  84. package/package.json +14 -14
package/README.md CHANGED
@@ -53,19 +53,21 @@ package's README — Host does not grow a `--repo` flag for it.
53
53
  | --------------- | -------------------------------------------------------------------------------------- |
54
54
  | `mason run` | Drain the TicketTracker, run Conductor, report |
55
55
  | `mason watch` | Snapshot the ledger and follow the journal. Ctrl-C leaves the worker running |
56
- | `mason status` | Snapshot the ledger, then exit. `--json` for scripts |
56
+ | `mason status` | Snapshot the ledger as a Feature / Ticket tree, then exit. `--json` for scripts |
57
57
  | `mason log` | Replay the journal, with the reason a phase stopped. `--problems` for just those |
58
58
  | `mason init` | Set this directory up: asks, writes the config and the Builder stub, then runs `setup` |
59
59
  | `mason setup` | Prepare the tracker. A plan by default; `--apply` writes. Exit 1 on a blocked step |
60
60
  | `mason doctor` | Check the config, the slots, and the manager. Exit 1 on any failure |
61
+ | `mason incident` | Gather what is known about one stop (ledger, journal, streams, transcripts) into one page, for a person to read or hand on. Without a key, list what waits. `--json`, `--out DIR`, `--with-config`. Read-only |
61
62
  | `mason explain` | Say what an agent role starts with: prompt, plugins, skills, the project's profile. Read-only, offline. Exit 1 on a setup every attempt would fail on |
62
63
  | `mason cancel` | Abandon a Ticket by key, when the tracker cannot say it is gone |
64
+ | `mason restart` | Resume an escalated Feature with a fresh workspace, made again from its line. Offline: the tracker is not told |
63
65
  | `mason release` | Drop a held unit without waiting for the bail clock |
64
66
 
65
67
  ### What an agent role starts with
66
68
 
67
69
  ```bash
68
- mason explain builder.producer # one role; no role names all of them
70
+ mason explain ticket.build.producer # one role; no role names all of them
69
71
  mason explain --json # the same facts for a script
70
72
  ```
71
73
 
@@ -128,7 +130,9 @@ mason watch
128
130
  That is the operator surface: the durable state of every Ticket, then the film
129
131
  of the current step (`submitted` included). It does not talk to the tracker, and
130
132
  it cannot `resume` or claim. `status` is the same snapshot without the
131
- follow. `--json` prints that snapshot as JSON.
133
+ follow: a header with the counts, each Feature with its Tickets under it
134
+ (escalated Features first, finished ones last, keys in numeric order), then the
135
+ Tickets of no Feature. `--json` prints that tree as JSON.
132
136
 
133
137
  When a run only printed `unavailable:<key>`, the reason is already in the
134
138
  journal and `log` is how you read it:
@@ -144,6 +148,24 @@ distinct failure. If a slot was started with `--transcript-dir`, the newest
144
148
  transcript files are named underneath, with the agent exit and duration read
145
149
  from the start of each file.
146
150
 
151
+ When a Ticket or a Feature is waiting for a person, `incident` gathers what the
152
+ run knows of it:
153
+
154
+ ```bash
155
+ mason incident # what waits for a person
156
+ mason incident 106 # one stop: the key, or the tracker's number
157
+ mason incident 106 --out ./inc # incident.md and incident.json, to hand on
158
+ mason incident 106 --with-config # also the config file, secret-looking values blanked
159
+ ```
160
+
161
+ The page says what the run was made of (versions, what the configuration switched
162
+ on), who refused the work and what they said, the Tickets of the
163
+ Feature with their pull requests, the Gate's configuration, the journal's events
164
+ of the last round and where the streams and transcripts are. What it could not
165
+ find is listed under "Not found". Streams exist only if the Project turned them
166
+ on (`--streams-dir`); without them the page has the Gate's report but not what
167
+ the Gate printed.
168
+
147
169
  To abandon a Ticket the tracker can no longer mention (a deleted issue, a
148
170
  manager with no `probe`), stop `run` if it holds the ledger lock, then:
149
171
 
@@ -162,6 +184,18 @@ mason release github:owner/name#19
162
184
 
163
185
  `release` destroys the workspace and forgets it; the next `run` isolates a fresh one.
164
186
 
187
+ A Feature that escalated keeps its work. When a person then changes its line and resumes it,
188
+ the work takes in what the line gained, merged into it, before it is judged again; a conflict
189
+ escalates the Feature again, naming it. To throw the work away instead, stop `run` if it holds
190
+ the lock, then:
191
+
192
+ ```bash
193
+ mason restart github:owner/name#106 # or just 106
194
+ ```
195
+
196
+ The Feature must be escalated. Its workspace is removed and it is resumed; the next `run`
197
+ cuts the work again from the line. The tracker is not told.
198
+
165
199
  The film is `<ledger>/events.jsonl`, next to `cursor`. It is not truth after a
166
200
  crash — the ledger is. The file can grow; there is no rotation yet.
167
201
 
@@ -178,43 +212,48 @@ value that starts with `@`, like every `@bluewombat/...` package name, needs
178
212
  quotes: `@` is a reserved YAML indicator at the start of a plain scalar.
179
213
 
180
214
  ```yaml
181
- featureTracker:
182
- package: "@bluewombat/manager-fake"
183
- options:
184
- source: ./.mason/source
185
- target: ./.mason/threads
186
- ticketTracker:
187
- package: "@bluewombat/manager-fake"
188
- options:
189
- source: ./.mason/source
190
- target: ./.mason/threads
191
215
  workLine:
192
216
  stable: ./work-line-stable
193
217
  isolation: "@bluewombat/isolation-copy"
194
218
  warm:
195
219
  cmd: [pnpm, install, --frozen-lockfile]
196
220
  timeoutMs: 600000
197
- builder:
198
- producer:
199
- cmd: [node, ./mason-builder.mjs]
200
- timeoutMs: 600000
201
- repair:
202
- cmd: [node, ./mason-repair.mjs]
203
- timeoutMs: 600000
204
- gates: { defaultTimeoutMs: 120000, gates: [] }
205
- maxAttempts: 3
206
221
  ticket:
222
+ tracker:
223
+ package: "@bluewombat/manager-fake"
224
+ options:
225
+ source: ./.mason/source
226
+ target: ./.mason/threads
227
+ build:
228
+ producer:
229
+ cmd: [node, ./mason-builder.mjs]
230
+ timeoutMs: 600000
231
+ repair:
232
+ cmd: [node, ./mason-repair.mjs]
233
+ timeoutMs: 600000
234
+ gates: { defaultTimeoutMs: 120000, gates: [] }
235
+ maxAttempts: 3
236
+ validate: { agent: false }
207
237
  gates: { defaultTimeoutMs: 900000, gates: [] }
238
+ feature:
239
+ tracker:
240
+ package: "@bluewombat/manager-fake"
241
+ options:
242
+ source: ./.mason/source
243
+ target: ./.mason/threads
208
244
  authority:
209
245
  enabled: false
210
246
  timeoutMs: 600000
211
247
  pollIntervalMs: 10000
212
248
  ```
213
249
 
250
+ The file reads by level, and each level in the order it is used: `ticket` and `feature` both hold
251
+ `tracker`, then what makes the work (`build` for a Ticket, `test` for a Feature), `validate`, `gates`, `fix`.
252
+
214
253
  | Field | Meaning |
215
254
  | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
216
- | `featureTracker` | `package` (a name or path that exports `createManager`) and `options`: where Features are written and signalled |
217
- | `ticketTracker` | Same shape: where the Tickets of a Feature are, and the Authority that holds the lines. May name the same package as `featureTracker`, or another |
255
+ | `feature.tracker` | `package` (a name or path that exports `createManager`) and `options`: where Features are written and signalled |
256
+ | `ticket.tracker` | Same shape: where the Tickets of a Feature are, and the Authority that holds the lines. May name the same package as `feature.tracker`, or another |
218
257
  | `workLine.stable` | Existing directory the work folds into. Optional when the manager names a reference work line: Host then keeps its own copy at `<home>/work-line` |
219
258
  | `workLine.targetStable` | A second copy of the work line, on the branch a Feature is folded into: what is merged into the Feature when it is brought up to date. Host keeps its own at `<home>/target-line` when the manager names a reference; required with an Authority otherwise |
220
259
  | `workLine.isolation` | Package name or path exporting `strategy` — required. Host loads it like a manager; Isolator/Integrator do not sniff |
@@ -227,14 +266,13 @@ pollIntervalMs: 10000
227
266
  | `ledger` | WorkLedger root (created). The Cursor is a file inside it. Default `<home>/ledger` |
228
267
  | `persist` | Package name or path exporting `openPersist({ ledgerRoot })`: how the ledger is stored under that root. Default `@bluewombat/persist-fs`; `@bluewombat/persist-sqlite` ships too. `watch` reads through the same backend |
229
268
  | `agents` | Agents a pass names instead of writing `cmd` (§ Agent passes written short) |
230
- | `builder.tests` | Required. The tests of the task, written before its code by an agent that is not the producer: `{ agent, prompt, homePlugins, appDeckProfile, timeoutMs, gates }`, or `{ agent: false }` for none. Its `gates` check them (`tests-red`: they must fail, for the right reason). What it wrote is committed as a checkpoint titled `Tests: …`, which `builder.gates` can keep intact (`tests-frozen`). A ticket the tracker labels as having no tests first (`no-tests` with the GitHub manager) runs no agent and no `tests-red` |
231
- | `builder.e2e` | Optional. Declares the e2e acceptance check (see § The e2e acceptance check): the pass that makes the Ticket holding the suite, `{ agent, prompt, homePlugins, appDeckProfile, timeoutMs, gates }` or `{ cmd, timeoutMs, gates }`. One command for the first pass and for a pass a Gate refused; its own `gates` (they check the form: compiles, one test per manifest id, no application file touched). Nothing is borrowed from `builder.producer`, `builder.repair` or `builder.gates`. No tests are written first for that Ticket. Needs a Feature Gate with `skipWhen: skip-test-e2e`, and the other way round |
232
- | `builder.producer` | `cmd` / `timeoutMs`. Spawned in the workspace. Exit 0 runs `builder.gates` |
233
- | `builder.repair` | The same, for a pass a Gate refused. **Required**, and never inherited: it names its own command. It is judged by the same `builder.gates` as the producer |
234
- | `builder.gates` | `defaultTimeoutMs` and `gates`: the one Gate list for `builder.producer` and `builder.repair`. `gates` written on either pass is refused, naming this key. Empty: the unit runs ungated |
235
- | `builder.maxAttempts` | Attempts one Task may start. Default 3 |
269
+ | `feature.test` | Optional. Declares the feature-test acceptance check (see § The feature-test acceptance check): the pass that makes the Ticket holding the suite, `{ agent, prompt, homePlugins, appDeckProfile, timeoutMs, gates }` or `{ cmd, timeoutMs, gates }`. One command for the first pass and for a pass a Gate refused; its own `gates` (they check the form: compiles, one test per manifest id, no application file touched). Nothing is borrowed from `ticket.build.producer`, `ticket.build.repair` or `ticket.build.gates`. Needs a Feature Gate with `skipWhen: skip-feature-test`, and the other way round |
270
+ | `ticket.build.producer` | `cmd` / `timeoutMs`. Spawned in the workspace. Exit 0 runs `ticket.build.gates` |
271
+ | `ticket.build.repair` | The same, for a pass a Gate refused. **Required**, and never inherited: it names its own command. It is judged by the same `ticket.build.gates` as the producer |
272
+ | `ticket.build.gates` | `defaultTimeoutMs` and `gates`: the one Gate list for `ticket.build.producer` and `ticket.build.repair`. `gates` written on either pass is refused, naming this key. Empty: the unit runs ungated |
273
+ | `ticket.build.maxAttempts` | Attempts one Task may start. Default 3 |
236
274
  | `ticket.fix` | `cmd` / `timeoutMs` / `gates`. Spawned when a judgement of the whole refused it — by an Authority, by `ticket.validate`, or by both. Judged by its own `gates` |
237
- | `ticket.validate` | `cmd` / `timeoutMs` / `gates`. A local, read-only judge of the assembled ticket, run after align, with or without an Authority. A refusal is repaired by `ticket.fix` the same as an Authority's. Judged by its own `gates`, never `builder`'s or `ticket.fix`'s. `validate` is required when a `ticket` block is written, and so is `feature.validate`: `{ agent: false, gates }` turns the agent off and keeps the gates, and `{ agent: false }` with no gates turns the review off — a bare `gates:` counts as none. |
275
+ | `ticket.validate` | `cmd` / `timeoutMs` / `gates`. A local, read-only judge of the assembled ticket, run after align, with or without an Authority. A refusal is repaired by `ticket.fix` the same as an Authority's. Judged by its own `gates`, never `ticket.build`'s or `ticket.fix`'s. `validate` is required when a `ticket` block is written, and so is `feature.validate`: `{ agent: false, gates }` turns the agent off and keeps the gates, and `{ agent: false }` with no gates turns the review off — a bare `gates:` counts as none. |
238
276
  | `ticket.maxAttempts` | Attempts that one fix may start. Default 3 |
239
277
  | `ticket.gates` | `defaultTimeoutMs` and `gates`: the produce:false judgement-only pass — no producer of its own, re-checking the assembled ticket. Empty: the Authority judges alone. Independent of `ticket.fix.gates` / `ticket.validate.gates` — never shared with either |
240
278
  | `timeoutMs` | How long a child **outside** a Task may run: manager, isolations |
@@ -246,7 +284,7 @@ pollIntervalMs: 10000
246
284
 
247
285
  Every `cmd` is named where it is used, and nothing falls back to a neighbour: a
248
286
  `repair` with no command of its own is refused rather than quietly running the
249
- first-pass agent again. `builder` makes the unit of work. `ticket` judges the whole
287
+ first-pass agent again. `ticket.build` makes the unit of work. `ticket` judges the whole
250
288
  and, when that judgement refuses it, runs `ticket.fix` — not a second first-pass.
251
289
  `ticket.validate`, when declared, is a second, local judge of the whole: it
252
290
  runs after align, before the Authority sees the work (or before the final
@@ -275,23 +313,23 @@ agents:
275
313
  denyShell: [] # commands refused to the agent, e.g. [gh, vercel]; ssh and scp always are
276
314
  transcripts: ./.mason/transcripts
277
315
  pluginsOut: ./.mason/plugins # where deck profiles are built
278
- builder:
279
- producer:
280
- agent: cursor
281
- prompt: ./prompts/builder.md
282
- homePlugins: [./ai-plugins/core/builder]
283
- appDeckProfile: .ai/profiles/builder
284
- args: [--mode, agent] # the vendor CLI's own flags
285
- timeoutMs: 1200000
286
- gates: { defaultTimeoutMs: 120000, gates: [] }
316
+ ticket:
317
+ build:
318
+ producer:
319
+ agent: cursor
320
+ prompt: ./prompts/builder.md
321
+ homePlugins: [./ai-plugins/core/builder]
322
+ appDeckProfile: .ai/profiles/builder
323
+ args: [--mode, agent] # the vendor CLI's own flags
324
+ timeoutMs: 1200000
287
325
  ```
288
326
 
289
327
  Host writes the `cmd` it stands for — `node <slots>/<role>.mjs --prompt-file …
290
328
  -- node <slots>/agents/cursor.mjs --transcript-dir … --plugin-dir … --app-deck-profile
291
329
  … --plugins-out … --agent-arg …` — with `<slots>` the
292
330
  `node_modules/@bluewombat/slots` next to the config. Nothing past the config
293
- knows a pass was written short. The roles: `builder.producer`,
294
- `builder.repair`, `ticket.fix`, `ticket.validate`, and `authority.describe`
331
+ knows a pass was written short. The roles: `ticket.build.producer`,
332
+ `ticket.build.repair`, `ticket.fix`, `ticket.validate`, and `authority.describe`
295
333
  (an argv, or `{ agent, prompt?, homePlugins?, appDeckProfile?, args? }`).
296
334
 
297
335
  | Key | Meaning |
@@ -387,20 +425,20 @@ Examples ship in [`@bluewombat/slots`](../../plugins/slots/README.md), installed
387
425
  the shipped roles and agents (Cursor CLI, Claude Code), the shipped Gates, and
388
426
  their options. What follows is what Host guarantees them.
389
427
 
390
- ```json
391
- "builder": {
392
- "producer": {
393
- "cmd": [
394
- "node", "./node_modules/@bluewombat/slots/builders/producer.mjs",
395
- "--",
396
- "node", "./node_modules/@bluewombat/slots/agents/claude.mjs"
397
- ],
398
- "timeoutMs": 600000
399
- }
400
- }
428
+ ```yaml
429
+ ticket:
430
+ build:
431
+ producer:
432
+ cmd:
433
+ - node
434
+ - ./node_modules/@bluewombat/slots/builders/producer.mjs
435
+ - --
436
+ - node
437
+ - ./node_modules/@bluewombat/slots/agents/claude.mjs
438
+ timeoutMs: 600000
401
439
  ```
402
440
 
403
- `builder.repair` is required too, with its own `cmd` and `timeoutMs`. The
441
+ `ticket.build.repair` is required too, with its own `cmd` and `timeoutMs`. The
404
442
  composition with `--` is the same.
405
443
 
406
444
  ### Builder
@@ -437,16 +475,16 @@ that completes cleanly but finds a problem is still a refusal.
437
475
 
438
476
  ### Gates
439
477
 
440
- Four sequences: `builder.gates`, shared by `builder.producer` and
441
- `builder.repair` (a repair is judged by exactly what judged the first try),
478
+ Four sequences: `ticket.build.gates`, shared by `ticket.build.producer` and
479
+ `ticket.build.repair` (a repair is judged by exactly what judged the first try),
442
480
  then one per assembly role — `ticket.fix`, `ticket.validate` — plus
443
481
  `ticket.gates` for the one judgement-only pass with no producer of its own
444
482
  (re-checking the assembled ticket; `ci-green` reads the checks the work line
445
483
  ran on what the Publisher placed, so it belongs there). The assembly lists are
446
484
  not shared with each other or with the builder, and no dispatch chooses between
447
485
  two buckets: a check right for one pass is not necessarily right for another,
448
- even within the same stage. `gates` written on `builder.producer` or
449
- `builder.repair` is refused, and the message names `builder.gates`.
486
+ even within the same stage. `gates` written on `ticket.build.producer` or
487
+ `ticket.build.repair` is refused, and the message names `ticket.build.gates`.
450
488
 
451
489
  Implementer's Gates are told `--stage unit` or `--stage ticket` alongside
452
490
  `--id`, `--attempt`, `--gate-id` and `--intention`.
@@ -454,7 +492,7 @@ Most have no use for it. `workspace-changed` does: "nothing changed" means
454
492
  nothing was done in a unit, and is the correct outcome for an assembly that
455
493
  needed nothing.
456
494
 
457
- `init` writes an empty list on `builder.producer` and `builder.repair`; add a
495
+ `init` writes an empty list on `ticket.build.producer` and `ticket.build.repair`; add a
458
496
  Gate when the Project has a check.
459
497
 
460
498
  Every child carries its own ceiling and no other. A Gate entry may name a
@@ -464,7 +502,7 @@ from what another child left behind, so a slow producer cannot leave its
464
502
  Gates a millisecond to answer in.
465
503
 
466
504
  What bounds the unit is therefore what you wrote:
467
- `builder.maxAttempts × (builder.producer.timeoutMs + Σ builder.gates timeouts)`
505
+ `ticket.build.maxAttempts × (ticket.build.producer.timeoutMs + Σ ticket.build.gates timeouts)`
468
506
  for a first pass, and the same shape with `repair` in place of `producer` once
469
507
  a Gate has refused one.
470
508
  A repair and an assembly fix each carry their own `timeoutMs`. An assembly fix
@@ -484,53 +522,46 @@ is bounded the same way from `ticket.maxAttempts` and `ticket.fix`.
484
522
  }
485
523
  ```
486
524
 
487
- ### The e2e acceptance check
525
+ ### The feature-test acceptance check
488
526
 
489
- A Project proves each Feature by an acceptance suite, run once on the assembled Feature
527
+ A Project proves each Feature by its own tests, run once on the assembled Feature
490
528
  line, when its config says so, by a pair:
491
529
 
492
530
  ```yaml
493
- builder:
494
- e2e: # makes the Ticket that holds the suite (label `e2e` on the tracker)
531
+ feature:
532
+ test: # makes the Ticket that holds the suite (label `feature-test` on the tracker)
495
533
  agent: claude
496
- prompt: ./prompts/e2e.md # {{task}} is the scenarios; {{report}}, {{refused_by}}, {{refusal}} on a refused pass
534
+ prompt: ./prompts/feature-test.md # {{task}} is the scenarios; {{report}}, {{refused_by}}, {{refusal}} on a refused pass
497
535
  timeoutMs: 1800000
498
536
  gates: # form only: it compiles, one test per manifest id, no application file touched
499
537
  gates:
500
- - { id: e2e-form, argv: [...], timeoutMs: 120000 }
501
- feature:
538
+ - { id: feature-tests-form, argv: [...], timeoutMs: 120000 }
502
539
  validate:
503
540
  agent: false
504
541
  gates:
505
542
  defaultTimeoutMs: 120000
506
543
  gates:
507
- - id: e2e
508
- skipWhen: skip-test-e2e # Host does not run this Gate for a Feature that carries the label
544
+ - id: feature-tests
545
+ skipWhen: skip-feature-test # Host does not run this Gate for a Feature that carries the label
509
546
  timeoutMs: 720000
510
547
  argv:
511
548
  - node
512
- - ./node_modules/@bluewombat/slots/gates/e2e.mjs
513
- - --up
514
- - pnpm e2e:up
515
- - --ready
516
- - pnpm e2e:ready
549
+ - ./node_modules/@bluewombat/slots/gates/feature-tests.mjs
517
550
  - --run
518
- - pnpm e2e
519
- - --down
520
- - pnpm e2e:down
551
+ - pnpm test --grep "{ids}" --reporter=junit --outputFile={junit}
521
552
  ```
522
553
 
523
554
  `skipWhen` is the one condition Host reads for a Gate, on a Feature's Gates only
524
555
  (`feature.validate`, `feature.gates`, `feature.fix`); its one value today is
525
- `skip-test-e2e`, whatever the tracker calls that label. A Gate left out is not a Gate that
526
- passed: the journal gets `gate-skipped` and the trace says "`e2e` not run: the Feature carries
527
- skip-test-e2e". The Gate knows nothing of the skip and no environment variable carries it.
556
+ `skip-feature-test`, whatever the tracker calls that label. A Gate left out is not a Gate that
557
+ passed: the journal gets `gate-skipped` and the trace says "`feature-test` not run: the Feature carries
558
+ skip-feature-test". The Gate knows nothing of the skip and no environment variable carries it.
528
559
 
529
- The tracker side: a Ticket carrying the e2e label (`e2eLabel` of the TicketTracker manager) is
530
- made by `builder.e2e` and has no tests written first; a Feature carrying the skip label
531
- (`skipTestE2eLabel` of the FeatureTracker manager) has that Ticket cancelled, with a line in
560
+ The tracker side: a Ticket carrying the feature-test label (`featureTestLabel` of the TicketTracker manager) is
561
+ made by `feature.test`; a Feature carrying the skip label
562
+ (`skipFeatureTestLabel` of the FeatureTracker manager) has that Ticket cancelled, with a line in
532
563
  the trace. The label is read again until the Feature is submitted. A Project that declares the
533
- check refuses a Feature that holds neither an e2e Ticket nor the skip label, or whose e2e
564
+ check refuses a Feature that holds neither a feature-test Ticket nor the skip label, or whose feature-test
534
565
  Ticket is not blocked by every other Ticket, as `invalid`, with the reason as a comment.
535
566
 
536
567
  What the Gate proves is a red/green verdict; what the operator reads is its text in the
@@ -610,7 +641,7 @@ it each stop `mason run` at boot, and `mason doctor` reports the same offline
610
641
  and is the operator's directory.
611
642
 
612
643
  ```
613
- assembled → align → recheck (builder.gates again, on the aligned work)
644
+ assembled → align → recheck (ticket.build.gates again, on the aligned work)
614
645
  conflict or failed → ticket.fix, then align again
615
646
  → ticket.validate (optional, local)
616
647
  refused → ticket.fix, then align again, recheck, validate again
@@ -631,13 +662,13 @@ fast-forward — the work line moved where the Authority is, and the next
631
662
  Isolation must start from that version, not from a copy left behind.
632
663
 
633
664
  A refusal is not a second failure path: its report reaches `ticket.fix`
634
- exactly as a Gate's report reaches `builder.repair`. After `ticket.maxRefusals`
665
+ exactly as a Gate's report reaches `ticket.build.repair`. After `ticket.maxRefusals`
635
666
  refusals the ticket escalates with that report as its Trace, and `done` names
636
667
  the Submission rather than a directory on the machine that ran.
637
668
 
638
669
  ## Write another manager
639
670
 
640
- Host loads whatever `featureTracker.package` and `ticketTracker.package` name, as long as it exports `createManager`. The
671
+ Host loads whatever `feature.tracker.package` and `ticket.tracker.package` name, as long as it exports `createManager`. The
641
672
  contract and a worked example are in
642
673
  [`@bluewombat/manager-kit`](../manager-kit/README.md). Nothing in Host changes.
643
674
 
@@ -706,7 +737,7 @@ change that breaks it does not merge.
706
737
  | `config/` | What a Project wrote: argv, `mason.config.yaml`, discovery, `HostOptions`. Pure: YAML in, options out | A runtime value (a Journal, a Port, a process handle) | Node, `manager-kit` types |
707
738
  | `plugins/` | Load a package by name and check its shape: a manager, an isolation strategy | What to do with the loaded thing | `config` |
708
739
  | `loop/` | The process: listen → probe → adapt → admit → run → report; composition (`open-host`, `transformers`, Authority, work line, lock, Cursor, journal, trace) | A command a human types; a string a human reads about the config | `plugins`, `config`, the kernel, Transformers |
709
- | `operator/` | Tools for a human: `init`, `setup`, `doctor`, `watch` / `status`, `log`, `cancel`, `release`, `board`, the terminal prompt | Anything the loop needs — the loop cannot see this layer | everything below |
740
+ | `operator/` | Tools for a human: `init`, `setup`, `doctor`, `watch` / `status`, `log`, `incident`, `cancel`, `restart`, `release`, `board`, the terminal prompt | Anything the loop needs — the loop cannot see this layer | everything below |
710
741
  | `index.ts`, `cli.ts` | The package surface and the dispatch. Nothing else sits at the top of `src/` | Logic | everything |
711
742
 
712
743
  Host's code names no plugin. A manager, an isolation strategy, a persistence
@@ -761,6 +792,6 @@ src/
761
792
  config/ argv, JSON config, config discovery → HostOptions; defaults: the plugin names Host ships with
762
793
  plugins/ load: the one loader; manager, isolation, persist: one shape each; discover
763
794
  loop/ open-host (composition), tick (the loop), deliveries, drive, probe, report, authority, work-line, cursor, lock, journal (the film), streams (what a child said), trace, transformers
764
- operator/ run, init, setup, doctor, explain, live (watch / status), log, film, cancel, release, board, prompt, git-remote
795
+ operator/ run, init, setup, doctor, explain, live (watch / status), log, incident, film, cancel, release, board, prompt, git-remote
765
796
  fixtures/ stand-in slots for the loop's tests
766
797
  ```
package/SPECS.md CHANGED
@@ -71,7 +71,7 @@ See § 8.
71
71
 
72
72
  ## 4. Loading a manager
73
73
 
74
- `featureTracker.package` and `ticketTracker.package` are each a package name or a path. The two sides may name the same package or two different ones: a package asked for a side it does not answer is refused by name. One package with the same options on both sides is built once. `doctor` and `setup` run each distinct manager once; the reference work line is named by the ticket tracker, which holds the Authority. Resolution imports the bare specifier
74
+ `feature.tracker.package` and `ticket.tracker.package` are each a package name or a path. The two sides may name the same package or two different ones: a package asked for a side it does not answer is refused by name. One package with the same options on both sides is built once. `doctor` and `setup` run each distinct manager once; the reference work line is named by the ticket tracker, which holds the Authority. Resolution imports the bare specifier
75
75
  first — mason and its managers installed side by side in one project — then
76
76
  walks up from the config file's directory, which covers a global `mason`
77
77
  driving a locally installed manager. A module without `createManager` is
@@ -103,7 +103,7 @@ The FeatureTracker's `listen` delivers Features whose specification is final.
103
103
  | `invalid` (new, or the intention changed) | emit `invalid` on the Feature, keyed by fingerprint |
104
104
  | `invalid` (same fingerprint as recorded) | leave it; the tracker was told, and telling it is what delivered it again |
105
105
  | `upsert` (absent or `waiting`) | `admitFeature`, emit `accepted`. Nothing is driven here: the sweep does it |
106
- | `upsert` (`open` or `escalated`) whose `skip_e2e` differs from the ledger's | `setFeatureSkipE2e`; journal `skip-e2e`; no other change |
106
+ | `upsert` (`open` or `escalated`) whose `skip_feature_test` differs from the ledger's | `setFeatureSkipFeatureTest`; journal `skip-feature-test`; no other change |
107
107
  | `upsert` (any other state) | leave it; journal `skipped`. An echo of a Host report is this row |
108
108
  | `upsert` + `signalsResume` while `escalated` | same as `resume` — a poll snapshot has no `labeled` edge |
109
109
  | `resume` | resume the Feature, emit `resumed` |
@@ -140,20 +140,33 @@ advanced (`advanceProject`):
140
140
  4. A Feature whose Tickets are all blocked escalates; one that cannot be
141
141
  settled because the Authority lacks `settled` waits.
142
142
 
143
- The e2e acceptance check, when the Project declares it (`builder.e2e`):
143
+ The Feature's own work (`.mason/feature-workspaces/<feature>`) is cut from its
144
+ line once, when it is closed, and a pass that sent it to the fix finds it where
145
+ it was left. A resume is the one time the line may have gained commits since
146
+ (a person's): `resumeFeature` marks the work stale (`workspace_stale`), and the
147
+ next pass, before judging, refreshes the line and runs the Integration with the
148
+ work as Parent and the line copy as Child (`<key>:resume`), so what the work
149
+ holds, a refused fix's commits included, stays. Once it is done the mark is
150
+ cleared (`markFeatureWorkspaceCurrent`); a refresh that fails or an interrupt
151
+ leaves it, and the next pass tries again. A conflict is not settled by Host: the
152
+ Feature escalates (`assembly`) with a reason naming the conflict, the work left
153
+ as it was, and the mark stays for the next resume. A work cut after the resume
154
+ is current by construction.
155
+
156
+ The feature-test acceptance check, when the Project declares it (`feature.test`):
144
157
 
145
158
  - A Feature does not open (and is reported `invalid`, once per reason) unless it holds exactly
146
- one e2e Ticket (`TicketEntry.e2e`), blocked by every other live Ticket, or carries `skip_e2e`.
147
- - A Ticket with `e2e` is admitted with `e2e` and `no_tests`: no tests are written first, and
148
- Transformers makes it with `builder.e2e` (its command, its Gates, for the first pass, a refused
159
+ one feature-test Ticket (`TicketEntry.featureTest`), blocked by every other live Ticket, or carries `skip_feature_test`.
160
+ - A Ticket with `feature-test` is admitted with `feature-test`, and
161
+ Transformers makes it with `feature.test` (its command, its Gates, for the first pass, a refused
149
162
  pass and the recheck) instead of the producer and the repair.
150
- - A Feature with `skip_e2e` has its e2e Ticket cancelled by the sweep (journal
151
- `e2e-ticket-cancelled`, a trace line), and Transformers leaves out, for that Feature, every
163
+ - A Feature with `skip_feature_test` has its feature-test Ticket cancelled by the sweep (journal
164
+ `feature-test-ticket-cancelled`, a trace line), and Transformers leaves out, for that Feature, every
152
165
  Gate of `feature.validate`, `feature.gates` and `feature.fix` that has `skipWhen:
153
- skip-test-e2e`: the journal gets `gate-skipped` with the Gate id and the reason, and the trace
154
- says "`<id>` not run: the Feature carries skip-test-e2e". The mark is read from the ledger as
166
+ skip-feature-test`: the journal gets `gate-skipped` with the Gate id and the reason, and the trace
167
+ says "`<id>` not run: the Feature carries skip-feature-test". The mark is read from the ledger as
155
168
  each pass starts (`TransformerSlots.marks`).
156
- - A Feature that is not marked and no longer holds a live e2e Ticket (it was cancelled while the
169
+ - A Feature that is not marked and no longer holds a live feature-test Ticket (it was cancelled while the
157
170
  mark was on) escalates when it closes.
158
171
 
159
172
  Host never drives an `escalated` unit: that state is frozen until a human `resume`.
@@ -221,7 +234,7 @@ the Ticket that was driven. The work line copy is refreshed onto the Feature's
221
234
  line before a Ticket starts: starting on a copy that was not fast-forwarded
222
235
  would build on a version that no longer exists.
223
236
 
224
- ## 8. init, setup, doctor, watch, status, log, cancel and release
237
+ ## 8. init, setup, doctor, watch, status, log, incident, cancel and release
225
238
 
226
239
  `init` writes a config that already runs: the Builder is a stub that says what to replace
227
240
  (example Cursor CLI and Claude Code Builders live in `@bluewombat/slots/builders/`),
@@ -229,14 +242,14 @@ and the Gate sequence starts empty. Example Gates live in
229
242
  `@bluewombat/slots/gates/` (`parent-clean`, `sensitive-path`,
230
243
  `workspace-changed`, `ci-green`).
231
244
 
232
- Four Gate sequences: `builder.gates`, shared by `builder.producer` and
233
- `builder.repair`, then `ticket.fix.gates`, `ticket.validate.gates` and
245
+ Four Gate sequences: `ticket.build.gates`, shared by `ticket.build.producer` and
246
+ `ticket.build.repair`, then `ticket.fix.gates`, `ticket.validate.gates` and
234
247
  `ticket.gates` for the one judgement-only pass with no producer of its own.
235
248
  The assembly sequences are not shared and nothing is dispatched between buckets: a check that is right for one pass can be wrong
236
249
  for another, even within the same stage — `workspace-changed` says an Attempt
237
- that changed nothing did nothing, which is true of `builder.producer`/`repair`
250
+ that changed nothing did nothing, which is true of `ticket.build.producer`/`repair`
238
251
  and false of `ticket.gates`'s pass, whose correct outcome is that nothing
239
- was left to do. A repair Attempt is judged by `builder.gates`, the same list as the
252
+ was left to do. A repair Attempt is judged by `ticket.build.gates`, the same list as the
240
253
  producer's (Host hands Implementer that list as both `gates` and `repairGates`). The assembly's
241
254
  judgement-only pass produces nothing unless something refused it first: it is
242
255
  a judgement, and a refusal is `ticket.fix`, not a second first-pass. Two
@@ -353,9 +366,16 @@ poll. A `listen` that did not complete is never held and never hidden.
353
366
  `watch` and `status` are the operator live view. They open the ledger the same
354
367
  way `run` does (`openFilesystemPersist` then `openWorkLedger`) and they do not
355
368
  open a manager, Conductor, or Host. `status` prints a snapshot of every known
356
- Ticket and exits. `watch` prints that snapshot, then follows new journal lines
369
+ Feature and Ticket and exits: a header with the counts, then one line per Feature
370
+ (key, state, title, the Submission or why it stopped) with its Tickets indented
371
+ under it, then the Tickets that belong to no Feature under `No Feature`. A Ticket
372
+ holding the feature tests is marked `[feature-test]`. Features that need a person
373
+ (`escalated`) come first, then those in progress, then `done` and `cancelled`.
374
+ Keys are ordered by tracker, then by number (`#18` before `#107`), the order
375
+ `incident` lists what waits for a person in too. Plain text, no colour. `watch` prints that snapshot, then follows new journal lines
357
376
  until SIGINT (exit 0). A missing journal is not an error: the snapshot still
358
- stands. `--json` prints the snapshot as JSON. Ctrl-C on watch leaves the worker
377
+ stands. `--json` prints the same tree: `{ features: [{ …, tickets: […] }], tickets: […] }`,
378
+ the latter holding the Tickets of no Feature. Ctrl-C on watch leaves the worker
359
379
  running. The file can grow without rotation. A followed line that records a
360
380
  stop includes the child's last words (`detail`, `reason`, a non-zero exit, a
361
381
  Gate report that did not pass). `run` prints that same note under the phase
@@ -372,6 +392,57 @@ slot was given `--transcript-dir` it names the newest transcript files with
372
392
  the agent exit and duration read from the start of each file. A missing
373
393
  journal is not an error. Exit 2 when the config or `--tail` cannot be read.
374
394
 
395
+ `incident [<key>]` gathers what is known about one stop into a single page, for a
396
+ person (or anything else) who has to understand it. Read-only: it opens the
397
+ ledger, the journal, the streams and the transcripts, and writes only where
398
+ `--out DIR` says (`incident.md` and `incident.json`). It does not open Host, a
399
+ manager or Conductor, so it works on a stopped run and on one still going. The
400
+ key is a full key or the tracker's number (`106`, `#106`) when one key ends so;
401
+ two matches name both and exit 2, no match exits 2. With no key it lists what
402
+ waits for a person: every escalated Feature and Ticket, one line each. `--json`
403
+ prints the page as JSON.
404
+
405
+ The page is the schema `bluewombat.incident/1`. It states, without judging:
406
+ the subject (kind, state, the first lines of what was asked, `blocked_by`); the
407
+ stop (layer, what the ledger says verbatim, who refused it, the Gate or agent
408
+ that ended the last round with its verdict, the Task and Attempt, the moment);
409
+ the stops of this subject in the journal and its resumes; the budgets spent;
410
+ the Tickets of the Feature, each with state, Attempts, pull request and the
411
+ moment it landed; the configuration of the refusing Gate; the system (bluewombat,
412
+ Node, the `@bluewombat/*` packages the Project installed, and what the
413
+ configuration switched on: trackers, ledger, Authority, tickets in parallel,
414
+ limits, which passes run an agent or only Gates, each Gate sequence, streams;
415
+ a command shows its program only, never its arguments, which may hold a key); the journal's events
416
+ of the last round; the stream files and the transcripts under the subject, with
417
+ the end of the refusing Gate's stream, each agent's exit, duration, closing
418
+ words and `MASON_VERDICT` / `MASON_BLOCKED` line; and `gaps`, what could not be
419
+ found. A reader must never read an absence as a "no": a missing journal, a
420
+ stream that was never filmed or is gone, a Gate no longer in the configuration
421
+ and a subject that is not stopped are each said in `gaps`. The last round is
422
+ what the journal holds between the previous stop of the subject (or its
423
+ beginning) and the last `status` of phase `escalated`; without that line the
424
+ page says so and takes the moment from the ledger. `--with-config` adds the
425
+ configuration file itself, with the value of any `key: value` line whose key
426
+ looks like a secret (token, secret, password, api key, credential) blanked. The
427
+ page quotes the ledger's
428
+ words with a fence longer than any backtick run in them.
429
+
430
+ An escalation records, beside its words, who refused it (`refused_by`: a Gate's
431
+ id or a role, when the refusal came with a name) and when (`at`, the ledger's
432
+ clock); a Feature's also records where its line stopped (`kind`: `assembly` for
433
+ the assembled line refused by a check, `submitted` for its offer). All are
434
+ optional: a record written before them stays readable, and the page then takes
435
+ the refuser from the last non-passing Gate whose words the ledger repeats. The
436
+ tracker is told the stage of a Feature's stop from that `kind` (`integrating`
437
+ for an assembled line, `submitting` otherwise).
438
+
439
+ `restart <feature-key>` resumes an escalated Feature with a fresh workspace: the
440
+ Feature's work is removed (the same `rm` as a cancel) and the ledger's `resumeFeature` is taken,
441
+ so the next pass cuts the work again from the line and judges it. Nothing is said to the tracker and
442
+ nothing is fetched. It takes the ledger lock — stop `run` first. The key is the full key or the
443
+ tracker number, as `incident` takes it. Exit 0 once restarted, 1 when the Feature is not
444
+ `escalated`, 2 for a missing, unknown or Ticket key. The journal gets `restarted`.
445
+
375
446
  `cancel <key>` abandons one Ticket the same way a `cancel` delivery does. The
376
447
  team still abandons in the TicketTracker; this is the hatch when the tracker
377
448
  cannot say the intention is gone (a manager with no `probe`, or an operator
@@ -423,10 +494,10 @@ positional: `release github:owner/name#19`.
423
494
  open, escalated or submitted. A Feature blocked by another does not open
424
495
  until it is `done`. A Feature whose Tickets are not all `task-ready` does
425
496
  not open.
426
- 16b. With the e2e check declared: a Feature without an e2e Ticket nor `skip_e2e` does not open and
427
- is reported `invalid`; with `skip_e2e` its e2e Ticket is cancelled and the e2e Gate is not run,
428
- the journal and the trace saying so; with an e2e Ticket the line is judged once by the suite,
429
- red goes to `feature.fix` and green lets the line be offered. A Project without `builder.e2e`
497
+ 16b. With the feature-test check declared: a Feature without a feature-test Ticket nor `skip_feature_test` does not open and
498
+ is reported `invalid`; with `skip_feature_test` its feature-test Ticket is cancelled and the feature-tests Gate is not run,
499
+ the journal and the trace saying so; with a feature-test Ticket the line is judged once by the suite,
500
+ red goes to `feature.fix` and green lets the line be offered. A Project without `feature.test`
430
501
  is unchanged.
431
502
  17. After an escalation, a `probe` of `gone` cancels that Ticket (or Feature)
432
503
  and the sweep goes on. `unavailable` leaves the
@@ -441,4 +512,13 @@ positional: `release github:owner/name#19`.
441
512
  themselves the failure. `--json` returns every kept line. It does
442
513
  not import `openHost`.
443
514
 
515
+ 20. `incident` on a Feature escalated by a Gate names that Gate, quotes the ledger
516
+ verbatim, lists the Feature's Tickets with their pull requests and says in
517
+ `gaps` what it could not read. On a Ticket that stopped on a Gate it names
518
+ the Gate and the Feature above it. With no key it lists what waits. It
519
+ opens no Host and writes nothing but `--out`.
520
+ 21. A Feature's line refused by a check that cannot be repaired escalates with
521
+ `refused_by` set to that check and `kind` `assembly`, and the tracker is told
522
+ `integrating`.
523
+
444
524
  How to install, configure and run: [README.md](./README.md).