@bluewombat/runtime 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +236 -142
  2. package/SPECS.md +138 -97
  3. package/dist/cli.js +35 -37
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/agent-shorthand.d.ts +24 -4
  6. package/dist/config/agent-shorthand.d.ts.map +1 -1
  7. package/dist/config/agent-shorthand.js +65 -28
  8. package/dist/config/agent-shorthand.js.map +1 -1
  9. package/dist/config/defaults.d.ts +0 -2
  10. package/dist/config/defaults.d.ts.map +1 -1
  11. package/dist/config/defaults.js +0 -2
  12. package/dist/config/defaults.js.map +1 -1
  13. package/dist/config/find-config.d.ts +3 -2
  14. package/dist/config/find-config.d.ts.map +1 -1
  15. package/dist/config/find-config.js +4 -4
  16. package/dist/config/find-config.js.map +1 -1
  17. package/dist/config/home.d.ts +2 -0
  18. package/dist/config/home.d.ts.map +1 -1
  19. package/dist/config/home.js +4 -2
  20. package/dist/config/home.js.map +1 -1
  21. package/dist/config/load-config.d.ts +2 -2
  22. package/dist/config/load-config.d.ts.map +1 -1
  23. package/dist/config/load-config.js +342 -73
  24. package/dist/config/load-config.js.map +1 -1
  25. package/dist/config/parse-args.d.ts +1 -1
  26. package/dist/config/parse-args.d.ts.map +1 -1
  27. package/dist/config/parse-args.js +132 -103
  28. package/dist/config/parse-args.js.map +1 -1
  29. package/dist/config/types.d.ts +142 -38
  30. package/dist/config/types.d.ts.map +1 -1
  31. package/dist/config/types.js +13 -1
  32. package/dist/config/types.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/loop/authority.d.ts +41 -5
  38. package/dist/loop/authority.d.ts.map +1 -1
  39. package/dist/loop/authority.js +68 -24
  40. package/dist/loop/authority.js.map +1 -1
  41. package/dist/loop/context.d.ts +23 -3
  42. package/dist/loop/context.d.ts.map +1 -1
  43. package/dist/loop/deliveries.d.ts +16 -16
  44. package/dist/loop/deliveries.d.ts.map +1 -1
  45. package/dist/loop/deliveries.js +35 -187
  46. package/dist/loop/deliveries.js.map +1 -1
  47. package/dist/loop/describe.d.ts +4 -6
  48. package/dist/loop/describe.d.ts.map +1 -1
  49. package/dist/loop/describe.js +2 -2
  50. package/dist/loop/describe.js.map +1 -1
  51. package/dist/loop/drive.d.ts +11 -7
  52. package/dist/loop/drive.d.ts.map +1 -1
  53. package/dist/loop/drive.js +24 -38
  54. package/dist/loop/drive.js.map +1 -1
  55. package/dist/loop/features.d.ts +23 -0
  56. package/dist/loop/features.d.ts.map +1 -0
  57. package/dist/loop/features.js +834 -0
  58. package/dist/loop/features.js.map +1 -0
  59. package/dist/loop/journal.d.ts +1 -1
  60. package/dist/loop/journal.js +2 -2
  61. package/dist/loop/journal.js.map +1 -1
  62. package/dist/loop/lock.d.ts +1 -1
  63. package/dist/loop/lock.js +1 -1
  64. package/dist/loop/open-host.d.ts.map +1 -1
  65. package/dist/loop/open-host.js +132 -31
  66. package/dist/loop/open-host.js.map +1 -1
  67. package/dist/loop/probe.d.ts +1 -1
  68. package/dist/loop/probe.d.ts.map +1 -1
  69. package/dist/loop/probe.js +51 -22
  70. package/dist/loop/probe.js.map +1 -1
  71. package/dist/loop/process-tree.d.ts +1 -1
  72. package/dist/loop/process-tree.js +1 -1
  73. package/dist/loop/report.d.ts +9 -7
  74. package/dist/loop/report.d.ts.map +1 -1
  75. package/dist/loop/report.js +49 -106
  76. package/dist/loop/report.js.map +1 -1
  77. package/dist/loop/streams.d.ts +5 -5
  78. package/dist/loop/streams.d.ts.map +1 -1
  79. package/dist/loop/streams.js +3 -3
  80. package/dist/loop/streams.js.map +1 -1
  81. package/dist/loop/tick.d.ts +6 -4
  82. package/dist/loop/tick.d.ts.map +1 -1
  83. package/dist/loop/tick.js +50 -78
  84. package/dist/loop/tick.js.map +1 -1
  85. package/dist/loop/trace.d.ts +5 -5
  86. package/dist/loop/trace.d.ts.map +1 -1
  87. package/dist/loop/trace.js +5 -14
  88. package/dist/loop/trace.js.map +1 -1
  89. package/dist/loop/transformers.d.ts +18 -14
  90. package/dist/loop/transformers.d.ts.map +1 -1
  91. package/dist/loop/transformers.js +139 -74
  92. package/dist/loop/transformers.js.map +1 -1
  93. package/dist/loop/warm.d.ts +3 -3
  94. package/dist/loop/warm.js +4 -4
  95. package/dist/loop/warm.js.map +1 -1
  96. package/dist/loop/work-line.d.ts +1 -1
  97. package/dist/operator/agent-plugins.d.ts +11 -0
  98. package/dist/operator/agent-plugins.d.ts.map +1 -1
  99. package/dist/operator/agent-plugins.js +17 -11
  100. package/dist/operator/agent-plugins.js.map +1 -1
  101. package/dist/operator/board.d.ts +3 -8
  102. package/dist/operator/board.d.ts.map +1 -1
  103. package/dist/operator/board.js +2 -11
  104. package/dist/operator/board.js.map +1 -1
  105. package/dist/operator/cancel.d.ts +2 -2
  106. package/dist/operator/cancel.js +6 -6
  107. package/dist/operator/cancel.js.map +1 -1
  108. package/dist/operator/doctor.d.ts +9 -0
  109. package/dist/operator/doctor.d.ts.map +1 -1
  110. package/dist/operator/doctor.js +110 -53
  111. package/dist/operator/doctor.js.map +1 -1
  112. package/dist/operator/explain.d.ts +76 -0
  113. package/dist/operator/explain.d.ts.map +1 -0
  114. package/dist/operator/explain.js +585 -0
  115. package/dist/operator/explain.js.map +1 -0
  116. package/dist/operator/film.d.ts +3 -11
  117. package/dist/operator/film.d.ts.map +1 -1
  118. package/dist/operator/film.js +8 -93
  119. package/dist/operator/film.js.map +1 -1
  120. package/dist/operator/init.d.ts.map +1 -1
  121. package/dist/operator/init.js +52 -55
  122. package/dist/operator/init.js.map +1 -1
  123. package/dist/operator/live.js +1 -1
  124. package/dist/operator/live.js.map +1 -1
  125. package/dist/operator/log.js +12 -6
  126. package/dist/operator/log.js.map +1 -1
  127. package/dist/operator/release.d.ts +2 -2
  128. package/dist/operator/release.js +7 -7
  129. package/dist/operator/release.js.map +1 -1
  130. package/dist/operator/run.d.ts +25 -0
  131. package/dist/operator/run.d.ts.map +1 -0
  132. package/dist/operator/run.js +70 -0
  133. package/dist/operator/run.js.map +1 -0
  134. package/dist/operator/setup.d.ts.map +1 -1
  135. package/dist/operator/setup.js +22 -16
  136. package/dist/operator/setup.js.map +1 -1
  137. package/dist/plugins/manager.d.ts +33 -2
  138. package/dist/plugins/manager.d.ts.map +1 -1
  139. package/dist/plugins/manager.js +67 -0
  140. package/dist/plugins/manager.js.map +1 -1
  141. package/package.json +15 -16
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Host: the process you run, `@bluewombat/runtime`, binary `mason`. It listens,
4
4
  admits, calls Conductor, and reports. It does not know a tracker: it loads one
5
- FeatureManager package and hands it the options it never reads. Contract:
5
+ TicketTracker package and hands it the options it never reads. Contract:
6
6
  [SPECS.md](./SPECS.md).
7
7
 
8
8
  ## Install
@@ -24,12 +24,11 @@ npx mason run
24
24
  npx mason watch
25
25
  ```
26
26
 
27
- A script gets the same result with no questions — `--manager` turns them off:
27
+ A script gets the same result with no questions — `--manager` turns them off (it names the one package that answers both sides):
28
28
 
29
29
  ```bash
30
30
  mason init --manager @bluewombat/manager-github \
31
31
  --manager-option repo=owner/name \
32
- --manager-option labels=mason \
33
32
  --work-line-stable ./work-line-stable \
34
33
  --clone git@github.com:owner/name.git --branch dev
35
34
  mason setup --apply
@@ -52,15 +51,43 @@ package's README — Host does not grow a `--repo` flag for it.
52
51
 
53
52
  | Command | Job |
54
53
  | --------------- | -------------------------------------------------------------------------------------- |
55
- | `mason run` | Drain the FeatureManager, run Conductor, report |
54
+ | `mason run` | Drain the TicketTracker, run Conductor, report |
56
55
  | `mason watch` | Snapshot the ledger and follow the journal. Ctrl-C leaves the worker running |
57
56
  | `mason status` | Snapshot the ledger, then exit. `--json` for scripts |
58
57
  | `mason log` | Replay the journal, with the reason a phase stopped. `--problems` for just those |
59
58
  | `mason init` | Set this directory up: asks, writes the config and the Builder stub, then runs `setup` |
60
59
  | `mason setup` | Prepare the tracker. A plan by default; `--apply` writes. Exit 1 on a blocked step |
61
60
  | `mason doctor` | Check the config, the slots, and the manager. Exit 1 on any failure |
62
- | `mason cancel` | Abandon a Feature by key, when the tracker cannot say it is gone |
63
- | `mason release` | Drop a held Subtask without waiting for the bail clock |
61
+ | `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
+ | `mason cancel` | Abandon a Ticket by key, when the tracker cannot say it is gone |
63
+ | `mason release` | Drop a held unit without waiting for the bail clock |
64
+
65
+ ### What an agent role starts with
66
+
67
+ ```bash
68
+ mason explain builder.producer # one role; no role names all of them
69
+ mason explain --json # the same facts for a script
70
+ ```
71
+
72
+ It reads the config, the role's prompt file, the plugin folders of the home, and — for the
73
+ project's side — the deck requests and locks of the work line as it was last refreshed. It runs
74
+ no agent and fetches nothing. Each line says where it comes from: `[home]` for the role you wrote
75
+ in the Mason home, `[base]` and `[profile]` for the project's deck setup, read from the
76
+ `ai-plugin.lock.json` the project committed (which lists the skills, agents, hooks and MCP servers
77
+ of each plugin, so the plugin does not have to be fetched to be described).
78
+
79
+ What cannot be known is said, not guessed:
80
+
81
+ - before the work line exists, the project's side is "not known yet";
82
+ - a request no committed lock pins, a prompt that does not place a required `{{placeholder}}` or
83
+ uses one the role does not fill, an unreadable prompt file, a missing plugin folder: `FAIL`
84
+ (exit 1), because every attempt of that role would fail the same way;
85
+ - a lock of a version it does not read, a missing profile, two plugins bringing a skill of the
86
+ same name: `warn`;
87
+ - the key of the agent is never printed, only whether its variable is set;
88
+ - a role written as a plain command is said to be one;
89
+ - the final prompt of a ticket, and what the agent's own tool reads from the checkout, are listed
90
+ as not covered.
64
91
 
65
92
  One `run` per ledger: `run` writes `<ledger>/lock` with its pid and a second
66
93
  `run` on the same home refuses to start while that process lives (two runs
@@ -74,15 +101,33 @@ steps it is handed. A manager without `setupManager` says it has nothing to do.
74
101
  `init` takes `--manager NAME` (which turns the questions off), `--manager-option KEY=VALUE`,
75
102
  `--work-line-stable DIR`, `--clone REMOTE`, `--branch NAME`, `--force` and `--interactive`.
76
103
 
104
+ `mason run` speaks before it does anything, so a terminal is never blank: it says that it
105
+ is starting, then the config, the trackers, the ledger and the poll interval, then that it is
106
+ opening the host and that it has started. A tick with work prints one line per step; a loop
107
+ with nothing to do says once a minute that it is polling; a tracker that cannot be read is
108
+ named on every poll. The last line says that it stopped and with which exit code, after the
109
+ error message on stderr when there was one.
110
+
111
+ ```
112
+ 10:28:50 run starting
113
+ 10:28:50 config /path/to/home
114
+ 10:28:50 trackers features @bluewombat/manager-github, tickets @bluewombat/manager-github
115
+ 10:28:50 ledger /path/to/home/.mason/ledger
116
+ 10:28:50 poll every 10s
117
+ 10:28:50 opening ledger, lock, work line and trackers
118
+ 10:28:51 started pid 4242, Ctrl-C to stop
119
+ 10:29:51 waiting nothing to do: 6 poll(s) in 60s
120
+ ```
121
+
77
122
  Leave `mason run` going in one terminal. In another:
78
123
 
79
124
  ```bash
80
125
  mason watch
81
126
  ```
82
127
 
83
- That is the operator surface: the durable state of every Feature, then the film
128
+ That is the operator surface: the durable state of every Ticket, then the film
84
129
  of the current step (`submitted` included). It does not talk to the tracker, and
85
- it cannot `ready` or claim. `status` is the same snapshot without the
130
+ it cannot `resume` or claim. `status` is the same snapshot without the
86
131
  follow. `--json` prints that snapshot as JSON.
87
132
 
88
133
  When a run only printed `unavailable:<key>`, the reason is already in the
@@ -95,25 +140,28 @@ mason log --problems
95
140
  `--all` keeps the idle polls. `--tail N` limits the text film (`0` shows every
96
141
  kept line). `--json` prints every kept line; with `--tail`, a truncated reply
97
142
  names how many were kept and how many are shown. The closing tally counts each
98
- distinct failure. A pause with no planner exit code is counted apart from a
99
- non-zero exit. If a slot was started with `--transcript-dir`, the newest
143
+ distinct failure. If a slot was started with `--transcript-dir`, the newest
100
144
  transcript files are named underneath, with the agent exit and duration read
101
145
  from the start of each file.
102
146
 
103
- To abandon a Feature the tracker can no longer mention (a deleted issue, a
147
+ To abandon a Ticket the tracker can no longer mention (a deleted issue, a
104
148
  manager with no `probe`), stop `run` if it holds the ledger lock, then:
105
149
 
106
150
  ```bash
107
151
  mason cancel github:owner/name#19
108
152
  ```
109
153
 
110
- After a crash left a Subtask `running` with nobody on it, stop `run` if it
111
- holds the lock, then:
154
+ A unit that was stopped (Ctrl-C, a crash) is picked up where it was by the next
155
+ `mason run`: its workspace is kept, the Attempts it finished still count and the
156
+ numbering goes on. To start a unit over instead (the line moved on, the work took
157
+ a wrong turn), stop `run` if it holds the lock, then:
112
158
 
113
159
  ```bash
114
160
  mason release github:owner/name#19
115
161
  ```
116
162
 
163
+ `release` destroys the workspace and forgets it; the next `run` isolates a fresh one.
164
+
117
165
  The film is `<ledger>/events.jsonl`, next to `cursor`. It is not truth after a
118
166
  crash — the ledger is. The file can grow; there is no rotation yet.
119
167
 
@@ -122,7 +170,7 @@ crash — the ledger is. The file can grow; there is no rotation yet.
122
170
  `mason.config.yaml`, found by walking up from the working directory. `--config
123
171
  FILE` names another one. Relative paths resolve against the file; relative
124
172
  flag paths resolve against the working directory. Flags override the file, and
125
- `--manager-option` overrides one manager option without dropping the others.
173
+ `--feature-tracker-option` and `--ticket-tracker-option` override one option of that side without dropping the others.
126
174
  Parsed with the `yaml` package (YAML 1.2 core schema), so a repeated key is
127
175
  refused rather than silently keeping the last one, and `no` / `on` / `off`
128
176
  stay plain strings — never coerced to a boolean the way YAML 1.1 would. A
@@ -130,33 +178,32 @@ value that starts with `@`, like every `@bluewombat/...` package name, needs
130
178
  quotes: `@` is a reserved YAML indicator at the start of a plain scalar.
131
179
 
132
180
  ```yaml
133
- manager: "@bluewombat/manager-fake"
134
- managerOptions:
135
- source: ./.mason/source
136
- target: ./.mason/threads
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
137
191
  workLine:
138
192
  stable: ./work-line-stable
139
193
  isolation: "@bluewombat/isolation-copy"
140
194
  warm:
141
195
  cmd: [pnpm, install, --frozen-lockfile]
142
196
  timeoutMs: 600000
143
- planner:
144
- cmd:
145
- - node
146
- - ./node_modules/@bluewombat/slots/planners/one-subtask.mjs
147
- timeoutMs: 600000
148
- gates: { defaultTimeoutMs: 120000, gates: [] }
149
197
  builder:
150
198
  producer:
151
199
  cmd: [node, ./mason-builder.mjs]
152
200
  timeoutMs: 600000
153
- gates: { defaultTimeoutMs: 120000, gates: [] }
154
201
  repair:
155
202
  cmd: [node, ./mason-repair.mjs]
156
203
  timeoutMs: 600000
157
- gates: { defaultTimeoutMs: 120000, gates: [] }
204
+ gates: { defaultTimeoutMs: 120000, gates: [] }
158
205
  maxAttempts: 3
159
- assembly:
206
+ ticket:
160
207
  gates: { defaultTimeoutMs: 900000, gates: [] }
161
208
  authority:
162
209
  enabled: false
@@ -166,40 +213,45 @@ pollIntervalMs: 10000
166
213
 
167
214
  | Field | Meaning |
168
215
  | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
169
- | `manager` | Package name or path that exports `createManager` |
170
- | `managerOptions` | Opaque to Host. What it holds is the manager's own documentation |
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 |
171
218
  | `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
+ | `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 |
172
220
  | `workLine.isolation` | Package name or path exporting `strategy` — required. Host loads it like a manager; Isolator/Integrator do not sniff |
173
- | `workLine.isolationOptions` | What that strategy is told, unread by Host — like `managerOptions`. `isolation-git` reads `exclude`, the paths a Child never gets (default `.env`, `.env.*`). A strategy without `createStrategy` refuses any |
174
- | `workLine.warm` | Optional `cmd` / `timeoutMs`. Runs once in the Feature workspace after Isolation, before any Subtask is copied from it — typically `pnpm install --frozen-lockfile`. Absent: Subtasks inherit a cold Feature |
221
+ | `workLine.isolationOptions` | What that strategy is told, unread by Host — like a manager's `options`. `isolation-git` reads `exclude`, the paths a Child never gets (default `.env`, `.env.*`). A strategy without `createStrategy` refuses any |
222
+ | `workLine.warm` | Optional `cmd` / `timeoutMs`. Runs in the workspace after Isolation, before the Builder — typically `pnpm install --frozen-lockfile`. Absent: the Builder gets a cold tree |
175
223
  | `workLine.branch` | The work line a fold lands on. Without it, what the reference names, or else whatever is checked out |
176
224
  | `authority` | `enabled`, the `publish` / `refresh` slots, and the optional `describe` slot (§ Describer). Absent: the fold is local |
177
225
  | `home` | Where Host keeps what is its own — default `./.mason` |
178
- | `workspaceRoot` | Isolated feature / Subtask directories (created). Default `<home>/workspaces` |
226
+ | `workspaceRoot` | Isolated directories (created). Default `<home>/workspaces` |
179
227
  | `ledger` | WorkLedger root (created). The Cursor is a file inside it. Default `<home>/ledger` |
180
228
  | `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 |
181
229
  | `agents` | Agents a pass names instead of writing `cmd` (§ Agent passes written short) |
182
- | `planner` | `cmd` / `timeoutMs` / `gates`. FeatureStandard in, a Plan out. Its own `gates` judges `workLineStable` once a Plan is otherwise accepted — not where the Planner agent itself reads from |
183
- | `builder.producer` | `cmd` / `timeoutMs` / `gates`. Spawned in the Subtask workspace. Exit 0 runs its own `gates` |
184
- | `builder.repair` | The same, for a pass a Gate refused. **Required**, and never inherited — including its `gates`: a repair with none of its own runs ungated, even when `builder.producer.gates` is not empty |
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 |
185
235
  | `builder.maxAttempts` | Attempts one Task may start. Default 3 |
186
- | `assembly.fix` | `cmd` / `timeoutMs` / `gates`. Spawned when a judgement of the whole refused it — by an Authority, by `assembly.validate`, or by both. Judged by its own `gates` |
187
- | `assembly.validate` | `cmd` / `timeoutMs` / `gates`. Optional. A local, read-only judge of the assembled feature, run after align, with or without an Authority. A refusal is repaired by `assembly.fix` the same as an Authority's. Judged by its own `gates`, never `builder`'s or `assembly.fix`'s |
188
- | `assembly.maxAttempts` | Attempts that one fix may start. Default 3 |
189
- | `assembly.gates` | `defaultTimeoutMs` and `gates`: the produce:false judgement-only pass — no producer of its own, re-checking the assembled feature. Empty: the Authority judges alone. Independent of `assembly.fix.gates` / `assembly.validate.gates` — never shared with either |
236
+ | `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. |
238
+ | `ticket.maxAttempts` | Attempts that one fix may start. Default 3 |
239
+ | `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 |
190
240
  | `timeoutMs` | How long a child **outside** a Task may run: manager, isolations |
191
- | `maxRefusals` | Times the work may be sent back before it escalates, by an Authority, by `assembly.validate`, or by both — one shared budget. Default 3 |
241
+ | `ticket.maxRefusals` | Times the work may be sent back before it escalates, by an Authority, by `ticket.validate`, or by both — one shared budget. Default 3 |
242
+ | `ticket.maxParallel` | How many Tickets of a Project are worked on at once. Default 1: one after the other. Above 1 needs `authority.enabled`; a Ticket waiting on a check or a person holds a place, the others go on. Tickets that wait on another (*Blocked by*) still start only once it is `done` |
243
+ | `feature` | Required with `authority.enabled`. How a Feature's line is delivered to the line it is folded into, by the same steps as a Ticket: the final line is merged into the Feature's work (`workLine.targetStable`, or Host's own copy of the reference), `validate` (same shape as `ticket.validate`) reviews the whole against the Feature's request and its `gates` (at least one) are where the project's checks on the whole line run, a refusal goes to `fix`, the line is published, and `gates` (at least one, the CI) judge it. `maxAttempts` and `maxRefusals` (default 3, same rule as a Ticket's) are its own. Nothing falls back to `ticket`. The merge itself is automatic only with `merge-when-ready`; the delivery to the Authority is for every Feature |
192
244
  | `pollIntervalMs` | Set it and the process keeps draining until SIGINT. Absent: one tick. Pick it with § Choosing `pollIntervalMs` — 10000 is the floor worth having |
193
- | `observability.streams` | `enabled`, `dir`, `keep`. Films what every child says, as it says it — one file per child under `<dir>/<feature>/`, named in the journal by a `stream-opened` line. `dir` resolves against the config file; default `<home>/streams`. `keep` is `stdout` / `stderr`, default both. Off unless `enabled` is true: a stream is the Project's own code and prompts in the clear |
245
+ | `observability.streams` | `enabled`, `dir`, `keep`. Films what every child says, as it says it — one file per child under `<dir>/<ticket>/`, named in the journal by a `stream-opened` line. `dir` resolves against the config file; default `<home>/streams`. `keep` is `stdout` / `stderr`, default both. Off unless `enabled` is true: a stream is the Project's own code and prompts in the clear |
194
246
 
195
247
  Every `cmd` is named where it is used, and nothing falls back to a neighbour: a
196
248
  `repair` with no command of its own is refused rather than quietly running the
197
- first-pass agent again. `builder` makes a Subtask. `assembly` judges the whole
198
- and, when that judgement refuses it, runs `assembly.fix` — not a second first-pass.
199
- `assembly.validate`, when declared, is a second, local judge of the whole: it
249
+ first-pass agent again. `builder` makes the unit of work. `ticket` judges the whole
250
+ and, when that judgement refuses it, runs `ticket.fix` — not a second first-pass.
251
+ `ticket.validate`, when declared, is a second, local judge of the whole: it
200
252
  runs after align, before the Authority sees the work (or before the final
201
- fold, with none). Its refusal is repaired by `assembly.fix` exactly as an
202
- Authority's is, and shares the same `maxRefusals` budget.
253
+ fold, with none). Its refusal is repaired by `ticket.fix` exactly as an
254
+ Authority's is, and shares the same `ticket.maxRefusals` budget.
203
255
 
204
256
  Paths a slot **writes** — `--transcript-dir`, `--agent-home` and
205
257
  `--plugins-out` — always resolve against the config file's directory, even
@@ -223,52 +275,47 @@ agents:
223
275
  denyShell: [] # commands refused to the agent, e.g. [gh, vercel]; ssh and scp always are
224
276
  transcripts: ./.mason/transcripts
225
277
  pluginsOut: ./.mason/plugins # where deck profiles are built
226
- planner:
227
- agent: cursor
228
- prompt: ./prompts/planner.md
229
- read: ./.mason/work-line # planner only
230
- plugins: [./ai-plugins/core/planner]
231
- profile: planner
232
- args: [--plan] # the vendor CLI's own flags
233
- timeoutMs: 600000
234
278
  builder:
235
279
  producer:
236
280
  agent: cursor
237
281
  prompt: ./prompts/builder.md
238
- plugins: [./ai-plugins/core/builder]
239
- profile: builder
282
+ homePlugins: [./ai-plugins/core/builder]
283
+ appDeckProfile: .ai/profiles/builder
284
+ args: [--mode, agent] # the vendor CLI's own flags
240
285
  timeoutMs: 1200000
241
286
  gates: { defaultTimeoutMs: 120000, gates: [] }
242
287
  ```
243
288
 
244
289
  Host writes the `cmd` it stands for — `node <slots>/<role>.mjs --prompt-file …
245
- -- node <slots>/agents/cursor.mjs --transcript-dir … --plugin-dir … --profile
290
+ -- node <slots>/agents/cursor.mjs --transcript-dir … --plugin-dir … --app-deck-profile
246
291
  … --plugins-out … --agent-arg …` — with `<slots>` the
247
292
  `node_modules/@bluewombat/slots` next to the config. Nothing past the config
248
- knows a pass was written short. The roles: `planner`, `builder.producer`,
249
- `builder.repair`, `assembly.fix`, `assembly.validate`, and `authority.describe`
250
- (an argv as before, or `{ agent, prompt?, plugins?, profile?, args? }`).
293
+ knows a pass was written short. The roles: `builder.producer`,
294
+ `builder.repair`, `ticket.fix`, `ticket.validate`, and `authority.describe`
295
+ (an argv, or `{ agent, prompt?, homePlugins?, appDeckProfile?, args? }`).
251
296
 
252
297
  | Key | Meaning |
253
298
  | --------- | --------------------------------------------------------------------------------------------------------- |
254
299
  | `agent` | A name `agents` declares. With `cmd` beside it, the pass is refused |
255
300
  | `prompt` | `--prompt-file` of the role slot. Absent: the role's built-in template |
256
301
  | `rules` | `--rules-file` of the role slot |
257
- | `read` | Planner only: the directory the Planner agent reads (`--read`) |
258
- | `plugins` | Plugin folders of the Mason home, each a `--plugin-dir` |
259
- | `profile` | The project's deck profile for this role. Needs the agent's `pluginsOut`. See the slots' § Agents/Plugins |
302
+ | `homePlugins` | Plugin folders of the Mason home, each a `--plugin-dir` |
303
+ | `appDeckProfile` | The app's deck profile for this role, as a path: `[<app dir>/].ai/profiles/<name>`. Needs the agent's `pluginsOut`. See the slots' § Agents/Plugins |
260
304
  | `args` | Appended to the agent's own `args`, each an `--agent-arg` |
261
305
 
262
- A `profile` is read in the project, not in the Mason home: the role gets the
263
- base plugins and that profile, as the project's committed deck locks pin them.
306
+ An `appDeckProfile` is read in the app, not in the Mason home: the role gets the
307
+ app's base plugins, that profile's plugins and its `prompt.md`, as the app's
308
+ committed deck locks pin them. deck keeps profiles in `<app>/.ai/profiles/<name>`,
309
+ so the path has that shape; `<app dir>` is where the app's `.ai/` is in the
310
+ checkout (`.` by default, `apps/web` in a monorepo). It stays inside the checkout.
264
311
  `mason doctor` checks those locks in the work line without the network.
265
312
 
266
- Every tracker field lives in `managerOptions`. Host does not read them.
313
+ Every tracker field lives in the `options` of its side. Host does not read them.
267
314
 
268
315
  ### Choosing `pollIntervalMs`
269
316
 
270
317
  The sleep between two ticks, and nothing else. While a tick drives work — a
271
- Planner, a Builder, a validate — it runs to the end; the interval does not
318
+ Builder, a validate — it runs to the end; the interval does not
272
319
  touch it. It only sets how long `mason run` takes to notice:
273
320
 
274
321
  - a new issue, or an edit to one;
@@ -276,7 +323,7 @@ touch it. It only sets how long `mason run` takes to notice:
276
323
  that submitted (`docs/DECISIONS.md`, the Authority re-drive row), so on a
277
324
  work line without CI the whole interval is waited for a Gate that answers in
278
325
  two seconds;
279
- - a `ready`, or an issue closed to cancel.
326
+ - a `resume`, or an issue closed to cancel.
280
327
 
281
328
  **What one tick costs**, with `@bluewombat/manager-github` (read in
282
329
  `src/loop/tick.ts` `runOnce`, `src/loop/probe.ts` `abandonGone`, the manager's
@@ -287,8 +334,8 @@ against the token's budget.
287
334
  | The tick | GitHub REST calls | Counted when nothing moved | Besides |
288
335
  | --------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ---------------------------------------------- |
289
336
  | Nothing to do | 1: the issues updated since the Cursor, 100 per page | 0 (304) | none: no fetch |
290
- | A Feature in flight | + 1 per Feature that can still be abandoned: is its issue still there | 0 (304) | a `git fetch` of the work line copy (not REST) |
291
- | A Feature `submitted` | + the judgement: `ci-green` asks for the pull request, its check runs and its statuses (about 3) | about 3: `ci-green` is its own process and keeps no `etag` | |
337
+ | A Ticket in flight | + 1 per Ticket that can still be abandoned: is its issue still there | 0 (304) | a `git fetch` of the work line copy (not REST) |
338
+ | A Ticket `submitted` | + the judgement: `ci-green` asks for the pull request, its check runs and its statuses (about 3) | about 3: `ci-green` is its own process and keeps no `etag` | |
292
339
 
293
340
  Reports to the tracker (a comment, a label move: 2 to 4 calls each) happen
294
341
  when the work moves, not per tick, and each changes the listing: the read
@@ -298,7 +345,7 @@ after a report is a full one.
298
345
  (GitHub's primary rate limit). It is shared by every caller using that token:
299
346
  the reports, `ci-green`, `gh`, and every other Mason home pointed at it.
300
347
 
301
- | `pollIntervalMs` | Idle, nothing moved | A Feature `submitted` (about 3 counted a tick) | Worst case, every read changed (about 5) |
348
+ | `pollIntervalMs` | Idle, nothing moved | A Ticket `submitted` (about 3 counted a tick) | Worst case, every read changed (about 5) |
302
349
  | ---------------- | ------------------- | ---------------------------------------------- | ---------------------------------------- |
303
350
  | 30000 | ~0 /h | 360 /h (7 %) | 600 /h (12 %) |
304
351
  | 15000 | ~0 /h | 720 /h (14 %) | 1,200 /h (24 %) |
@@ -312,14 +359,14 @@ listing. Without conditional reads an idle tick counted one call (360 /h at
312
359
  **The floor.** An idle tick takes 1 to 1.5 s, almost all of it the `listen`
313
360
  call (measured on `tilap/orangemonkey-site`, October 2026). Below 2 to 3 s
314
361
  the loop never sleeps. Below 10 s nothing a person notices is gained: a
315
- Feature takes minutes, and 10 s against 30 s saves about 20 s per Feature,
362
+ Ticket takes minutes, and 10 s against 30 s saves about 20 s per Ticket,
316
363
  mostly the post-Submission wait (30 s of a 5 min 40 s run, measured). Going
317
- lower spends the token while a Feature is submitted, for seconds nobody
364
+ lower spends the token while a Ticket is submitted, for seconds nobody
318
365
  notices.
319
366
 
320
- **So:** 10000 for one Mason home on a token. Idle no longer spends the
367
+ **So:** 10000 for one Mason home on a token. Idle does not spend the
321
368
  budget, so the quota is not what holds the floor: usefulness is, and a
322
- submitted Feature still is (its judgement counts). Several homes on the same
369
+ submitted Ticket still is (its judgement counts). Several homes on the same
323
370
  token add up: multiply the `submitted` and worst-case columns by their count
324
371
  and stay under half the budget. The journal is not a reason either way: quiet
325
372
  ticks are folded into one `idle` line a minute (`src/loop/journal.ts`).
@@ -330,7 +377,7 @@ settle or its timeout ends the attempt.
330
377
 
331
378
  ## Slots
332
379
 
333
- Commands mason spawns and never imports: the Planner, the Builder, each Gate,
380
+ Commands mason spawns and never imports: the Builder, each Gate,
334
381
  and — when an Authority is enabled — the Publisher and the Refresher. Every one
335
382
  reads argv and answers with **one JSON object on stdout**; an exit code alone is
336
383
  not the contract. mason does not pick a producer, and a slot may be written in
@@ -341,10 +388,6 @@ the shipped roles and agents (Cursor CLI, Claude Code), the shipped Gates, and
341
388
  their options. What follows is what Host guarantees them.
342
389
 
343
390
  ```json
344
- "planner": {
345
- "cmd": ["node", "./node_modules/@bluewombat/slots/planners/one-subtask.mjs"],
346
- "timeoutMs": 600000
347
- },
348
391
  "builder": {
349
392
  "producer": {
350
393
  "cmd": [
@@ -362,8 +405,8 @@ composition with `--` is the same.
362
405
 
363
406
  ### Builder
364
407
 
365
- Spawned in the Subtask workspace with `--intention`, `--definition-of-done`,
366
- `--id`, `--attempt`, and `--context`. On a retry it also gets `--report` and
408
+ Spawned in the workspace with `--intention`, `--id`, `--attempt`, and
409
+ `--context`. On a retry it also gets `--report` and
367
410
  `--report-from`: what refused the last pass, and which Gate said it. Exit 0
368
411
  means the Gates may run.
369
412
 
@@ -373,19 +416,19 @@ hides the change from them.
373
416
 
374
417
  The config always names a producer (and a repair). Implementer can still run a
375
418
  Task as its Gate sequence alone — that is the first judgement of the assembled
376
- feature, which makes nothing.
419
+ ticket, which makes nothing.
377
420
 
378
- ### Assembly fix
421
+ ### Ticket fix
379
422
 
380
- Spawned in the feature workspace when a judgement of the whole refused it
423
+ Spawned in the ticket workspace when a judgement of the whole refused it
381
424
  (`--report`, `--report-from`). Same argv as a Builder otherwise. Host hands
382
- Implementer `assembly.fix` as that pass's only agent: there is no first-pass
425
+ Implementer `ticket.fix` as that pass's only agent: there is no first-pass
383
426
  producer at this moment. The refusal it repairs may have come from an
384
- Authority or from `assembly.validate`.
427
+ Authority or from `ticket.validate`.
385
428
 
386
- ### Assembly validate
429
+ ### Ticket validate
387
430
 
388
- Spawned in the feature workspace after align, with or without an Authority —
431
+ Spawned in the ticket workspace after align, with or without an Authority —
389
432
  `--intention`, `--id`, `--attempt`, `--context`. No `--report`: it is a
390
433
  read-only review, not a repair, and takes nothing a producer would. Optional.
391
434
  Its verdict is not the exit code alone: the slot reads what the agent itself
@@ -394,24 +437,22 @@ that completes cleanly but finds a problem is still a refusal.
394
437
 
395
438
  ### Gates
396
439
 
397
- Five independent sequences, one per role — `planner`, `builder.producer`,
398
- `builder.repair`, `assembly.fix`, `assembly.validate` — plus `assembly.gates`
399
- for the one judgement-only pass with no producer of its own (re-checking the
400
- assembled feature; `ci-green` reads the checks the work line ran on what the
401
- Publisher placed, so it belongs there). No sharing, no dispatch choosing
402
- between two buckets: a check right for one pass is not necessarily right for
403
- another, even within the same stage. `builder.repair` is the sharpest edge —
404
- it does not inherit `builder.producer.gates`, so a repair Attempt with no
405
- Gate of its own runs ungated.
406
-
407
- Implementer's Gates are told `--stage unit` or `--stage assembly` alongside
408
- `--id`, `--attempt`, `--gate-id`, `--intention` and `--definition-of-done`.
440
+ Four sequences: `builder.gates`, shared by `builder.producer` and
441
+ `builder.repair` (a repair is judged by exactly what judged the first try),
442
+ then one per assembly role — `ticket.fix`, `ticket.validate` — plus
443
+ `ticket.gates` for the one judgement-only pass with no producer of its own
444
+ (re-checking the assembled ticket; `ci-green` reads the checks the work line
445
+ ran on what the Publisher placed, so it belongs there). The assembly lists are
446
+ not shared with each other or with the builder, and no dispatch chooses between
447
+ 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`.
450
+
451
+ Implementer's Gates are told `--stage unit` or `--stage ticket` alongside
452
+ `--id`, `--attempt`, `--gate-id` and `--intention`.
409
453
  Most have no use for it. `workspace-changed` does: "nothing changed" means
410
454
  nothing was done in a unit, and is the correct outcome for an assembly that
411
- needed nothing. FeatureBreakdown's Gates (`planner.gates`) are told only
412
- `--key` and `--gate-id` — no Attempt counter, no `--stage`, no
413
- `--definition-of-done` — so a Gate written for Implementer's shape is not a
414
- drop-in for `planner.gates`.
455
+ needed nothing.
415
456
 
416
457
  `init` writes an empty list on `builder.producer` and `builder.repair`; add a
417
458
  Gate when the Project has a check.
@@ -419,15 +460,15 @@ Gate when the Project has a check.
419
460
  Every child carries its own ceiling and no other. A Gate entry may name a
420
461
  `timeoutMs`; without one it takes its sequence's `defaultTimeoutMs`, and a Gate
421
462
  that ends up with neither is refused when the config loads. Nothing is derived
422
- from what another child left behind, so a slow producer can no longer leave its
463
+ from what another child left behind, so a slow producer cannot leave its
423
464
  Gates a millisecond to answer in.
424
465
 
425
- What bounds a Subtask is therefore what you wrote:
426
- `builder.maxAttempts × (builder.producer.timeoutMs + Σ builder.producer.gates timeouts)`
466
+ What bounds the unit is therefore what you wrote:
467
+ `builder.maxAttempts × (builder.producer.timeoutMs + Σ builder.gates timeouts)`
427
468
  for a first pass, and the same shape with `repair` in place of `producer` once
428
469
  a Gate has refused one.
429
470
  A repair and an assembly fix each carry their own `timeoutMs`. An assembly fix
430
- is bounded the same way from `assembly.maxAttempts` and `assembly.fix`.
471
+ is bounded the same way from `ticket.maxAttempts` and `ticket.fix`.
431
472
 
432
473
  ```json
433
474
  {
@@ -443,16 +484,63 @@ is bounded the same way from `assembly.maxAttempts` and `assembly.fix`.
443
484
  }
444
485
  ```
445
486
 
446
- ### Planner
487
+ ### The e2e acceptance check
488
+
489
+ A Project proves each Feature by an acceptance suite, run once on the assembled Feature
490
+ line, when its config says so, by a pair:
491
+
492
+ ```yaml
493
+ builder:
494
+ e2e: # makes the Ticket that holds the suite (label `e2e` on the tracker)
495
+ agent: claude
496
+ prompt: ./prompts/e2e.md # {{task}} is the scenarios; {{report}}, {{refused_by}}, {{refusal}} on a refused pass
497
+ timeoutMs: 1800000
498
+ gates: # form only: it compiles, one test per manifest id, no application file touched
499
+ gates:
500
+ - { id: e2e-form, argv: [...], timeoutMs: 120000 }
501
+ feature:
502
+ validate:
503
+ agent: false
504
+ gates:
505
+ defaultTimeoutMs: 120000
506
+ gates:
507
+ - id: e2e
508
+ skipWhen: skip-test-e2e # Host does not run this Gate for a Feature that carries the label
509
+ timeoutMs: 720000
510
+ argv:
511
+ - node
512
+ - ./node_modules/@bluewombat/slots/gates/e2e.mjs
513
+ - --up
514
+ - pnpm e2e:up
515
+ - --ready
516
+ - pnpm e2e:ready
517
+ - --run
518
+ - pnpm e2e
519
+ - --down
520
+ - pnpm e2e:down
521
+ ```
522
+
523
+ `skipWhen` is the one condition Host reads for a Gate, on a Feature's Gates only
524
+ (`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.
528
+
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
532
+ 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
534
+ Ticket is not blocked by every other Ticket, as `invalid`, with the reason as a comment.
447
535
 
448
- FeatureStandard in, a Plan out. It is given `--key`, `--intention`,
449
- `--title` and `--max-units`. `init` writes the
450
- bootstrap Planner — one Subtask that is the intention — from
451
- `@bluewombat/slots/planners/one-subtask.mjs`.
536
+ What the Gate proves is a red/green verdict; what the operator reads is its text in the
537
+ journal (`gate-finished`) — a red suite is also in the reason of the refusal the fix is
538
+ given and in the escalation. A passing suite's text is not shown on the pull request: see
539
+ RESERVATIONS I17.
452
540
 
453
541
  ### Describer
454
542
 
455
- How a delivered feature describes itself — the words the manager puts on the
543
+ How a delivered ticket describes itself — the words the manager puts on the
456
544
  Submission and on the fold — is the Project's, not Host's. Three levels, one
457
545
  key, `authority.describe`:
458
546
 
@@ -460,10 +548,10 @@ key, `authority.describe`:
460
548
  | -------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------- |
461
549
  | not set | the manager's own default: the issue's title, its body as the human wrote it | none |
462
550
  | `messages/conventional.mjs` | `<type>: <title>` (`--scope`, `--type`), the body as written | none |
463
- | `messages/git-producer.mjs -- <agent>` | an agent reads the git diff and writes both under `--rules-file` — the team's guideline | one agent run per feature |
551
+ | `messages/git-producer.mjs -- <agent>` | an agent reads the git diff and writes both under `--rules-file` — the team's guideline | one agent run per ticket |
464
552
 
465
553
  It is an argv like `publish` and `refresh`, bounded by the global `timeoutMs`.
466
- Host spawns it once per feature, in the feature workspace, when the work is
554
+ Host spawns it once per ticket, in the ticket workspace, when the work is
467
555
  offered — with `--id`, `--title`, `--intention` and `--target` (the work line
468
556
  it lands on) — and takes one line back: `{ "subject": "…", "body": "…" }`.
469
557
  Host hands that description to the manager as is; what it becomes is the
@@ -471,12 +559,9 @@ manager's business (for GitHub: the squash's title and message, the pull
471
559
  request's first paragraph). A slot that fails is skipped and the journal says
472
560
  so.
473
561
 
474
- Subtasks are never described: their folds are folded again into the
475
- feature's, so their message is the Subtask's intention — its headline when
476
- the Planner wrote one (a line, a blank line, the rest: the shape of a commit),
477
- else its first sentence cut at a word, no markdown. That headline is also what
478
- the tracker's Plan, the progress comments and the Submission's change list
479
- show, so a Planner is asked to write one.
562
+ The commit that takes up what the Builder made is worded from the title of the
563
+ issue (or the headline of its intention: a line, a blank line, the rest — the
564
+ shape of a commit), else its first sentence cut at a word, no markdown.
480
565
 
481
566
  ### Write another slot
482
567
 
@@ -486,8 +571,8 @@ otherwise repeat is [`@bluewombat/slot-kit`](../slot-kit/README.md).
486
571
 
487
572
  ## Submissions
488
573
 
489
- Without an Authority, a finished feature is folded straight into WorkLineStable
490
- and that is the end of it. With one, the assembled feature is **offered** to an
574
+ Without an Authority, a finished ticket is folded straight into WorkLineStable
575
+ and that is the end of it. With one, the assembled ticket is **offered** to an
491
576
  outside judge first, and only an `accepted` verdict lets it into the work line.
492
577
 
493
578
  It is **declared**, never inferred:
@@ -522,34 +607,37 @@ strategy does not know, a branch the remote does not have, a `workLine.branch`
522
607
  that disagrees with the reference, or a `workLine.stable` that is not a copy of
523
608
  it each stop `mason run` at boot, and `mason doctor` reports the same offline
524
609
  (`reference`, `work line`). Without a reference, `workLine.stable` is required
525
- and is the operator's directory, as before.
610
+ and is the operator's directory.
526
611
 
527
612
  ```
528
- assembled → align → assembly.validate (optional, local)
529
- refused → assembly.fix, then align again, then validate again
613
+ assembled → align → recheck (builder.gates again, on the aligned work)
614
+ conflict or failed → ticket.fix, then align again
615
+ → ticket.validate (optional, local)
616
+ refused → ticket.fix, then align again, recheck, validate again
530
617
  accepted → Publisher places the work → manager opens the Submission
531
618
  → submitted, and the run comes back to it on each poll
532
- assembly.gates judge what was published
533
- refused → assembly.fix, then the same Submission is offered again
534
- accepted → the Authority folds it → done
619
+ ticket.gates judge what was published
620
+ refused → ticket.fix, then the same Submission is offered again
621
+ accepted → has the line moved on since the align? yes: align again
622
+ no: the Authority folds it → done (a refused fold also aligns again)
535
623
  ```
536
624
 
537
- Neither half judges: that is what `assembly.gates` are for. The Authority places
538
- and folds; a Gate says whether the fold may happen. `assembly.validate`, when
625
+ Neither half judges: that is what `ticket.gates` are for. The Authority places
626
+ and folds; a Gate says whether the fold may happen. `ticket.validate`, when
539
627
  declared, judges before either half — the same verdict has the same fate
540
628
  whether or not there is an Authority to offer the work to afterwards. After the
541
629
  Authority folds, Host brings its copy of the work line up to date with a
542
630
  fast-forward — the work line moved where the Authority is, and the next
543
631
  Isolation must start from that version, not from a copy left behind.
544
632
 
545
- A refusal is not a second failure path: its report reaches `assembly.fix`
546
- exactly as a Gate's report reaches `builder.repair`. After `maxRefusals`
547
- refusals the feature escalates with that report as its Trace, and `done` names
633
+ 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`
635
+ refusals the ticket escalates with that report as its Trace, and `done` names
548
636
  the Submission rather than a directory on the machine that ran.
549
637
 
550
638
  ## Write another manager
551
639
 
552
- Host loads whatever `manager` names, as long as it exports `createManager`. The
640
+ Host loads whatever `featureTracker.package` and `ticketTracker.package` name, as long as it exports `createManager`. The
553
641
  contract and a worked example are in
554
642
  [`@bluewombat/manager-kit`](../manager-kit/README.md). Nothing in Host changes.
555
643
 
@@ -559,14 +647,20 @@ contract and a worked example are in
559
647
  import { openHost } from "@bluewombat/runtime";
560
648
 
561
649
  const host = await openHost({
562
- manager: "@bluewombat/manager-fake",
563
- managerOptions: { source: "./.mason/source", target: "./.mason/threads" },
650
+ featureTracker: {
651
+ package: "@bluewombat/manager-fake",
652
+ options: { source: "./.mason/source", target: "./.mason/threads" },
653
+ },
654
+ ticketTracker: {
655
+ package: "@bluewombat/manager-fake",
656
+ options: { source: "./.mason/source", target: "./.mason/threads" },
657
+ },
564
658
  // …paths and slots
565
659
  });
566
660
  await host.runOnce();
567
661
  ```
568
662
 
569
- `manager` also takes an already-loaded module, which is how a test drives Host
663
+ Each `package` also takes an already-loaded module, which is how a test drives Host
570
664
  without resolving a package.
571
665
 
572
666
  ## Stack
@@ -618,7 +712,7 @@ change that breaks it does not merge.
618
712
  Host's code names no plugin. A manager, an isolation strategy, a persistence
619
713
  backend, the shipped slots: each is loaded by the name a config gives, through
620
714
  `plugins/`, never imported. The names Host falls back to — the two isolation
621
- strategies `init` writes, the default `persist`, the bootstrap Planner — are
715
+ strategies `init` writes, the default `persist` — are
622
716
  strings in `config/defaults.ts` and nowhere else; `check-host.mjs` refuses an
623
717
  import of any of them and a spelled-out name anywhere else. Host's
624
718
  `package.json` declares exactly those defaults, so a config `init` wrote runs
@@ -667,6 +761,6 @@ src/
667
761
  config/ argv, JSON config, config discovery → HostOptions; defaults: the plugin names Host ships with
668
762
  plugins/ load: the one loader; manager, isolation, persist: one shape each; discover
669
763
  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
670
- operator/ init, setup, doctor, live (watch / status), log, film, cancel, release, board, prompt, git-remote
764
+ operator/ run, init, setup, doctor, explain, live (watch / status), log, film, cancel, release, board, prompt, git-remote
671
765
  fixtures/ stand-in slots for the loop's tests
672
766
  ```