@bluewombat/runtime 0.1.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 (151) hide show
  1. package/README.md +473 -0
  2. package/SPECS.md +303 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +172 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config/defaults.d.ts +17 -0
  8. package/dist/config/defaults.d.ts.map +1 -0
  9. package/dist/config/defaults.js +17 -0
  10. package/dist/config/defaults.js.map +1 -0
  11. package/dist/config/find-config.d.ts +9 -0
  12. package/dist/config/find-config.d.ts.map +1 -0
  13. package/dist/config/find-config.js +25 -0
  14. package/dist/config/find-config.js.map +1 -0
  15. package/dist/config/home.d.ts +17 -0
  16. package/dist/config/home.d.ts.map +1 -0
  17. package/dist/config/home.js +23 -0
  18. package/dist/config/home.js.map +1 -0
  19. package/dist/config/load-config.d.ts +13 -0
  20. package/dist/config/load-config.d.ts.map +1 -0
  21. package/dist/config/load-config.js +537 -0
  22. package/dist/config/load-config.js.map +1 -0
  23. package/dist/config/parse-args.d.ts +25 -0
  24. package/dist/config/parse-args.d.ts.map +1 -0
  25. package/dist/config/parse-args.js +478 -0
  26. package/dist/config/parse-args.js.map +1 -0
  27. package/dist/config/types.d.ts +144 -0
  28. package/dist/config/types.d.ts.map +1 -0
  29. package/dist/config/types.js +2 -0
  30. package/dist/config/types.js.map +1 -0
  31. package/dist/index.d.ts +30 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +16 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/loop/authority.d.ts +51 -0
  36. package/dist/loop/authority.d.ts.map +1 -0
  37. package/dist/loop/authority.js +166 -0
  38. package/dist/loop/authority.js.map +1 -0
  39. package/dist/loop/context.d.ts +44 -0
  40. package/dist/loop/context.d.ts.map +1 -0
  41. package/dist/loop/context.js +2 -0
  42. package/dist/loop/context.js.map +1 -0
  43. package/dist/loop/cursor.d.ts +4 -0
  44. package/dist/loop/cursor.d.ts.map +1 -0
  45. package/dist/loop/cursor.js +20 -0
  46. package/dist/loop/cursor.js.map +1 -0
  47. package/dist/loop/deliveries.d.ts +22 -0
  48. package/dist/loop/deliveries.d.ts.map +1 -0
  49. package/dist/loop/deliveries.js +240 -0
  50. package/dist/loop/deliveries.js.map +1 -0
  51. package/dist/loop/describe.d.ts +40 -0
  52. package/dist/loop/describe.d.ts.map +1 -0
  53. package/dist/loop/describe.js +48 -0
  54. package/dist/loop/describe.js.map +1 -0
  55. package/dist/loop/drive.d.ts +15 -0
  56. package/dist/loop/drive.d.ts.map +1 -0
  57. package/dist/loop/drive.js +42 -0
  58. package/dist/loop/drive.js.map +1 -0
  59. package/dist/loop/journal.d.ts +22 -0
  60. package/dist/loop/journal.d.ts.map +1 -0
  61. package/dist/loop/journal.js +49 -0
  62. package/dist/loop/journal.js.map +1 -0
  63. package/dist/loop/lock.d.ts +11 -0
  64. package/dist/loop/lock.d.ts.map +1 -0
  65. package/dist/loop/lock.js +60 -0
  66. package/dist/loop/lock.js.map +1 -0
  67. package/dist/loop/message.d.ts +34 -0
  68. package/dist/loop/message.d.ts.map +1 -0
  69. package/dist/loop/message.js +48 -0
  70. package/dist/loop/message.js.map +1 -0
  71. package/dist/loop/open-host.d.ts +13 -0
  72. package/dist/loop/open-host.d.ts.map +1 -0
  73. package/dist/loop/open-host.js +180 -0
  74. package/dist/loop/open-host.js.map +1 -0
  75. package/dist/loop/probe.d.ts +9 -0
  76. package/dist/loop/probe.d.ts.map +1 -0
  77. package/dist/loop/probe.js +57 -0
  78. package/dist/loop/probe.js.map +1 -0
  79. package/dist/loop/report.d.ts +26 -0
  80. package/dist/loop/report.d.ts.map +1 -0
  81. package/dist/loop/report.js +328 -0
  82. package/dist/loop/report.js.map +1 -0
  83. package/dist/loop/tick.d.ts +24 -0
  84. package/dist/loop/tick.d.ts.map +1 -0
  85. package/dist/loop/tick.js +169 -0
  86. package/dist/loop/tick.js.map +1 -0
  87. package/dist/loop/trace.d.ts +16 -0
  88. package/dist/loop/trace.d.ts.map +1 -0
  89. package/dist/loop/trace.js +49 -0
  90. package/dist/loop/trace.js.map +1 -0
  91. package/dist/loop/transformers.d.ts +27 -0
  92. package/dist/loop/transformers.d.ts.map +1 -0
  93. package/dist/loop/transformers.js +229 -0
  94. package/dist/loop/transformers.js.map +1 -0
  95. package/dist/loop/work-line.d.ts +73 -0
  96. package/dist/loop/work-line.d.ts.map +1 -0
  97. package/dist/loop/work-line.js +109 -0
  98. package/dist/loop/work-line.js.map +1 -0
  99. package/dist/operator/board.d.ts +31 -0
  100. package/dist/operator/board.d.ts.map +1 -0
  101. package/dist/operator/board.js +67 -0
  102. package/dist/operator/board.js.map +1 -0
  103. package/dist/operator/cancel.d.ts +19 -0
  104. package/dist/operator/cancel.d.ts.map +1 -0
  105. package/dist/operator/cancel.js +70 -0
  106. package/dist/operator/cancel.js.map +1 -0
  107. package/dist/operator/doctor.d.ts +15 -0
  108. package/dist/operator/doctor.d.ts.map +1 -0
  109. package/dist/operator/doctor.js +416 -0
  110. package/dist/operator/doctor.js.map +1 -0
  111. package/dist/operator/git-remote.d.ts +39 -0
  112. package/dist/operator/git-remote.d.ts.map +1 -0
  113. package/dist/operator/git-remote.js +78 -0
  114. package/dist/operator/git-remote.js.map +1 -0
  115. package/dist/operator/init.d.ts +28 -0
  116. package/dist/operator/init.d.ts.map +1 -0
  117. package/dist/operator/init.js +522 -0
  118. package/dist/operator/init.js.map +1 -0
  119. package/dist/operator/live.d.ts +19 -0
  120. package/dist/operator/live.d.ts.map +1 -0
  121. package/dist/operator/live.js +149 -0
  122. package/dist/operator/live.js.map +1 -0
  123. package/dist/operator/prompt.d.ts +35 -0
  124. package/dist/operator/prompt.d.ts.map +1 -0
  125. package/dist/operator/prompt.js +125 -0
  126. package/dist/operator/prompt.js.map +1 -0
  127. package/dist/operator/setup.d.ts +18 -0
  128. package/dist/operator/setup.d.ts.map +1 -0
  129. package/dist/operator/setup.js +62 -0
  130. package/dist/operator/setup.js.map +1 -0
  131. package/dist/plugins/discover.d.ts +13 -0
  132. package/dist/plugins/discover.d.ts.map +1 -0
  133. package/dist/plugins/discover.js +81 -0
  134. package/dist/plugins/discover.js.map +1 -0
  135. package/dist/plugins/isolation.d.ts +78 -0
  136. package/dist/plugins/isolation.d.ts.map +1 -0
  137. package/dist/plugins/isolation.js +64 -0
  138. package/dist/plugins/isolation.js.map +1 -0
  139. package/dist/plugins/load.d.ts +35 -0
  140. package/dist/plugins/load.d.ts.map +1 -0
  141. package/dist/plugins/load.js +109 -0
  142. package/dist/plugins/load.js.map +1 -0
  143. package/dist/plugins/manager.d.ts +43 -0
  144. package/dist/plugins/manager.d.ts.map +1 -0
  145. package/dist/plugins/manager.js +47 -0
  146. package/dist/plugins/manager.js.map +1 -0
  147. package/dist/plugins/persist.d.ts +33 -0
  148. package/dist/plugins/persist.d.ts.map +1 -0
  149. package/dist/plugins/persist.js +26 -0
  150. package/dist/plugins/persist.js.map +1 -0
  151. package/package.json +66 -0
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # runtime
2
+
3
+ Host: the process you run, `@bluewombat/runtime`, binary `mason`. It listens,
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:
6
+ [SPECS.md](./SPECS.md).
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @bluewombat/runtime @bluewombat/manager-github @bluewombat/slots
12
+ npx mason init
13
+ ```
14
+
15
+ `init` at a terminal asks its way through: which manager (only when more than
16
+ one is installed), what that manager needs, and where WorkLineStable is — it can
17
+ clone it, and refuses a branch the remote does not have. Then it writes the
18
+ files and runs `setup` itself, offering to apply what the tracker is missing.
19
+ The Builder is left for you.
20
+
21
+ ```bash
22
+ npx mason doctor
23
+ npx mason run
24
+ npx mason watch
25
+ ```
26
+
27
+ A script gets the same result with no questions — `--manager` turns them off:
28
+
29
+ ```bash
30
+ mason init --manager @bluewombat/manager-github \
31
+ --manager-option repo=owner/name \
32
+ --manager-option labels=mason \
33
+ --work-line-stable ./work-line-stable \
34
+ --clone git@github.com:owner/name.git --branch dev
35
+ mason setup --apply
36
+ mason doctor
37
+ mason run
38
+ ```
39
+
40
+ Each `--manager-option` is read by the question that would have asked for it, so
41
+ a malformed one stops `init` before it writes. `--clone` does what the wizard
42
+ offers: it checks the branch is really on the remote before cloning, falls back
43
+ to the remote's own default branch when `--branch` is left out, and leaves an
44
+ existing directory alone. `--interactive` forces the questions back on when
45
+ stdin is a pipe. `doctor` is the command whose exit code says whether a run will
46
+ work.
47
+
48
+ A GitHub Issues tracker is `@bluewombat/manager-github`, configured in that
49
+ package's README — Host does not grow a `--repo` flag for it.
50
+
51
+ ## Commands
52
+
53
+ | Command | Job |
54
+ | -------------- | -------------------------------------------------------------------------------------- |
55
+ | `mason run` | Drain the FeatureManager, run Conductor, report |
56
+ | `mason watch` | Snapshot the ledger and follow the journal. Ctrl-C leaves the worker running |
57
+ | `mason status` | Snapshot the ledger, then exit. `--json` for scripts |
58
+ | `mason init` | Set this directory up: asks, writes the config and the Builder stub, then runs `setup` |
59
+ | `mason setup` | Prepare the tracker. A plan by default; `--apply` writes. Exit 1 on a blocked step |
60
+ | `mason doctor` | Check the config, the slots, and the manager. Exit 1 on any failure |
61
+ | `mason cancel` | Abandon a Feature by key, when the tracker cannot say it is gone |
62
+
63
+ One `run` per ledger: `run` writes `<ledger>/lock` with its pid and a second
64
+ `run` on the same home refuses to start while that process lives (two runs
65
+ both listen, both drive, and the tracker hears everything twice). A lock a
66
+ dead process left behind is taken over.
67
+
68
+ `setup` is the only command that changes anything outside this machine, and only
69
+ with `--apply`. What it prepares is the manager's business — Host prints the
70
+ steps it is handed. A manager without `setupManager` says it has nothing to do.
71
+
72
+ `init` takes `--manager NAME` (which turns the questions off), `--manager-option KEY=VALUE`,
73
+ `--work-line-stable DIR`, `--clone REMOTE`, `--branch NAME`, `--force` and `--interactive`.
74
+
75
+ Leave `mason run` going in one terminal. In another:
76
+
77
+ ```bash
78
+ mason watch
79
+ ```
80
+
81
+ That is the operator surface: the durable state of every Feature, then the film
82
+ of the current step (`submitted` included). It does not talk to the tracker, and
83
+ it cannot `ready` or claim. `status` is the same snapshot without the
84
+ follow. `--json` prints that snapshot as JSON.
85
+
86
+ To abandon a Feature the tracker can no longer mention (a deleted issue, a
87
+ manager with no `probe`), stop `run` if it holds the ledger lock, then:
88
+
89
+ ```bash
90
+ mason cancel github:owner/name#19
91
+ ```
92
+
93
+ The film is `<ledger>/events.jsonl`, next to `cursor`. It is not truth after a
94
+ crash — the ledger is. The file can grow; there is no rotation yet.
95
+
96
+ ## Config
97
+
98
+ `mason.config.json`, found by walking up from the working directory. `--config
99
+ FILE` names another one. Relative paths resolve against the file; relative
100
+ flag paths resolve against the working directory. Flags override the file, and
101
+ `--manager-option` overrides one manager option without dropping the others.
102
+
103
+ ```json
104
+ {
105
+ "manager": "@bluewombat/manager-fake",
106
+ "managerOptions": { "source": "./.mason/source", "target": "./.mason/threads" },
107
+ "workLine": { "stable": "./work-line-stable", "isolation": "@bluewombat/isolation-copy" },
108
+ "planner": {
109
+ "cmd": ["node", "./node_modules/@bluewombat/slots/planners/one-subtask.mjs"],
110
+ "timeoutMs": 600000
111
+ },
112
+ "builder": {
113
+ "producer": { "cmd": ["node", "./mason-builder.mjs"], "timeoutMs": 600000 },
114
+ "repair": { "cmd": ["node", "./mason-repair.mjs"], "timeoutMs": 600000 },
115
+ "maxAttempts": 3,
116
+ "gates": { "defaultTimeoutMs": 120000, "gates": [] }
117
+ },
118
+ "assembly": { "gates": { "defaultTimeoutMs": 900000, "gates": [] } },
119
+ "authority": { "enabled": false },
120
+ "timeoutMs": 600000,
121
+ "pollIntervalMs": 30000
122
+ }
123
+ ```
124
+
125
+ | Field | Meaning |
126
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
127
+ | `manager` | Package name or path that exports `createManager` |
128
+ | `managerOptions` | Opaque to Host. What it holds is the manager's own documentation |
129
+ | `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` |
130
+ | `workLine.isolation` | Package name or path exporting `strategy` — required. Host loads it like a manager; Isolator/Integrator do not sniff |
131
+ | `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 |
132
+ | `workLine.branch` | The work line a fold lands on. Without it, what the reference names, or else whatever is checked out |
133
+ | `authority` | `enabled`, the `publish` / `refresh` slots, and the optional `describe` slot (§ Describer). Absent: the fold is local |
134
+ | `home` | Where Host keeps what is its own — default `./.mason` |
135
+ | `workspaceRoot` | Isolated feature / Subtask directories (created). Default `<home>/workspaces` |
136
+ | `ledger` | WorkLedger root (created). The Cursor is a file inside it. Default `<home>/ledger` |
137
+ | `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 |
138
+ | `planner` | `cmd` / `timeoutMs`. FeatureStandard in, a Plan out |
139
+ | `builder.producer` | `cmd` / `timeoutMs`. Spawned in the Subtask workspace. Exit 0 lets Gates run |
140
+ | `builder.repair` | The same, for a pass a Gate refused. **Required**, and never inherited |
141
+ | `builder.maxAttempts` | Attempts one Task may start. Default 3 |
142
+ | `builder.gates` | `defaultTimeoutMs` and `gates`: ordered checks on each Subtask |
143
+ | `assembly.fix` | `cmd` / `timeoutMs`. Spawned when a judgement of the whole refused it. Needs `authority.enabled` |
144
+ | `assembly.maxAttempts` | Attempts that one fix may start. Default 3 |
145
+ | `assembly.gates` | Ordered checks on the assembled feature. Empty: the Authority judges alone |
146
+ | `timeoutMs` | How long a child **outside** a Task may run: manager, isolations |
147
+ | `maxRefusals` | Times an Authority may send a Submission back before it escalates. Default 3 |
148
+ | `pollIntervalMs` | Set it and the process keeps draining until SIGINT. Absent: one tick |
149
+
150
+ Every `cmd` is named where it is used, and nothing falls back to a neighbour: a
151
+ `repair` with no command of its own is refused rather than quietly running the
152
+ first-pass agent again. `builder` makes a Subtask. `assembly` judges the whole
153
+ and, when that judgement refuses it, runs `assembly.fix` — not a second first-pass.
154
+
155
+ Every tracker field lives in `managerOptions`. Host does not read them.
156
+
157
+ ## Slots
158
+
159
+ Commands mason spawns and never imports: the Planner, the Builder, each Gate,
160
+ and — when an Authority is enabled — the Publisher and the Refresher. Every one
161
+ reads argv and answers with **one JSON object on stdout**; an exit code alone is
162
+ not the contract. mason does not pick a producer, and a slot may be written in
163
+ anything that can print a line.
164
+
165
+ Examples ship in [`@bluewombat/slots`](../../plugins/slots/README.md), installed with Host —
166
+ the shipped roles and agents (Cursor CLI, Claude Code), the shipped Gates, and
167
+ their options. What follows is what Host guarantees them.
168
+
169
+ ```json
170
+ "planner": {
171
+ "cmd": ["node", "./node_modules/@bluewombat/slots/planners/one-subtask.mjs"],
172
+ "timeoutMs": 600000
173
+ },
174
+ "builder": {
175
+ "producer": {
176
+ "cmd": [
177
+ "node", "./node_modules/@bluewombat/slots/builders/producer.mjs",
178
+ "--",
179
+ "node", "./node_modules/@bluewombat/slots/agents/claude.mjs"
180
+ ],
181
+ "timeoutMs": 600000
182
+ }
183
+ }
184
+ ```
185
+
186
+ `builder.repair` is required too, with its own `cmd` and `timeoutMs`. The
187
+ composition with `--` is the same.
188
+
189
+ ### Builder
190
+
191
+ Spawned in the Subtask workspace with `--intention`, `--definition-of-done`,
192
+ `--id`, `--attempt`, and `--context`. On a retry it also gets `--report` and
193
+ `--report-from`: what refused the last pass, and which Gate said it. Exit 0
194
+ means the Gates may run.
195
+
196
+ A Builder **leaves its work uncommitted in the workspace**. That is where
197
+ Integrator folds from, and it is what the Gates read: a commit moves `HEAD` and
198
+ hides the change from them.
199
+
200
+ The config always names a producer (and a repair). Implementer can still run a
201
+ Task as its Gate sequence alone — that is the first judgement of the assembled
202
+ feature, which makes nothing.
203
+
204
+ ### Assembly fix
205
+
206
+ Spawned in the feature workspace when a judgement of the whole refused it
207
+ (`--report`, `--report-from`). Same argv as a Builder otherwise. Host hands
208
+ Implementer `assembly.fix` as that pass's only agent: there is no first-pass
209
+ producer at this moment.
210
+
211
+ ### Gates
212
+
213
+ Two sequences, because a Subtask and the feature it assembles into are not the
214
+ same situation. `builder.gates` runs on each Subtask; `assembly.gates` on the
215
+ assembled feature. Put the Project's own checks in `builder.gates`, and fill
216
+ `assembly.gates` with what only makes sense on the whole — `ci-green` reads the
217
+ checks the work line ran on what the Publisher placed, so it belongs there.
218
+
219
+ Every Gate is told `--stage unit` or `--stage assembly` alongside `--id`,
220
+ `--attempt`, `--gate-id`, `--intention` and `--definition-of-done`. Most have no
221
+ use for it. `workspace-changed` does: "nothing changed" means nothing was done
222
+ in a unit, and is the correct outcome for an assembly that needed nothing.
223
+
224
+ `init` writes an empty list; add a Gate when the Project has a check.
225
+
226
+ Every child carries its own ceiling and no other. A Gate entry may name a
227
+ `timeoutMs`; without one it takes its sequence's `defaultTimeoutMs`, and a Gate
228
+ that ends up with neither is refused when the config loads. Nothing is derived
229
+ from what another child left behind, so a slow producer can no longer leave its
230
+ Gates a millisecond to answer in.
231
+
232
+ What bounds a Subtask is therefore what you wrote:
233
+ `builder.maxAttempts × (builder.producer.timeoutMs + Σ builder.gates timeouts)`.
234
+ A repair and an assembly fix each carry their own `timeoutMs`. An assembly fix
235
+ is bounded the same way from `assembly.maxAttempts` and `assembly.fix`.
236
+
237
+ ```json
238
+ {
239
+ "id": "ci-green",
240
+ "argv": [
241
+ "node",
242
+ "./node_modules/@bluewombat/slots/gates/ci-green.mjs",
243
+ "--token-env",
244
+ "MASON_GITHUB_TOKEN",
245
+ "--require-checks"
246
+ ],
247
+ "timeoutMs": 900000
248
+ }
249
+ ```
250
+
251
+ ### Planner
252
+
253
+ FeatureStandard in, a Plan out. It is given `--key`, `--intention`,
254
+ `--title` and `--max-units`. `init` writes the
255
+ bootstrap Planner — one Subtask that is the intention — from
256
+ `@bluewombat/slots/planners/one-subtask.mjs`.
257
+
258
+ ### Describer
259
+
260
+ How a delivered feature describes itself — the words the manager puts on the
261
+ Submission and on the fold — is the Project's, not Host's. Three levels, one
262
+ key, `authority.describe`:
263
+
264
+ | `authority.describe` | The Submission's description | Cost |
265
+ | -------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------- |
266
+ | not set | the manager's own default: the issue's title, its body as the human wrote it | none |
267
+ | `messages/conventional.mjs` | `<type>: <title>` (`--scope`, `--type`), the body as written | none |
268
+ | `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 |
269
+
270
+ It is an argv like `publish` and `refresh`, bounded by the global `timeoutMs`.
271
+ Host spawns it once per feature, in the feature workspace, when the work is
272
+ offered — with `--id`, `--title`, `--intention` and `--target` (the work line
273
+ it lands on) — and takes one line back: `{ "subject": "…", "body": "…" }`.
274
+ Host hands that description to the manager as is; what it becomes is the
275
+ manager's business (for GitHub: the squash's title and message, the pull
276
+ request's first paragraph). A slot that fails is skipped and the journal says
277
+ so.
278
+
279
+ Subtasks are never described: their folds are folded again into the
280
+ feature's, so their message is the Subtask's intention — its headline when
281
+ the Planner wrote one (a line, a blank line, the rest: the shape of a commit),
282
+ else its first sentence cut at a word, no markdown. That headline is also what
283
+ the tracker's Plan, the progress comments and the Submission's change list
284
+ show, so a Planner is asked to write one.
285
+
286
+ ### Write another slot
287
+
288
+ Anything that prints the right line. The plumbing a slot written in Node would
289
+ otherwise repeat is [`@bluewombat/slot-kit`](../slot-kit/README.md).
290
+
291
+
292
+ ## Submissions
293
+
294
+ Without an Authority, a finished feature is folded straight into WorkLineStable
295
+ and that is the end of it. With one, the assembled feature is **offered** to an
296
+ outside judge first, and only an `accepted` verdict lets it into the work line.
297
+
298
+ It is **declared**, never inferred:
299
+
300
+ ```json
301
+ "authority": {
302
+ "enabled": true,
303
+ "publish": ["node", "./node_modules/@bluewombat/slots/publishers/git.mjs"],
304
+ "refresh": ["node", "./node_modules/@bluewombat/slots/refreshers/git.mjs"]
305
+ }
306
+ ```
307
+
308
+ `enabled` says whether the Project has an outside judge at all. `publish` is the
309
+ slot that puts the work in front of it, and `refresh` the one that reads back
310
+ what it accepted — both are required when enabled. The manager must declare
311
+ `submit` and `fold` — `@bluewombat/manager-github` does. Enabled against a manager
312
+ that does not, or with nothing naming the work line it is offered to, is refused
313
+ before the run starts rather than quietly folded locally.
314
+
315
+ ### The reference work line
316
+
317
+ With an Authority, the reference work line lives where the Authority is, and the
318
+ directory here is only Host's copy of it. A manager that names that reference
319
+ (`referenceManager`, in words the Isolation strategy reads — `remote` and
320
+ `branch` for `@bluewombat/isolation-git`) spares the config from repeating them:
321
+ leave `workLine.stable` and `workLine.branch` out, and Host keeps the copy at
322
+ `<home>/work-line`, fetched on the first run, and refreshed before every pass that has work to drive. The
323
+ Refresher's `--target` and the Submission's target are read from the reference.
324
+
325
+ Everything about that copy is settled before a run writes anything: a key the
326
+ strategy does not know, a branch the remote does not have, a `workLine.branch`
327
+ that disagrees with the reference, or a `workLine.stable` that is not a copy of
328
+ it each stop `mason run` at boot, and `mason doctor` reports the same offline
329
+ (`reference`, `work line`). Without a reference, `workLine.stable` is required
330
+ and is the operator's directory, as before.
331
+
332
+ ```
333
+ assembled → Publisher places the work → manager opens the Submission
334
+ → submitted, and the run comes back to it on each poll
335
+ assembly.gates judge what was published
336
+ refused → assembly.fix, then the same Submission is offered again
337
+ accepted → the Authority folds it → done
338
+ ```
339
+
340
+ Neither half judges: that is what `assembly.gates` are for. The Authority places
341
+ and folds; a Gate says whether the fold may happen. After the
342
+ Authority folds, Host brings its copy of the work line up to date with a
343
+ fast-forward — the work line moved where the Authority is, and the next
344
+ Isolation must start from that version, not from a copy left behind.
345
+
346
+ A refusal is not a second failure path: its report reaches `assembly.fix`
347
+ exactly as a Gate's report reaches `builder.repair`. After `maxRefusals`
348
+ refusals the feature escalates with that report as its Trace, and `done` names
349
+ the Submission rather than a directory on the machine that ran.
350
+
351
+ ## Write another manager
352
+
353
+ Host loads whatever `manager` names, as long as it exports `createManager`. The
354
+ contract and a worked example are in
355
+ [`@bluewombat/manager-kit`](../manager-kit/README.md). Nothing in Host changes.
356
+
357
+ ## Import
358
+
359
+ ```ts
360
+ import { openHost } from "@bluewombat/runtime";
361
+
362
+ const host = await openHost({
363
+ manager: "@bluewombat/manager-fake",
364
+ managerOptions: { source: "./.mason/source", target: "./.mason/threads" },
365
+ // …paths and slots
366
+ });
367
+ await host.runOnce();
368
+ ```
369
+
370
+ `manager` also takes an already-loaded module, which is how a test drives Host
371
+ without resolving a package.
372
+
373
+ ## Stack
374
+
375
+ Repository stack — TypeScript, Node 24.20, Biome, `node:test`. See [docs/DEVELOPMENT.md](../../../docs/DEVELOPMENT.md).
376
+
377
+ ## Setup
378
+
379
+ From the repository root:
380
+
381
+ ```bash
382
+ nvm use
383
+ npm install
384
+ ```
385
+
386
+ ## Commands (development)
387
+
388
+ | Task | Command |
389
+ | --------- | ---------------- |
390
+ | Lint | `npm run lint` |
391
+ | Format | `npm run format` |
392
+ | Typecheck | `npm run tsc` |
393
+ | Test | `npm test` |
394
+ | Build | `npm run build` |
395
+
396
+ Binary after build: `dist/cli.js` (package bin name `mason`).
397
+
398
+ ## Layers
399
+
400
+ Host is the most tangled package by nature: it is where everything else meets.
401
+ So it is cut into four layers, and imports point one way only:
402
+
403
+ ```
404
+ operator → loop → plugins → config
405
+ ```
406
+
407
+ A file imports from its own layer or a lower one, never a higher one. That is
408
+ the whole rule. `scripts/check-host.mjs` enforces it in `npm run lint`, so a
409
+ change that breaks it does not merge.
410
+
411
+ | Layer | Holds | Does not hold | May import |
412
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------- |
413
+ | `config/` | What a Project wrote: argv, `mason.config.json`, discovery, `HostOptions`. Pure: JSON in, options out | A runtime value (a Journal, a Port, a process handle) | Node, `manager-kit` types |
414
+ | `plugins/` | Load a package by name and check its shape: a manager, an isolation strategy | What to do with the loaded thing | `config` |
415
+ | `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 |
416
+ | `operator/` | Tools for a human: `init`, `setup`, `doctor`, `watch` / `status`, `cancel`, `board`, the terminal prompt | Anything the loop needs — the loop cannot see this layer | everything below |
417
+ | `index.ts`, `cli.ts` | The package surface and the dispatch. Nothing else sits at the top of `src/` | Logic | everything |
418
+
419
+ Host's code names no plugin. A manager, an isolation strategy, a persistence
420
+ backend, the shipped slots: each is loaded by the name a config gives, through
421
+ `plugins/`, never imported. The names Host falls back to — the two isolation
422
+ strategies `init` writes, the default `persist`, the bootstrap Planner — are
423
+ strings in `config/defaults.ts` and nowhere else; `check-host.mjs` refuses an
424
+ import of any of them and a spelled-out name anywhere else. Host's
425
+ `package.json` declares exactly those defaults, so a config `init` wrote runs
426
+ with nothing else installed. A manager has no default, so it is not declared.
427
+
428
+ Two consequences worth stating, because they are the easy mistakes:
429
+
430
+ - A test hands `openHost` its film through the second argument
431
+ (`openHost(options, { journal })`), never through `HostOptions`. Options are
432
+ what a Project can write; a Journal is not.
433
+ - "Is this path a git work line?" (`inspectWorkLine`) belongs to the loop, and
434
+ `doctor` and `init` ask the loop. The remote helpers that only `init` and
435
+ `doctor` need (`git-remote.ts`) stay in `operator/`.
436
+
437
+ ### Working in Host
438
+
439
+ Each kind of change has one place, and the order below is the order that keeps
440
+ the layers true. Skipping ahead is how a package like this one tangles.
441
+
442
+ 1. **A change to the loop** touches `loop/` and its tests, nothing above. If it
443
+ needs a new value from the config, add the key in `config/` first, with its
444
+ parse test, then read it in `loop/`.
445
+ 2. **A new pluggable thing** is a `Shape` in `plugins/` — a name for refusals
446
+ and a `check` of the imported module — loaded by `plugins/load.ts` like the
447
+ others; a config key that names it; a name in `config/defaults.ts` when it
448
+ has a default; and a call from `loop/open-host.ts`. Host imports the
449
+ default package so the config `init` writes runs, and never imports a
450
+ plugin from code. The strategy's `refOf` is the model: the plugin says, Host
451
+ relays.
452
+ 3. **A new command for a human** is a file in `operator/`. It may read the
453
+ config, load plugins, and open the loop; it may not be needed by any of
454
+ them.
455
+ 4. **Splitting `operator/` into its own package** comes after all of these,
456
+ and only if it keeps growing. `init` needs the config writers and `doctor`
457
+ the plugin loaders, and the layer rule already gives the isolation a
458
+ package would.
459
+
460
+ ## Layout
461
+
462
+ ```
463
+ SPECS.md behavioural contract
464
+ README.md how to install, configure and run; the layers above
465
+ src/
466
+ index.ts the package surface
467
+ cli.ts mason run | watch | status | cancel | init | setup | doctor
468
+ config/ argv, JSON config, config discovery → HostOptions; defaults: the plugin names Host ships with
469
+ plugins/ load: the one loader; manager, isolation, persist: one shape each; discover
470
+ loop/ open-host (composition), tick (the loop), deliveries, drive, probe, report, authority, work-line, cursor, lock, journal, trace, transformers
471
+ operator/ init, setup, doctor, live (watch / status), cancel, board, prompt, git-remote
472
+ fixtures/ stand-in slots for the loop's tests
473
+ ```