cloving 1.4.0 → 1.5.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 (167) hide show
  1. package/README.md +100 -15
  2. package/dist/src/broker/actor/authority-health.d.ts +9 -0
  3. package/dist/src/broker/actor/authority-health.js +24 -0
  4. package/dist/src/broker/actor/authority-runtime.d.ts +4 -2
  5. package/dist/src/broker/actor/authority-runtime.js +29 -20
  6. package/dist/src/broker/actor/authority-session.d.ts +6 -19
  7. package/dist/src/broker/actor/authority-session.js +66 -88
  8. package/dist/src/broker/actor/authority-settings.d.ts +2 -1
  9. package/dist/src/broker/actor/authority-settings.js +4 -1
  10. package/dist/src/broker/actor/authority-upgrades.d.ts +5 -1
  11. package/dist/src/broker/actor/authority-upgrades.js +2 -1
  12. package/dist/src/broker/actor/authority-watchdog.d.ts +6 -2
  13. package/dist/src/broker/actor/authority-watchdog.js +31 -9
  14. package/dist/src/broker/actor/authority.js +3 -1
  15. package/dist/src/broker/actor/projection-memory.d.ts +43 -0
  16. package/dist/src/broker/actor/projection-memory.js +207 -0
  17. package/dist/src/broker/actor/runtime-parts.d.ts +5 -0
  18. package/dist/src/broker/actor/runtime-parts.js +8 -3
  19. package/dist/src/broker/authority-restore.js +1 -1
  20. package/dist/src/broker/effect-upgrade.d.ts +1 -0
  21. package/dist/src/broker/effect-upgrade.js +4 -1
  22. package/dist/src/cli/arguments.js +1 -0
  23. package/dist/src/cli/client.d.ts +3 -1
  24. package/dist/src/cli/client.js +4 -4
  25. package/dist/src/cli/command-requests.d.ts +30 -1
  26. package/dist/src/cli/command-requests.js +43 -2
  27. package/dist/src/cli/commands.js +57 -48
  28. package/dist/src/cli/config-notice.js +5 -2
  29. package/dist/src/cli/doctor-retention.d.ts +2 -0
  30. package/dist/src/cli/doctor-retention.js +93 -0
  31. package/dist/src/cli/doctor.js +5 -0
  32. package/dist/src/cli/init-prompt.js +1 -1
  33. package/dist/src/cli/mcp-server.js +4 -4
  34. package/dist/src/cli/mcp-tools.d.ts +14 -14
  35. package/dist/src/cli/mcp-tools.js +212 -50
  36. package/dist/src/cli/queue-arguments.d.ts +15 -0
  37. package/dist/src/cli/queue-arguments.js +31 -0
  38. package/dist/src/cli/queue-render.d.ts +3 -0
  39. package/dist/src/cli/queue-render.js +26 -0
  40. package/dist/src/cli/reload-render.d.ts +2 -0
  41. package/dist/src/cli/reload-render.js +12 -0
  42. package/dist/src/cli/render.js +37 -8
  43. package/dist/src/cli/report-github-reader.js +1 -1
  44. package/dist/src/cli/runtime-arguments.d.ts +17 -3
  45. package/dist/src/cli/runtime-arguments.js +23 -19
  46. package/dist/src/cli/toolchain.d.ts +1 -0
  47. package/dist/src/cli/toolchain.js +1 -0
  48. package/dist/src/cli/usage.d.ts +2 -2
  49. package/dist/src/cli/usage.js +40 -5
  50. package/dist/src/cli/watch-api-document.d.ts +430 -0
  51. package/dist/src/cli/watch-api-document.js +291 -0
  52. package/dist/src/cli/watch-api-run.d.ts +56 -0
  53. package/dist/src/cli/watch-api-run.js +112 -0
  54. package/dist/src/cli/watch-api.d.ts +17 -0
  55. package/dist/src/cli/watch-api.js +230 -0
  56. package/dist/src/cli/watch-header-view.d.ts +1 -0
  57. package/dist/src/cli/watch-header-view.js +9 -1
  58. package/dist/src/cli/watch-model.d.ts +1 -0
  59. package/dist/src/cli/watch-model.js +3 -0
  60. package/dist/src/cli/watch-page.js +3 -1
  61. package/dist/src/cli/watch-reads.d.ts +1 -0
  62. package/dist/src/cli/watch-reads.js +6 -5
  63. package/dist/src/cli/watch-render.d.ts +1 -0
  64. package/dist/src/cli/watch-server.js +18 -4
  65. package/dist/src/cli.js +50 -43
  66. package/dist/src/configuration-display.js +2 -0
  67. package/dist/src/configuration-schema.d.ts +5 -0
  68. package/dist/src/daemon/claude-recovery.js +13 -9
  69. package/dist/src/daemon/claude-worker.d.ts +3 -3
  70. package/dist/src/daemon/claude-worker.js +10 -11
  71. package/dist/src/daemon/daemon-adapters.d.ts +7 -0
  72. package/dist/src/daemon/daemon-adapters.js +7 -3
  73. package/dist/src/daemon/daemon-loops.d.ts +2 -0
  74. package/dist/src/daemon/daemon-loops.js +17 -11
  75. package/dist/src/daemon/daemon.js +71 -60
  76. package/dist/src/daemon/github-attach.js +77 -4
  77. package/dist/src/daemon/github-calls.js +1 -1
  78. package/dist/src/daemon/github-methods.js +6 -2
  79. package/dist/src/daemon/issue-queue.d.ts +3 -0
  80. package/dist/src/daemon/issue-queue.js +66 -73
  81. package/dist/src/daemon/merge-turns.d.ts +10 -0
  82. package/dist/src/daemon/merge-turns.js +37 -0
  83. package/dist/src/daemon/queue-control.d.ts +13 -0
  84. package/dist/src/daemon/queue-control.js +86 -0
  85. package/dist/src/daemon/queue-journal.d.ts +20 -0
  86. package/dist/src/daemon/queue-journal.js +89 -0
  87. package/dist/src/daemon/record-archive.d.ts +19 -0
  88. package/dist/src/daemon/record-archive.js +82 -0
  89. package/dist/src/daemon/record-sweep.d.ts +24 -0
  90. package/dist/src/daemon/record-sweep.js +147 -0
  91. package/dist/src/daemon/retrying-turn.js +4 -4
  92. package/dist/src/daemon/settings-reload.d.ts +26 -0
  93. package/dist/src/daemon/settings-reload.js +82 -0
  94. package/dist/src/daemon/turn-details.d.ts +4 -0
  95. package/dist/src/daemon/turn-details.js +33 -0
  96. package/dist/src/daemon/worker-profile.d.ts +1 -0
  97. package/dist/src/daemon/worker-profile.js +4 -0
  98. package/dist/src/daemon/workflow-generations.d.ts +7 -4
  99. package/dist/src/daemon/workflow-generations.js +22 -14
  100. package/dist/src/daemon/workflow-reload.d.ts +41 -0
  101. package/dist/src/daemon/workflow-reload.js +244 -0
  102. package/dist/src/daemon/workflow-replay.d.ts +2 -1
  103. package/dist/src/daemon/workflow-replay.js +7 -3
  104. package/dist/src/doctor-report.d.ts +2 -2
  105. package/dist/src/doctor-report.js +2 -0
  106. package/dist/src/private-config.schema.json +31 -0
  107. package/dist/src/project-settings.d.ts +15 -1
  108. package/dist/src/project-settings.js +33 -1
  109. package/dist/src/queue-state.d.ts +26 -0
  110. package/dist/src/queue-state.js +34 -0
  111. package/dist/src/record-retention.d.ts +46 -0
  112. package/dist/src/record-retention.js +81 -0
  113. package/dist/src/service/active-tickets.js +5 -0
  114. package/dist/src/service/dispatcher.d.ts +4 -0
  115. package/dist/src/service/dispatcher.js +33 -0
  116. package/dist/src/service/queue-handlers.d.ts +4 -0
  117. package/dist/src/service/queue-handlers.js +32 -0
  118. package/dist/src/service/registry.d.ts +28 -2
  119. package/dist/src/service/registry.js +48 -0
  120. package/dist/src/service/replies.d.ts +16 -9
  121. package/dist/src/service/replies.js +5 -2
  122. package/dist/src/service/run-handlers.js +15 -10
  123. package/dist/src/service/service.d.ts +7 -3
  124. package/dist/src/service/service.js +16 -31
  125. package/dist/src/service/worker-totals.d.ts +4 -0
  126. package/dist/src/service/worker-totals.js +7 -0
  127. package/dist/src/service/workflow-reload.d.ts +35 -0
  128. package/dist/src/service/workflow-reload.js +52 -0
  129. package/dist/src/storage/decision-snapshots.d.ts +1 -2
  130. package/dist/src/storage/decision-snapshots.js +1 -7
  131. package/dist/src/storage/domain-changes.d.ts +1 -1
  132. package/dist/src/storage/domain-changes.js +1 -0
  133. package/dist/src/storage/domain-schema.d.ts +4 -1
  134. package/dist/src/storage/domain-schema.js +6 -2
  135. package/dist/src/storage/effect-schema.d.ts +1 -0
  136. package/dist/src/storage/effect-schema.js +4 -0
  137. package/dist/src/storage/projection-changes.d.ts +12 -0
  138. package/dist/src/storage/projection-changes.js +194 -0
  139. package/dist/src/storage/projection-lookups.d.ts +7 -0
  140. package/dist/src/storage/projection-lookups.js +30 -0
  141. package/dist/src/workflow-declaration.d.ts +1 -0
  142. package/dist/src/workflow-declaration.js +3 -0
  143. package/dist/tools/compile-workflows.js +3 -0
  144. package/dist/tools/project-configuration.d.ts +7 -2
  145. package/dist/tools/project-configuration.js +30 -1
  146. package/dist/tools/self-host-processes.js +37 -13
  147. package/dist/tools/self-host-project.d.ts +2 -0
  148. package/dist/tools/self-host-project.js +21 -3
  149. package/dist/tools/self-host.js +34 -24
  150. package/dist/tools/workflow-candidate-program.d.ts +4 -2
  151. package/dist/tools/workflow-candidate-program.js +30 -11
  152. package/dist/tools/workflow-candidate-source.d.ts +1 -0
  153. package/dist/tools/workflow-candidate-source.js +11 -3
  154. package/package.json +2 -2
  155. package/templates/develop/README.md +81 -4
  156. package/templates/develop/develop/flow-tests.ts +1 -1
  157. package/templates/develop/develop/interruption-tests.ts +157 -0
  158. package/templates/develop/develop/local.ts +1 -0
  159. package/templates/develop/develop/option-tests.ts +138 -9
  160. package/templates/develop/develop/red-green.ts +55 -0
  161. package/templates/develop/develop/report.ts +31 -11
  162. package/templates/develop/develop/round.ts +1 -0
  163. package/templates/develop/develop/scenarios.ts +27 -0
  164. package/templates/develop/develop/turn.ts +26 -6
  165. package/templates/develop/develop.declaration.json +1 -1
  166. package/templates/develop/develop.test.ts +4 -0
  167. package/templates/develop/template.json +29 -2
package/README.md CHANGED
@@ -100,7 +100,7 @@ For a project-local tarball installation, use `./node_modules/.bin/cloving` wher
100
100
 
101
101
  Each file requires `"version": 1`. Environment settings override private settings, which override project settings, which override built-in defaults. The start guide names each configured field's source: `environment`, `private` or `project`.
102
102
 
103
- The project file supports `checks.beforeMerge`, `checks.required`, `checks.redGreen`, `tickets.command`, `tickets.nextCommand`, `queue.everySeconds`, `queue.lanes` and `queue.dailySpendLimitUsd`. The private file supports these same fields plus `harness.name`, `harness.kind`, `harness.path`, `harness.command`, `harness.turn`, `harness.output`, `profiles`, `fallbackModels`, `tokenPrices`, `privateTexts`, `notify.command`, `watch.port`, `watch.actions`, `selfUpdate.everySeconds`, `github.command` and `workflows.folder`. Commands are arrays of arguments; seconds, lanes, spend limits and ports are numbers. A watch port can also be `"off"`; watch actions are `"on"` or `"off"`.
103
+ The project file supports `checks.beforeMerge`, `checks.required`, `checks.redGreen`, `tickets.command`, `tickets.nextCommand`, `queue.everySeconds`, `queue.lanes` and `queue.dailySpendLimitUsd`. The private file supports these same fields plus `harness.name`, `harness.kind`, `harness.path`, `harness.command`, `harness.turn`, `harness.output`, `profiles`, `fallbackModels`, `tokenPrices`, `privateTexts`, `notify.command`, `watch.port`, `watch.actions`, `selfUpdate.everySeconds`, `github.command`, `workflows.folder`, `retention.days`, `retention.action` and `retention.archiveFolder`. Commands are arrays of arguments; seconds, lanes, spend limits and ports are numbers. A watch port can also be `"off"`; watch actions are `"on"` or `"off"`. The retention days are a whole number of at least 1, or `null` to keep all records; the default is 30. The retention action is `"archive"` or `"delete"`; the default is `"archive"`. The archive folder is an absolute path, or `null` for the `archive` folder in the state folder.
104
104
 
105
105
  For example, a shared `.cloving/config.json` can contain:
106
106
 
@@ -126,16 +126,18 @@ cloving config unset queue.lanes --private
126
126
 
127
127
  All four commands accept `--json` and `--repository <directory>`. Use the setting names listed above; `profiles` and `tokenPrices` are whole JSON maps, and dictionary subpaths are unknown settings. Values are parsed as JSON, falling back to text. Changes are validated before writing and go to the field's default file; `--private` writes an override in the private file. `list` and `get` show each value's source, including `default`, and all hidden file values. JSON results have `schemaVersion: 1`; list returns a `settings` array, get returns `field`, `value`, `source` and `hidden`. A default `null` means no configured value. Mutation results include `field`, `scope`, `action` and `notice`; notice is null when no service is running.
128
128
 
129
- Each change holds a file lock from read through atomic replacement, keeping the previous bytes as `<file>.bak`. A first write needs no backup. Private files, backups and temporary files have mode 0600. Locks from stopped processes are reclaimed; a live lock waits at most ten seconds, then exits with `settings-locked`. Invalid settings and unknown fields exit with status 2 without changing a file. For a running service, the result explains whether to restart to use the new value or whether an environment or private source will keep hiding it after a restart. Settings do not reload automatically.
129
+ Each change holds a file lock from read through atomic replacement, keeping the previous bytes as `<file>.bak`. A first write needs no backup. Private files, backups and temporary files have mode 0600. Locks from stopped processes are reclaimed; a live lock waits at most ten seconds, then exits with `settings-locked`. Invalid settings and unknown fields exit with status 2 without changing a file. For a running service, the result explains whether to run `cloving reload` or to restart to use the new value, or whether an environment or private source will keep hiding it after a restart. Settings do not reload automatically.
130
130
 
131
131
  Changing or unsetting `harness.name` clears kind, path and command. Changing or unsetting kind clears path and command; changing or unsetting path clears command. This keeps the previous harness's configuration from carrying into a new selection.
132
132
 
133
133
  The package includes [the project schema](src/project-config.schema.json) and [the private schema](src/private-config.schema.json), also installed under `dist/src/`. Unknown fields fail with `unknown-setting: <field>`, invalid values with `invalid-setting: <field>`, and unsupported or missing versions with `unsupported-config-version: version`. Private fields in the project file fail with `private-setting-in-project-file: <field>`. Files cannot set runtime directories, ownership leases, host bounds or the watch repository; those remain environment settings.
134
134
 
135
- Run `cloving doctor` from the repository before starting the service. It lists each requirement with a fixed code, `ok`, `fail` or `skip`, and a fixing command. Required failures and skipped prerequisites exit with status 1. `cloving doctor --json` prints the same checks as `{ "version": 1, "checks": [...] }` for setup agents.
135
+ Run `cloving doctor` from the repository before starting the service. It lists each requirement with a fixed code, `ok`, `warn`, `fail` or `skip`, and a fixing command. Required failures and skipped prerequisites exit with status 1. A warning does not change the exit status. `cloving doctor --json` prints the same checks as `{ "version": 1, "checks": [...] }` for setup agents.
136
136
 
137
137
  The required `no-workflow` check fails when neither the project nor the user folder has a workflow declaration. Its fix is `cloving init`. During `cloving init-prompt` setup, defer this check to the template step, copy or create the workflow, then run doctor again. `no-workflow` must report `ok` before setup is ready.
138
138
 
139
+ The optional `retention` check shows the size and the folder count of `journal/` and `github/` in the state folder, the age of the oldest kept record folder, and the values of `retention.days`, `retention.action` and `retention.archiveFolder`. The size includes all files in the two folders. The folder count and the age include only the record folders that the retention rule can archive or delete. The age of a folder starts at the change time of its newest file. The check gives `warn` when `retention.days` is `null` and the records are larger than 1 GB (1,000,000,000 bytes). An invalid retention setting fails the check and names the variable.
140
+
139
141
  Doctor probes harness versions and effective executable paths without running a turn. Shell scripts and simple `node script` wrappers receive `--version` from the repository folder; unsupported launcher arguments fail the probe. GitHub login and reads use the origin's host, including GitHub Enterprise. GitHub calls are reads limited to ten seconds each; a service probe waits at most five seconds. Recent Greptile activity does not establish that a usable review is available, and missing activity does not prove the app is absent.
140
142
 
141
143
  ## First run
@@ -169,7 +171,7 @@ The form sends the same run request as the CLI and retains its random command ID
169
171
  - `CLOVING_NOTIFY_COMMAND`: a JSON list naming a command to notify the operator about pending decisions, blocked runs, failed runs and stopped schedules. It has no default.
170
172
  - `CLOVING_NEXT_COMMAND`: a JSON list of a command and its arguments that prints the next ready ticket ID for `--next`. It has no default.
171
173
  - `CLOVING_CLAUDE_COMMAND`: a replacement of the `claude` command of the worker.
172
- - `CLOVING_PROFILES`: a JSON file of named turn profiles, read once when the service starts.
174
+ - `CLOVING_PROFILES`: a JSON file of named turn profiles. The service reads it at the start and at each `cloving reload`.
173
175
  - `CLOVING_COMMANDS`: a JSON object mapping command names to argument arrays, such as `{"review-requests":["/opt/cloving/scan-reviews.mjs"]}`. Names contain 1–64 ASCII letters, digits or hyphens; the first argument must be an absolute executable path. Unset means no commands. Invalid values stop startup with `invalid-commands`.
174
176
  - `CLOVING_COMMAND_SECONDS`: a whole number from 1 to 3600, default 60. Invalid values stop startup with `invalid-command-seconds`. A command exceeding the deadline fails with `command-timeout`.
175
177
  - `CLOVING_WATCH_ACTIONS`: `off` disables starting runs from the board; default `on`.
@@ -186,6 +188,63 @@ The private `notify-events.json` journal records a notification as `pending` bef
186
188
 
187
189
  Journal updates use a temporary file and rename. Sent and exhausted failed delivery details expire after seven days; their keys remain to prevent later duplicate delivery. Unfinished deliveries remain until resolved. A crash after command success but before recording that success can repeat delivery. Decisions opened and closed between checks can be missed.
188
190
 
191
+ #### Read API of the watch page
192
+
193
+ The watch page has a read API at `http://127.0.0.1:3663/api/v1`. Its OpenAPI 3.1 document is at `http://127.0.0.1:3663/api/v1/openapi.json`. OpenAPI client generators can read the document and make a typed client. The API accepts only `GET` and needs no authentication. An error reply is `application/problem+json` with a `code` member, for example `not-found` or `invalid-query`.
194
+
195
+ - `GET /api/v1/` gives the version of the API and its endpoints.
196
+ - `GET /api/v1/openapi.json` gives the OpenAPI document.
197
+ - `GET /api/v1/service` tells if the Cloving service answers.
198
+ - `GET /api/v1/workflows` gives the workflows with their counts and card fields.
199
+ - `GET /api/v1/runs` gives one page of runs, newest first. The query parameters `workflowId`, `state`, `limit` (1 to 200, default 50) and `cursor` select the runs. The `nextCursor` of a page gives the next page.
200
+ - `GET /api/v1/runs/{runId}` gives one run with its phases, open decisions, reviews and agent turns.
201
+ - `GET /api/v1/runs/{runId}/events` gives the events of one run, the lowest sequence first. The query parameter `after` (default 0) gives only the events with a higher sequence.
202
+ - `GET /api/v1/stream` sends Server-Sent Events. Each message has the event `change` and the data `{"updatedAt":"<time>"}`. Read the runs again when a message arrives.
203
+
204
+ Replace `<run-id>` with the ID of a run of `develop`, for example from the run list:
205
+
206
+ ```sh
207
+ curl http://127.0.0.1:3663/api/v1/
208
+ curl http://127.0.0.1:3663/api/v1/openapi.json
209
+ curl http://127.0.0.1:3663/api/v1/service
210
+ curl http://127.0.0.1:3663/api/v1/workflows
211
+ curl 'http://127.0.0.1:3663/api/v1/runs?workflowId=develop&state=waiting_gate&limit=20'
212
+ curl 'http://127.0.0.1:3663/api/v1/runs/<run-id>'
213
+ curl 'http://127.0.0.1:3663/api/v1/runs/<run-id>/events?after=40'
214
+ curl -N http://127.0.0.1:3663/api/v1/stream
215
+ ```
216
+
217
+ The run list has the active runs and the runs of the last 7 days. The run detail reads each run, also a run older than 7 days. The events endpoint gives no event payloads, and the API has no write endpoint.
218
+
219
+ ### Use Cloving from an MCP client
220
+
221
+ `cloving mcp --repository <directory>` serves Cloving tools to an MCP client over standard input and output. The server connects to the service of that repository. Start the service with `cloving start` before the client calls a tool.
222
+
223
+ Register the server in Claude Code:
224
+
225
+ ```sh
226
+ claude mcp add cloving -- cloving mcp --repository <directory>
227
+ ```
228
+
229
+ Register the server in Codex with this table in `~/.codex/config.toml`:
230
+
231
+ ```toml
232
+ [mcp_servers.cloving]
233
+ command = "cloving"
234
+ args = ["mcp", "--repository", "<directory>"]
235
+ ```
236
+
237
+ The server has ten tools:
238
+
239
+ - `cloving_status`, `cloving_questions` and `cloving_events` read the runs, the pending decisions and the events, as `cloving status`, `cloving questions` and `cloving events` do.
240
+ - `cloving_run` submits a run, as `cloving run` does. Give exactly one source: `prompt`, `ticket` (a ticket ID or `next`) or `input` (a JSON object, not a file path).
241
+ - `cloving_answer`, `cloving_approve` and `cloving_deny` record a decision. Read `reference`, `expectedRevision`, `generation` and `subjectDigest` from `cloving_questions`.
242
+ - `cloving_pause`, `cloving_resume` and `cloving_drop` send a run command with the `expectedRevision` of the run.
243
+
244
+ Each of the seven tools that change state requires a `commandId`. The server never makes a command ID. After a lost reply (`outcome-unknown`), send the same request with the same `commandId` again. The service then gives the stored receipt, also when the first request came from the CLI. A tool gives the same JSON object as the CLI command with `--json`. A refusal of the service gives `isError: true`, and an argument that is not valid gives the JSON-RPC error `-32602`. A cancel of a tool call stops the wait for the reply, but it does not cancel a command that the service admitted.
245
+
246
+ A tool call cannot start the service and cannot select a different repository. The server binds to one repository when it starts.
247
+
189
248
  ### Answer decisions from a phone
190
249
 
191
250
  Cloving can send its pending decisions to the Cloving app on your phone, and the phone can answer them. `cloving start` runs the phone relay as a child process. `CLOVING_PHONE_RELAY` with the value `off` starts no relay. `CLOVING_RELAY_ORIGIN` selects the relay server; the default is `https://cloving.ai`.
@@ -273,6 +332,10 @@ Operator values combine GitHub merges and repository issue-label events with dae
273
332
 
274
333
  `cloving start` compiles and registers only project and user workflows. The start guide lists each registered workflow with a run command that matches its input contract. With no workflow, the service still starts and prints "No workflow. Run: cloving init". Status continues to show stored runs after their workflows have been removed.
275
334
 
335
+ To add, change or remove a workflow file while the service runs, run `cloving reload`. The service checks and compiles the workflow files again in a child process. New runs use the new bundle. A run that started before the reload stays on the bundle of its pin, and its agent turn continues. When a workflow file gets a new refusal, for example a compile error, the reload keeps the current workflows and shows the reason. A change of the queue workflow or of a schedule needs a restart. After you remove all workflow files, a reload removes all workflows. `cloving status --json` shows the current `bundleDigest`.
336
+
337
+ The reload also reads the settings files and the profiles file again. It applies changes of `profiles`, `fallbackModels`, `tokenPrices` and `workflows.folder`. New turns use the new values, and a turn in work keeps the values of its start. A changed `workflows.folder` applies to the workflow part of the same reload. The reply lists these fields in `reloadedFields`. It lists each other changed field in `restartFields`, and the service keeps the old value of that field until a restart. The environment variables of the start keep their precedence over the files. An invalid settings file, an invalid profiles file or a profile that names a harness with no worker at the start of the service refuses the reload with its code, for example `invalid-config-json`, `invalid-profiles` or `harness-no-worker`. Then nothing changes.
338
+
276
339
  Use `cloving init` to choose and configure a template. The `develop` template supports ticket input, queue selection, review and merge choices. The `idea-to-issue` template researches an idea and hands the resulting issue to a declared development workflow. The commands below assume those templates have been copied and configured:
277
340
 
278
341
  ```sh
@@ -291,6 +354,8 @@ The rule: read the diff before you approve.
291
354
 
292
355
  Claude turns have web research tools by default, an empty GitHub CLI configuration folder, and no GitHub token environment variables. This is not a sandbox: a turn can still find other credentials. The PRD refusal rules catch the listed private text and common sensitive forms; unlisted private names can pass. A `post-uncertain` result requires checking GitHub before trying another run.
293
356
 
357
+ `cloving status --active` lists only the runs that are not `completed`, `failed` or `cancelled`. It uses the same order and format as `cloving status`. The service selects the active runs before it builds a run view. Thus it makes no view query for a finished run. The number of queries does not change with the number of finished runs, but the service still reads the stored state of each run. `--active` works with `--json`, `--limit` and `--after`. With `--json`, one request returns one page of active runs and its `next` cursor.
358
+
294
359
  `cloving status RUN_ID --json` includes `run.input`, and `cloving status --json` includes `input` on each listed run. It is the normalized submission admitted with the run, such as `{"ticket":"297"}` for a ticket workflow or the submitted fields of another declared workflow. It survives service restarts and excludes resolver-added ticket text and repository paths. Older runs without a stored submission show `null`. The text status of one run shows `Input:` as compact JSON, limited to the first 200 characters with `…` appended when longer. Terminal control and hidden format characters are escaped.
295
360
 
296
361
  ### The ticket command
@@ -332,7 +397,7 @@ Before enabling more than one lane, update the next-ticket command to honor `CLO
332
397
 
333
398
  In the ticket text returned by `CLOVING_TICKET_COMMAND`, each `#N` in a sentence starting with **Needs** inside `## Dependencies` is a hard dependency. For example, `Needs #123 and #124.` waits until both GitHub issues close. Other references and sections do not create dependencies. Cloving reads dependency state with `CLOVING_GH_COMMAND` (default `gh`); an unavailable or invalid response prevents admission. An open dependency returns `ticket-dependency-open` and puts the ticket in the skip list. Waiting tickets are checked again each cycle with a free lane, so a closed dependency makes its ticket eligible again.
334
399
 
335
- `cloving status` and watch display busy lanes and skipped tickets with their reasons. A pending self-update holds new admissions while active lanes drain. Three unsuccessful runs in a row, counted in the order they finish, stop the queue; a successful handoff clears the streak. Ordinary restarts preserve it. After inspecting and reconciling the failures, start with `--reset-queue` to clear the streak. A conflict caused by an earlier lane's merge returns its diagnostics to a fix round. Lanes do not add CI runners or parallelize turns within one run.
400
+ `cloving status` and watch display the queue state, busy lanes and skipped tickets with their reasons. A pending self-update holds new admissions while active lanes drain. Three unsuccessful runs in a row, counted in the order they finish, stop the queue; a successful handoff clears the streak. Ordinary restarts preserve it. After inspecting and reconciling the failures, run `cloving queue resume`, or start with `--reset-queue`, to clear the streak. A conflict caused by an earlier lane's merge returns its diagnostics to a fix round. The merge steps of one repository take turns: a step waits until the earlier step ends, and then it merges `main`, runs the merge check, pushes and waits for CI. Lanes do not add CI runners or parallelize turns within one run.
336
401
 
337
402
  An empty selection gives `no-ready-ticket`; a failed command gives `next-ticket-not-available`. Missing configuration gives `next-source-not-configured`. Multiple IDs, an invalid ID, or `next` as the selected ID give `invalid-ticket-id`. Use only one of `--next`, `--ticket`, `--prompt` and `--input`.
338
403
 
@@ -346,7 +411,7 @@ Worker turns retry an exact Codex capacity refusal or a Claude overload after 60
346
411
 
347
412
  Each retry keeps the work tree, profile, budget and limits, and starts a new session with a continuation line before the full prompt. After any network failure, every later attempt gets the network continuation, which asks it to check earlier commits, pushes, review comments and messages, avoid repeating completed writes, and stop to report a write whose result is unknown. It also gets fresh git evidence for commits since this turn's first attempt. Cloving fetches the branch from `origin` and marks each commit `pushed`, `not pushed` or `unknown`; an absent origin branch means `not pushed`. Each git command has a 30-second limit and is cancelled by drop. Failed reads give explicit unknown evidence.
348
413
 
349
- Before a retry, Cloving checks the work tree's git index lock, including linked work trees. Capacity and network retries never remove a lock: `worktree-index-locked` records its path and tells the operator to remove it when no git process uses the work tree. Before a Claude resume after a daemon restart or a dead turn process, Cloving stops same-user processes whose working directories are in the turn work tree, waits up to 15 seconds after SIGTERM, then sends SIGKILL. The daemon itself and processes outside that tree are excluded. Cloving removes a work tree index lock only when it identified the lock owners before cleanup, those owners have exited, and the lock has not changed. A lock with an unknown owner, including one whose owner closed its descriptor, stays in place and reports `worktree-index-locked`; inspect its owner before removing it manually. Stopped process IDs are recorded in `resume-processes.json` in the turn folder. An unreadable index path gives `worktree-index-unknown`. Neither failure starts another attempt. The step keeps one operation identity and sums usage across attempts. Private turn journals, status text and JSON, and watch retain each attempt's model, token counts, cost estimate and `retryKind` (`capacity`, `network`, or `null` for other results). Older attempts without the field show no kind. Attempt kinds appear after the turn ends. Resumed attempts also show `resumedAfter: daemon-restart` in status text, JSON and watch.
414
+ Before a retry, Cloving checks the work tree's git index lock, including linked work trees. Capacity and network retries never remove a lock: `worktree-index-locked` records its path and tells the operator to remove it when no git process uses the work tree. Before a Claude resume after a daemon restart or a dead turn process, Cloving stops same-user processes whose working directories are in the turn work tree, waits up to 15 seconds after SIGTERM, then sends SIGKILL. Cloving does the same cleanup before it reports `claude-turn-interrupted` for a turn that it cannot resume, so a later turn does not share the work tree with an earlier process. A cleanup failure gives its own reason instead of `claude-turn-interrupted`. The daemon itself and processes outside that tree are excluded. Cloving removes a work tree index lock only when it identified the lock owners before cleanup, those owners have exited, and the lock has not changed. A lock with an unknown owner, including one whose owner closed its descriptor, stays in place and reports `worktree-index-locked`; inspect its owner before removing it manually. Stopped process IDs are recorded in `resume-processes.json` in the turn folder. An unreadable index path gives `worktree-index-unknown`. Neither failure starts another attempt. The step keeps one operation identity and sums usage across attempts. Private turn journals, status text and JSON, and watch retain each attempt's model, token counts, cost estimate and `retryKind` (`capacity`, `network`, or `null` for other results). Older attempts without the field show no kind. Attempt kinds appear after the turn ends. Resumed attempts also show `resumedAfter: daemon-restart` in status text, JSON and watch.
350
415
 
351
416
  `CLOVING_FALLBACK_MODELS` accepts a JSON array of at most three nonempty model names, for example `'["gpt-6-astra"]'`. Without the setting or with `[]`, every attempt keeps the main model. The first attempt and first retry use the main model; each later attempt uses the next configured fallback within the existing attempt limit. With three attempts, only the first fallback can be used. The worker command must explicitly name a model with `-m NAME`, `--model NAME` or `--model=NAME`; otherwise it stays unchanged and the private journal records `fallback-model-unavailable`. Fallbacks apply only when the preceding attempt ended with a capacity refusal. A network retry keeps the model of the configured command. A fallback model can have a higher price. Each attempt's estimate uses its own model's price; an unknown price or missing token count leaves the total estimate unknown. Malformed settings refuse daemon start with `invalid-fallback-models`.
352
417
 
@@ -360,6 +425,8 @@ The worker is not confined. It runs the `claude` command of the search path as t
360
425
 
361
426
  Single-run status (`cloving status RUN_ID`, also with `--json`) and watch show each worker command call and its run totals. Token counts retain the reported input, cache read and output categories separately. Codex input includes cached tokens; cache read tokens are not added to input tokens. These selected counts are not complete billing usage or a record of all subagent work. Missing or invalid fields show `Token usage not known`. Costs show the worker-reported USD price when available, otherwise `≈ 7.82 USD (estimate)` when an estimate is available, otherwise `Price not known`; a reported zero shows `0.00 USD`. Totals for an unfinished run are labeled `so far`. JSON uses `null` for unknown numeric values, retains the total reported price field `claudeCostUsd`, and adds `costDisplay` to the run and each worker turn.
362
427
 
428
+ The totals of a run stay in the service state after the retention rule moves its operation records. When a turn folder of the run is in the `journal` folder of the archive, single-run status shows `Turn details are archived in the journal folder of the archive: <folder>`, and JSON adds `run.turnDetails` as `{ "state": "archived", "folder": "<archive>/journal" }`. With the `delete` action, status shows `Turn details were deleted by the retention rule`, and JSON adds `{ "state": "deleted" }`. The daemon shows a deletion only for a finished run that ended before the retention period and has no turn folder in the state folder. Runs with kept records have no `turnDetails` field.
429
+
363
430
  The built-in table uses standard short-context prices read on **2026-10-18** from the [OpenAI pricing page](https://openai.com/api/pricing/), in USD per million tokens:
364
431
 
365
432
  | Model | Input | Cached input | Output |
@@ -452,7 +519,9 @@ The last worker report must end with exactly one line: `Ticket status: complete`
452
519
 
453
520
  After a merge, a protected GitHub operation updates the issue body and labels. Complete work closes the issue with reason `completed` and adds "Completed in pull request #M." to `## Cloving status`. Partial work keeps the issue open and appends `- Pull request #M: partial. Remaining: ...` to that section at the end of the body, preserving earlier partial lines and other body text. Items are separated by semicolons. The issue privacy check runs before new remaining text is written; a refusal uses "Remaining work is in the private run report." instead. No issue comment is posted. Status writes add and remove only workflow labels, preserving unrelated label edits. Before a body write, Cloving rereads the issue and refuses a changed body with `github-issue-conflict`; the merged outcome records `issueUpdate: failed`. GitHub does not support conditional PATCH requests for this endpoint, so this check cannot make the read and write atomic.
454
521
 
455
- Partial work removes `cloving: in progress` and adds `cloving: ready`, so the next queue cycle can select the same issue and read its remaining work from the issue body. The third partial run adds `cloving: blocked` instead of `cloving: ready`. Completion removes these workflow labels. Status operations reconcile the pull request number in the stored history after a restart to avoid duplicate writes. The unmodified develop template declares an effect limit of 27 units.
522
+ Partial work removes `cloving: in progress` and adds `cloving: ready`, so the next queue cycle can select the same issue and read its remaining work from the issue body. The third partial run adds `cloving: blocked` instead of `cloving: ready`. Completion removes these workflow labels. Status operations reconcile the pull request number in the stored history after a restart to avoid duplicate writes. The unmodified develop template declares an effect limit of 31 units.
523
+
524
+ When a worker turn ends with `claude-turn-interrupted`, the develop template runs the turn again once in the same work tree. This occurs when the daemon cannot resume an open turn after a restart, for example when the turn has no recorded session. The prompt of the new turn tells the worker that the earlier turn stopped before its report, and that the work tree holds its work. A second interruption of the same turn ends the run as blocked with the reason `turn-interrupted` and adds `cloving: blocked`. Other turn failures fail the run.
456
525
 
457
526
  Partial history comes only from a hidden HTML comment at the end of the issue body, beginning with `cloving-status`. Its JSON object has `version: 1`, a `partialRuns` array of pull request numbers and remaining-item lists, and an optional `completedPullRequest`. Double hyphens and greater-than signs in JSON strings are escaped so they cannot end the comment. Each write regenerates the readable section from this data and replaces the exact previously generated section. Other Markdown stays unchanged, including status examples, lists, tables, HTML and unclosed fences. Missing, broken or unknown metadata counts as zero earlier partial runs and is replaced on the next write; readable text is never used to reconstruct history. A block followed by other issue text is left unchanged and counts as missing terminal metadata.
458
527
 
@@ -462,6 +531,20 @@ Merged runs retain `handoff_requested` with `ticketStatus: complete` or `partial
462
531
 
463
532
  To resume a queue stopped at this limit, stop the service with Control-C, inspect the failures and reconcile the affected issues, then run `cloving start --reset-queue` with the same repository, runtime and queue settings. This clears the failure streak once and preserves tracked commands. Ordinary restarts, including automatic self-updates, preserve the count. Resetting the queue does not change issue labels or retry blocked issues.
464
533
 
534
+ ### Queue control
535
+
536
+ The operator controls the queue of a running service with three commands. Each command uses the service protocol, and `--json` prints one JSON object.
537
+
538
+ - `cloving queue [--json]` shows the state of the queue: `running`, `stopped`, `draining` or `drained`. It also shows the busy lanes, the failed runs in a row and, for a stopped queue, the reason. `cloving status` and the watch page show the same state.
539
+ - `cloving queue resume [--command-id <id>] [--json]` sets the failure count to 0 and ends a stop or a drain. The next queue check can start a ready ticket. No restart is necessary.
540
+ - `cloving queue drain [--command-id <id>] [--json]` makes the queue start no new run. The runs in work continue to their end. When no lane is busy, the queue journal records the drain time, the service log shows `Queue drained` one time, and the state changes to `drained`. Until the queue records that time, the state stays `draining`.
541
+
542
+ A drained queue is a safe point for a restart. The drain stays in place after a restart. `cloving queue resume` or `cloving start --reset-queue` ends it. A drain does not stop manual runs. A manual run that starts after the drain time shows `draining` until the run ends.
543
+
544
+ With no `--command-id`, the command makes an ID. The text output prints it on standard error, and the JSON result includes it as `commandId`. The queue journal keeps the newest 32 command IDs across restarts. A repeated command with a kept ID does not apply the command again, and the reply shows the current queue state with `replayed: true`. A different command with a kept ID gets `conflicting-command`. The queue applies a command with an older ID as a new command.
545
+
546
+ A queue that stopped on an error shows the error as the reason and refuses commands until a restart. An invalid queue journal and a queue command that the journal cannot save are two examples of these errors.
547
+
465
548
  At the confidence score 5/5, the worker runs the local merge check and waits for CI of the exact head it will merge, including an updated head pushed after merging the default branch. When CI passes, it merges the pull request with a merge commit, with no human approval. Then it removes the remote branch, the work tree and the local branch. With a lower score, the Claude worker gets the summary and the findings of the review for one more round. After three such rounds, or after 60 minutes with no review, the run stops and the pull request stays open.
466
549
 
467
550
  Set `CLOVING_REQUIRED_CHECKS` to a JSON list of check run names, such as `["quality", "quality_linux"]`, to require those checks. Without the setting, all GitHub Actions checks on the head commit are required. The newest run of each name decides; `success`, `neutral` and `skipped` pass. With no setting and no Actions checks after a 60-second grace period, a repository without CI can merge. A configured missing check keeps waiting. CI is polled every 30 seconds with a separate 90-minute limit. A timeout returns `ci-pending` and blocks without a fix round.
@@ -805,7 +888,7 @@ const attached = await protectedEffect({
805
888
  });
806
889
  ```
807
890
 
808
- The method uploads the file as an asset of the release `release`, with the asset name `pr-<number>-<name>`. Then it creates or updates one comment on the pull request with a link to the asset, so a second call adds no second comment. When the release does not exist, the method creates it as a pre-release that is not the latest release. `name` is a file name with the extension `.mp4`, `.webm` or `.png`. The file must be a regular file of at most 100 MB, and a symbolic link is refused. `release` has lowercase letters, digits and hyphens, and it cannot start with `v` and a digit, so it never matches a version tag. The comment text must pass the same privacy check as issue texts. The failure reasons are `invalid-payload`, `attachment-not-available`, `attachment-too-large` and the usual GitHub reasons. The candidate declaration must include `methods: { "github": ["attach-asset"] }`.
891
+ The method uploads the file as an asset of the release `release`, with the asset name `pr-<number>-<name>`. Then it creates or updates one comment on the pull request with a link to the asset, so a second call adds no second comment. GitHub does not play a release video in the page. An optional `place` is `comment` (the default) or `description`. With `description`, the method writes the text and the file into the pull request body between the markers `<!-- cloving-attachment:<number>:<name> -->` and `<!-- /cloving-attachment:<number>:<name> -->`, with `gh pr edit --body-file <file> --attach <file>`. `gh` uploads the file to GitHub, and GitHub shows a video as a player. This place makes no release asset and no comment, a second call replaces the block, and it needs a `gh` version with `gh pr edit --attach`. When the release does not exist, the method creates it as a pre-release that is not the latest release. `name` is a file name with the extension `.mp4`, `.webm` or `.png`. The file must be a regular file of at most 100 MB, and a symbolic link is refused. `release` has lowercase letters, digits and hyphens, and it cannot start with `v` and a digit, so it never matches a version tag. The comment text must pass the same privacy check as issue texts. The failure reasons are `invalid-payload`, `attachment-not-available`, `attachment-too-large` and the usual GitHub reasons. The candidate declaration must include `methods: { "github": ["attach-asset"] }`.
809
892
 
810
893
  The template tests use `fakeStepBridge` from `cloving/workflows/testing` to record effect and gate requests and supply fake results. `templates/develop/develop.test.ts` exercises the develop template through that public testing API.
811
894
 
@@ -1048,7 +1131,7 @@ Three consecutive failed scheduled runs stop a schedule. A completed run with ou
1048
1131
 
1049
1132
  Schedule settings load at startup. A scheduler transport or journal failure stops scheduling and writes a diagnostic; reconcile the state and restart to retry a pending slot. Full product admission remains subject to the external verification policy.
1050
1133
 
1051
- Startup retains each complete compiled project under `workflow-bundles` in private daemon state, beside the generated `project` directory. Each content-addressed snapshot includes workflow sources, declarations, registration manifests, all compiler artifacts, and the installed dependency graph and Node identity. Snapshots bind to the canonical repository and survive later source edits or removal. Storage lookup validates every captured file and returns `pinned-workflow-unavailable` for missing or changed bytes; startup refuses reuse of a matching damaged snapshot without repairing it. No retained generation is deleted automatically. Before Local World starts, the daemon loads the retained generations needed by unfinished runs. Their starts, replays and continuations use the recorded bundle, including after workflow removal; new submissions use current registrations. Duplicate answers and replay preserve effect receipts and spending.
1134
+ Startup retains each complete compiled project under `workflow-bundles` in private daemon state, beside the generated `project` directory. Each content-addressed snapshot includes workflow sources, declarations, registration manifests, all compiler artifacts, and the installed dependency graph and Node identity. Snapshots bind to the canonical repository and survive later source edits or removal. Storage lookup validates every captured file and returns `pinned-workflow-unavailable` for missing or changed bytes; startup refuses reuse of a matching damaged snapshot without repairing it. No retained generation is deleted automatically. Before Local World starts, the daemon loads the retained generations needed by unfinished runs. Their starts, replays and continuations use the recorded bundle, including after workflow removal; new submissions use current registrations. A workflow message of a dropped run gets a done answer. No workflow code runs, the daemon records no observation, and the Local World queue stops its retries. Duplicate answers and replay preserve effect receipts and spending.
1052
1135
 
1053
1136
  ### Project and user workflows
1054
1137
 
@@ -1116,13 +1199,15 @@ After a merge or branch update changes `package.json` or `package-lock.json`, re
1116
1199
 
1117
1200
  `npm run check` and `npm run test:concurrency` currently reject admission because the complete quality policy is not implemented. Setup checks are not product acceptance. State and retained releases will live outside the package installation; installing or updating the package does not activate a daemon or migrate state.
1118
1201
 
1119
- Run `npm run test:scale` on an otherwise idle machine to measure authority opening at 1,000, 10,000 and 50,000 events. This command runs the benchmark alone, without coverage; the regular test suite excludes it. The fixture uses completed runs with budgets, gates and released continuations. It requires each opening to take less than two seconds and the largest opening to take less than eight times the 10,000-event opening. Replay uses the pure reducer with run-scoped projections and preserves shared effect and lease state. Large effect histories, growing gate or pending-intent state within one run, and cancelled-run recovery still require separate scaling work; this benchmark does not establish their bounds. The generated-history parity and tamper tests compare replay with the full reducer and cover every event type.
1202
+ Run `npm run test:scale` on an otherwise idle machine to measure authority opening at 1,000, 10,000 and 50,000 events. This command runs the benchmark alone, without coverage; the regular test suite excludes it. The fixture uses completed runs with budgets, gates and released continuations. It requires each opening to take less than two seconds and the largest opening to take less than eight times the 10,000-event opening. Replay uses the pure reducer with run-scoped projections and preserves shared effect and lease state. Large effect histories, growing gate or pending-intent state within one run, and cancelled-run recovery still require separate scaling work; this benchmark does not establish their bounds. The generated-history parity and tamper tests compare replay with the full reducer and cover every event type. The same command also measures the first status page of a service with 700 runs and 7,000 events, and requires an answer in less than 200 ms. The service keeps the projected authority state in memory, so a status request reads no projected table in full after the start.
1120
1203
 
1121
- `npm run check:quality` checks the npm manifest, lock and installed versions, regenerates TypeScript output in memory, inventories first-party files, and runs strict compilation, the proposed module catalog, workflow/effect boundaries, suppression and test-integrity checks, formatting, typed lint, structural checks, clone detection and the regular local test suite. Production modules require at least 90% line, statement and function coverage and 85% branch coverage per file. Coverage uses native V8 ranges mapped to the syntax tree of the exact Node-stripped source; unimported production files fail. Class and named object types allow at most 24 callable members; interfaces allow 10. Clone checks support ordinary TypeScript and JavaScript; JSX is rejected until separately qualified. `npm run check:lint`, `npm run check:format` and `npm run test:quality` run individual development checks.
1204
+ `npm run check:quality` checks the npm manifest, lock and installed versions, builds the package with `npm run build`, regenerates TypeScript output in memory, inventories first-party files, and runs strict compilation, the proposed module catalog, workflow/effect boundaries, suppression and test-integrity checks, formatting, typed lint, structural checks, clone detection and the regular local test suite. Production modules require at least 90% line, statement and function coverage and 85% branch coverage per file. Coverage uses native V8 ranges mapped to the syntax tree of the exact Node-stripped source; unimported production files fail. Class and named object types allow at most 24 callable members; interfaces allow 10. Clone checks support ordinary TypeScript and JavaScript; JSX is rejected until separately qualified. `npm run check:lint`, `npm run check:format` and `npm run test:quality` run individual development checks.
1122
1205
 
1123
1206
  The gate uses the committed `quality/test-durations.json` record to start files with unknown durations first in path order, then recorded files from longest to shortest, breaking ties by path. Stale entries have no effect. The launcher passes the ordered list directly to Node’s programmatic test runner because CLI discovery sorts positional paths. The test inventory and sorted report stay the same. Run `npm run record:test-durations` to refresh the record after adding or changing tests, then commit the JSON. This runs the regular suite with the gate's worker count, timeout settings, isolated environment and V8 coverage, sums each file's top-level test and suite durations in seconds, and writes entries in path order after a successful complete run. Nested tests are not counted twice. Temporary coverage stays outside the repository and is removed afterward. The command accepts the same concurrency and time-factor settings as the gate.
1124
1207
 
1125
- CI runs two duration-balanced shards for macOS and each Linux Node version. Only shard 1 runs the static checks. Each shard builds the package before testing so runtime declarations are available independently of other test files, then exports raw production coverage. A final job checks their joined coverage. The required checks remain `quality` and `quality_linux`. Without a shard setting, the local gate runs all checks and coverage as before.
1208
+ CI runs three duration-balanced shards for macOS and each Linux Node version on each pull request and each push to `main`. Only shard 1 runs the static checks. Each shard builds the package before testing, the same as the local gate, so runtime declarations are available independently of other test files. On a pull request, each shard exports raw production coverage, and a final job checks their joined coverage. A push to `main` runs the same shards with `CLOVING_TEST_COVERAGE=off`, so its test processes collect no coverage, and it skips the coverage job. The required checks remain `quality` and `quality_linux`. Without a shard setting, the local gate runs all checks and coverage as before and ignores `CLOVING_TEST_COVERAGE`.
1209
+
1210
+ A version tag runs no tests, because the `main` CI already tested its commit. The `release_check` job requires that the tagged commit is on `main`, and it waits up to 80 minutes for the `main` CI run of the same commit to pass. Then the publish job builds the package, publishes it and creates the GitHub release.
1126
1211
 
1127
1212
  The quality report includes elapsed time and the effective limit in milliseconds for each bounded command: npm inventory, clone detection, tests and coverage conversion. Commands using more than 80 percent of their limit produce a warning in the JSON report and on stderr after the report. Failed checks print the collected timings and warnings with the original error and a failing exit status. Warnings preserve a passing result. If your change made the check slower, address the warning in the same ticket.
1128
1213
 
@@ -1138,13 +1223,13 @@ done
1138
1223
 
1139
1224
  The expansion helper waits for two animation frames before horizontal input. Resolving in the first callback can send the gesture before the widened Done column renders, leaving the board at its old horizontal scroll limit even after scrolling settles. The regression delays that rendering pass with `testMilliseconds(250)` and keeps the full card visibility assertion. A failure reports the card, column and board rectangles, both scroll positions and the viewport; compare the card's right edge with the board's right edge to distinguish horizontal clipping from an incomplete vertical scroll.
1140
1225
 
1141
- Before a ticket report, commit your work and run `npm run check:clean`. It refuses uncommitted changes, archives the committed tree of `HEAD` into a temporary directory, copies the worktree's `node_modules`, and runs `node tools/check-quality.ts` there with the same environment and exit code. Ignored build output such as `dist/` is absent, so tests that rely on stale local files fail. The temporary copy is removed after success, failure or SIGINT/SIGTERM interruption.
1226
+ Before a ticket report, commit your work and run `npm run check:clean`. It refuses uncommitted changes, archives the committed tree of `HEAD` into a temporary directory, copies the worktree's `node_modules`, and runs `node tools/check-quality.ts` there with the same environment and exit code. Ignored files are absent, so tests that rely on stale local files fail. The gate builds `dist/` again from the committed sources. The temporary copy is removed after success, failure or SIGINT/SIGTERM interruption.
1142
1227
 
1143
- After merging dependency changes, run `npm ci --ignore-scripts` in the worktree before checking it. Copied dependencies can belong to an earlier lockfile; `check:clean` copies them unchanged. An `installed-lock-mismatch` requires refreshing that installation and rerunning the checks. If existing `dist/` output produces `generated-output-mismatch`, regenerate it with `npm run build` before rerunning `check:quality`.
1228
+ After merging dependency changes, run `npm ci --ignore-scripts` in the worktree before checking it. Copied dependencies can belong to an earlier lockfile; `check:clean` copies them unchanged. An `installed-lock-mismatch` requires refreshing that installation and rerunning the checks.
1144
1229
 
1145
1230
  Set `CLOVING_CHECK_TIME_FACTOR` to a whole number from 1 to 9 (default 1) to multiply the developer gate test and coverage command time limits, the inner hook check limit (15 seconds by default), and the installed hook limit (30 seconds by default). With factor 3, the hook limits are 45 and 90 seconds, respectively.
1146
1231
 
1147
- Set `CLOVING_TEST_CONCURRENCY` to a whole number from 1 to 8 to override the developer gate's test worker count. Without the setting, the gate uses one fewer worker than the available cores, with a minimum of 2 and a maximum of 4. macOS CI sets it to 3 and Ubuntu CI sets it to 4. CI also sets `NODE_OPTIONS` to `--max-old-space-size=4096`, because the gate process needs about 2 GB of heap, and the hosted macOS runner gives only about 2 GB by default. Pull requests run on the same hosted runners. Pushes start CI only for `main` and `v*` tags. A new push to a pull request cancels the older CI runs of that pull request, and a new manual run cancels the older manual runs of its branch; runs of `main` and of tags are never cancelled. Invalid values fail with an error.
1232
+ Set `CLOVING_TEST_CONCURRENCY` to a whole number from 1 to 16 to override the developer gate's test worker count. Without the setting, the gate uses one fewer worker than the available cores, with a minimum of 2 and a maximum of 4. macOS CI sets it to 3 and Ubuntu CI sets it to 4. CI also sets `NODE_OPTIONS` to `--max-old-space-size=4096`, because the gate process needs about 2 GB of heap, and the hosted macOS runner gives only about 2 GB by default. Fork pull requests and other events use GitHub-hosted runners. Pushes start CI only for `main` and `v*` tags. A new push to a pull request cancels the older CI runs of that pull request, and a new manual run cancels the older manual runs of its branch; runs of `main` and of tags are never cancelled. Invalid values fail with an error.
1148
1233
 
1149
1234
  Tests that stop a child process should await `stopChild` from `test/stop-child.ts`. Use `stopChildGroup` for a detached process group, including descendants whose leader has exited. Both helpers send `SIGTERM`, wait up to five seconds times `CLOVING_CHECK_TIME_FACTOR` for exit, then escalate to `SIGKILL`. When a test requires a hard crash or an unresponsive child, start that child with `NODE_V8_COVERAGE` omitted so a kill cannot interrupt its coverage recording. Keep coverage enabled for ordinary child runs and recovery runs.
1150
1235
 
@@ -0,0 +1,9 @@
1
+ import type { Logger } from "solid-objects";
2
+ export interface AuthorityHealth {
3
+ readonly refresh: () => Promise<void>;
4
+ readonly condition: () => string | null;
5
+ }
6
+ export declare function authorityHealth(source: {
7
+ readonly condition: () => Promise<string | undefined>;
8
+ readonly logger: Logger;
9
+ }): AuthorityHealth;
@@ -0,0 +1,24 @@
1
+ export function authorityHealth(source) {
2
+ const state = { condition: null, checks: 0, checked: 0 };
3
+ return {
4
+ refresh: async () => {
5
+ state.checks += 1;
6
+ const check = state.checks;
7
+ const detected = (await source.condition()) ?? null;
8
+ if (check < state.checked)
9
+ return;
10
+ state.checked = check;
11
+ changed(state, { detected, logger: source.logger });
12
+ },
13
+ condition: () => state.condition,
14
+ };
15
+ }
16
+ function changed(state, change) {
17
+ const { detected, logger } = change;
18
+ const previous = state.condition;
19
+ state.condition = detected;
20
+ if (previous === null && detected !== null)
21
+ logger.warn({ event: "authority-unhealthy", condition: detected });
22
+ if (previous !== null && detected === null)
23
+ logger.info({ event: "authority-recovered", condition: previous });
24
+ }
@@ -1,8 +1,9 @@
1
- import { type Database, type InstrumentationEvent, type JsonObject, type SolidObjectsRuntime } from "solid-objects";
1
+ import { type Database, type InstrumentationEvent, type JsonObject, type Logger, type SolidObjectsRuntime } from "solid-objects";
2
2
  import { type SequenceListener } from "./authority-changes.ts";
3
3
  import { type SchemaState } from "../../storage/domain-schema.ts";
4
4
  import { type AvailableManifest, type Delivery } from "./authority-delivery.ts";
5
5
  import { type AuthorityTiming, type EffectSettings } from "./authority-settings.ts";
6
+ import { type ProjectionMemory } from "./projection-memory.ts";
6
7
  export interface AuthorityOptions {
7
8
  readonly delivery?: Delivery;
8
9
  readonly timing?: AuthorityTiming;
@@ -44,11 +45,12 @@ export interface SessionLimits {
44
45
  export interface AuthorityRuntime {
45
46
  readonly runtime: SolidObjectsRuntime;
46
47
  readonly reader: Database;
48
+ readonly memory: ProjectionMemory;
47
49
  readonly invoker: (actorId: string, idempotencyKey?: string) => AuthorityInvoker;
48
50
  readonly stop: () => Promise<void>;
49
51
  readonly handlersSettled: () => Promise<boolean>;
50
52
  readonly setEpoch: (epoch: number) => void;
51
- readonly unhealthy: () => string | undefined;
53
+ readonly logger: Logger;
52
54
  readonly watchMilliseconds: number;
53
55
  readonly limits: SessionLimits;
54
56
  readonly releaseEarlierOwners: () => Promise<void>;
@@ -11,9 +11,10 @@ import { sequenceSubscriber, } from "./authority-changes.js";
11
11
  import { contentionMilliseconds, lockContention, lockExclusively, observeSettings, requiredSettingsViolation, takeOwnership, } from "./authority-connection.js";
12
12
  import { inspectSchema, } from "../../storage/domain-schema.js";
13
13
  import { deliveryHandler, } from "./authority-delivery.js";
14
- import { authorityTiming, effectConfiguration, } from "./authority-settings.js";
14
+ import { authorityLogger, authorityTiming, effectConfiguration, } from "./authority-settings.js";
15
15
  import { failureCounter, registeredRuntime, runtimeSettings, wallClock, } from "./runtime-parts.js";
16
16
  import { strandingWatchdog, } from "./authority-watchdog.js";
17
+ import { projectionMemory, } from "./projection-memory.js";
17
18
  export { authorityTiming } from "./authority-settings.js";
18
19
  export function refused(reason, error) {
19
20
  return {
@@ -78,13 +79,6 @@ export async function recoverDeliveries(invoker) {
78
79
  return staged;
79
80
  }
80
81
  }
81
- function unhealthy(watchdog) {
82
- const dead = watchdog.deadLettered();
83
- if (dead !== undefined)
84
- return `dead-lettered message ${dead}`;
85
- const stranded = watchdog.stranded();
86
- return stranded === undefined ? undefined : `stranded message ${stranded}`;
87
- }
88
82
  const deliveryCallbacks = ["deliveryCompleted", "deliveryFailed"];
89
83
  async function retryDeliveryCallbacks(retry) {
90
84
  const administration = { authorizationContext: retry.broker };
@@ -131,11 +125,18 @@ async function storedActorRefusal(check) {
131
125
  return sequence === latest ? undefined : actorSequenceMismatch;
132
126
  }
133
127
  async function condition(watch) {
134
- const { runtime, broker, watchdog } = watch;
135
- const dead = watchdog.deadLettered();
136
- if (dead === undefined)
137
- return unhealthy(watchdog);
138
- return `dead-lettered message ${dead}: ${await deadLetterCause({ runtime, broker, messageId: dead })}`;
128
+ const { runtime, broker, database, watchdog } = watch;
129
+ const report = await watchdog.report((messageIds) => messageRuns(database, messageIds));
130
+ if (report === undefined)
131
+ return undefined;
132
+ const { kind, messageId } = report;
133
+ if (kind === "stranded")
134
+ return `stranded message ${messageId}`;
135
+ return `dead-lettered message ${messageId}: ${await deadLetterCause({ runtime, broker, messageId })}`;
136
+ }
137
+ async function messageRuns(database, messageIds) {
138
+ const rows = await database.connection((connection) => connection.all(`SELECT messages.id AS message_id, runs.status AS status FROM solid_messages AS messages JOIN runs ON runs.run_id = coalesce(json_extract(messages.arguments, '$.command.runId'), json_extract(messages.arguments, '$.arguments.runId')) WHERE messages.id IN (${messageIds.map(() => "?").join(", ")})`, messageIds));
139
+ return new Map(rows.map((row) => [text(row, "message_id"), text(row, "status")]));
139
140
  }
140
141
  async function deadLetterCause(lookup) {
141
142
  const { runtime, broker, messageId } = lookup;
@@ -150,6 +151,14 @@ async function deadLetterCause(lookup) {
150
151
  return "unreadable cause";
151
152
  }
152
153
  }
154
+ function invokerOf(parts) {
155
+ const { runtime, actor, broker, timing } = parts;
156
+ return (actorId, idempotencyKey) => runtime.ref(actor, actorId).with({
157
+ authorizationContext: broker,
158
+ timeoutMilliseconds: timing.callTimeoutMilliseconds,
159
+ ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
160
+ });
161
+ }
153
162
  export function authorityRuntime(database, options) {
154
163
  const timing = options.timing ?? authorityTiming;
155
164
  const reader = guardApplicationDatabase(database);
@@ -159,6 +168,8 @@ export function authorityRuntime(database, options) {
159
168
  boundMilliseconds: timing.callTimeoutMilliseconds,
160
169
  });
161
170
  const effects = effectConfiguration(options);
171
+ const logger = authorityLogger(options.log);
172
+ const memory = projectionMemory({ logger });
162
173
  const shared = { reader, effects, clock: options.clock ?? wallClock };
163
174
  const ClovingAuthority = authorityActor({
164
175
  ...shared,
@@ -168,6 +179,7 @@ export function authorityRuntime(database, options) {
168
179
  database,
169
180
  actor: ClovingAuthority,
170
181
  settings: runtimeSettings({ timing, options, broker, watchdog }),
182
+ record: memory.record,
171
183
  });
172
184
  const delivery = trackedDelivery({
173
185
  runtime,
@@ -183,22 +195,19 @@ export function authorityRuntime(database, options) {
183
195
  return {
184
196
  runtime,
185
197
  reader,
186
- invoker: (actorId, idempotencyKey) => runtime.ref(ClovingAuthority, actorId).with({
187
- authorizationContext: broker,
188
- timeoutMilliseconds: timing.callTimeoutMilliseconds,
189
- ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
190
- }),
198
+ memory,
199
+ invoker: invokerOf({ runtime, actor: ClovingAuthority, broker, timing }),
191
200
  ...delivery,
192
201
  setEpoch: (value) => {
193
202
  epoch = value;
194
203
  },
195
- unhealthy: () => unhealthy(watchdog),
204
+ logger,
196
205
  watchMilliseconds: timing.leaseMilliseconds,
197
206
  limits: effects,
198
207
  releaseEarlierOwners: () => releaseEarlierOwners({ runtime, broker, timing }),
199
208
  retryDeliveryCallbacks: () => retryDeliveryCallbacks({ runtime, broker }),
200
209
  storedActorRefusal: (actorId) => storedActorRefusal({ database, actor: ClovingAuthority, actorId }),
201
- condition: () => condition({ runtime, broker, watchdog }),
210
+ condition: () => condition({ runtime, broker, database, watchdog }),
202
211
  subscribe: sequenceSubscriber({ realtime: runtime.realtime, broker }),
203
212
  };
204
213
  }
@@ -1,35 +1,22 @@
1
- import type { Database, JsonObject } from "solid-objects";
2
- import { type AuthorityEvent, type EffectIntent } from "../../authority/reducer.ts";
3
- import { type ProjectionSnapshot } from "../../storage/decision-snapshots.ts";
1
+ import type { Database, JsonObject, Logger } from "solid-objects";
2
+ import type { AuthorityEvent, EffectIntent } from "../../authority/reducer.ts";
4
3
  import type { AdmissionResult, ExecutionManifest } from "../../workflow-admission.ts";
5
4
  import { type StoredState } from "../authority-restore.ts";
6
5
  import { type SequenceChanges, type SequenceListener } from "./authority-changes.ts";
7
6
  import { type AuthorityQueries } from "./authority-queries.ts";
8
7
  import { type EffectQueries } from "./effect-queries.ts";
8
+ import type { OperationRecord, ProjectionMemory } from "./projection-memory.ts";
9
9
  import { type ConnectionSettings } from "./authority-connection.ts";
10
10
  import { type StoreReceipt } from "./authority-receipts.ts";
11
11
  export type { StoreReceipt } from "./authority-receipts.ts";
12
12
  export type { StoredState } from "../authority-restore.ts";
13
+ export { operationRecords, type OperationRecord } from "./projection-memory.ts";
13
14
  export interface CommandContext {
14
15
  readonly recordedAt: string;
15
16
  readonly manifest?: ExecutionManifest;
16
17
  readonly epoch?: number;
17
18
  readonly documents?: readonly string[];
18
19
  }
19
- export interface OperationRecord {
20
- readonly operationId: string;
21
- readonly kind: EffectIntent["kind"];
22
- readonly runId: string;
23
- readonly createdSequence: number;
24
- readonly releasedSequence: number | null;
25
- readonly deliveries: readonly {
26
- readonly effectId: string;
27
- readonly outcome: string | null;
28
- readonly receipt: string | null;
29
- readonly purpose: string;
30
- readonly epoch: number | null;
31
- }[];
32
- }
33
20
  export interface Authority {
34
21
  readonly execute: (command: unknown, context: CommandContext) => Promise<StoreReceipt>;
35
22
  readonly state: () => Promise<StoredState>;
@@ -50,6 +37,7 @@ export interface Authority {
50
37
  export interface SessionParts {
51
38
  readonly database: Database;
52
39
  readonly reader: Database;
40
+ readonly memory: ProjectionMemory;
53
41
  readonly execute: (request: JsonObject) => Promise<unknown>;
54
42
  readonly run: (signal: AbortSignal) => Promise<void>;
55
43
  readonly stop: () => Promise<void>;
@@ -58,10 +46,9 @@ export interface SessionParts {
58
46
  readonly epoch: number;
59
47
  readonly stopLimit: number;
60
48
  readonly recoveryLimit: number;
61
- readonly unhealthy: () => string | undefined;
62
49
  readonly condition: () => Promise<string | undefined>;
50
+ readonly logger: Logger;
63
51
  readonly watchMilliseconds: number;
64
52
  readonly subscribe: (listener: SequenceListener) => () => void;
65
53
  }
66
54
  export declare function authoritySession(parts: SessionParts): Authority;
67
- export declare function operationRecords(snapshot: Pick<ProjectionSnapshot, "outbox" | "deliveries">): readonly OperationRecord[];