toolroll 0.9.76 → 0.9.78

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 (144) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +5 -5
  4. package/dist/agent-fence.js +46 -10
  5. package/dist/assignment.js +3 -0
  6. package/dist/backup-ui.d.ts +9 -1
  7. package/dist/backup-ui.js +28 -3
  8. package/dist/backup.js +4 -3
  9. package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
  10. package/dist/browser/workspace.css +2 -2
  11. package/dist/browser/workspace.js +31 -31
  12. package/dist/browser-workspace.d.ts +2 -0
  13. package/dist/builder.d.ts +18 -3
  14. package/dist/builder.js +152 -47
  15. package/dist/chat-decide.js +4 -0
  16. package/dist/check-failure.d.ts +92 -0
  17. package/dist/check-failure.js +295 -0
  18. package/dist/check-progress.d.ts +28 -1
  19. package/dist/check-progress.js +105 -6
  20. package/dist/check-report.d.ts +67 -6
  21. package/dist/check-report.js +254 -9
  22. package/dist/checkout-cleanup.js +2 -2
  23. package/dist/claim.d.ts +9 -0
  24. package/dist/claim.js +9 -1
  25. package/dist/cli.js +8 -5
  26. package/dist/coding-handoff.d.ts +1 -0
  27. package/dist/coding-handoff.js +20 -11
  28. package/dist/commit-extract.d.ts +6 -0
  29. package/dist/commit-extract.js +63 -8
  30. package/dist/console-chrome.d.ts +41 -0
  31. package/dist/console-chrome.js +166 -0
  32. package/dist/contracts/console-api.d.ts +1 -1
  33. package/dist/contracts/console-api.js +1 -1
  34. package/dist/contracts/release-check.d.ts +12 -0
  35. package/dist/contracts/release-check.js +17 -0
  36. package/dist/contracts/release-terms.d.ts +41 -0
  37. package/dist/contracts/release-terms.js +53 -0
  38. package/dist/contracts/verification-receipt.d.ts +20 -2
  39. package/dist/contracts/verification-receipt.js +15 -3
  40. package/dist/contracts/verification-view.d.ts +8 -0
  41. package/dist/controller-service.js +4 -1
  42. package/dist/controller-supervisor.d.ts +8 -0
  43. package/dist/controller-supervisor.js +33 -1
  44. package/dist/demo.js +3 -2
  45. package/dist/deploy-gate.js +5 -0
  46. package/dist/deploy-rescue.d.ts +41 -0
  47. package/dist/deploy-rescue.js +94 -0
  48. package/dist/desktop-update.d.ts +1 -1
  49. package/dist/dispose.d.ts +3 -0
  50. package/dist/dispose.js +9 -5
  51. package/dist/doctor.d.ts +7 -0
  52. package/dist/doctor.js +10 -2
  53. package/dist/envelope.js +2 -1
  54. package/dist/evidence.d.ts +9 -0
  55. package/dist/evidence.js +19 -4
  56. package/dist/exec.d.ts +9 -0
  57. package/dist/exec.js +22 -2
  58. package/dist/git-floor.d.ts +16 -0
  59. package/dist/git-floor.js +36 -0
  60. package/dist/grant.js +5 -1
  61. package/dist/guides.js +22 -11
  62. package/dist/latency-timeline.d.ts +46 -0
  63. package/dist/latency-timeline.js +120 -0
  64. package/dist/live-backup.d.ts +39 -0
  65. package/dist/live-backup.js +271 -0
  66. package/dist/operate.d.ts +13 -0
  67. package/dist/operate.js +969 -328
  68. package/dist/planner.js +2 -2
  69. package/dist/prepared-evidence.d.ts +2 -1
  70. package/dist/prepared-evidence.js +8 -3
  71. package/dist/project-knowledge.js +6 -2
  72. package/dist/project-learning.js +2 -2
  73. package/dist/proof.js +6 -0
  74. package/dist/read-executor-worker.js +8 -4
  75. package/dist/read-executor.d.ts +18 -1
  76. package/dist/read-executor.js +8 -0
  77. package/dist/release-adapters.d.ts +80 -2
  78. package/dist/release-adapters.js +441 -16
  79. package/dist/release-authority.js +6 -2
  80. package/dist/release-auto.d.ts +38 -7
  81. package/dist/release-auto.js +131 -15
  82. package/dist/release-card.d.ts +15 -3
  83. package/dist/release-card.js +25 -9
  84. package/dist/release-carry.d.ts +21 -1
  85. package/dist/release-carry.js +22 -0
  86. package/dist/release-ci-hold.d.ts +41 -0
  87. package/dist/release-ci-hold.js +53 -0
  88. package/dist/release-cli.d.ts +3 -0
  89. package/dist/release-cli.js +95 -17
  90. package/dist/release-coverage.js +5 -2
  91. package/dist/release-gate.d.ts +5 -0
  92. package/dist/release-gate.js +1 -1
  93. package/dist/release-recheck.d.ts +1 -1
  94. package/dist/release-recheck.js +8 -2
  95. package/dist/release.d.ts +144 -14
  96. package/dist/release.js +421 -81
  97. package/dist/review-context.js +8 -6
  98. package/dist/runner-process.d.ts +67 -0
  99. package/dist/runner-process.js +219 -0
  100. package/dist/runner.d.ts +48 -6
  101. package/dist/runner.js +73 -9
  102. package/dist/sandbox-runtime.d.ts +8 -0
  103. package/dist/sandbox-runtime.js +14 -2
  104. package/dist/scope.d.ts +19 -1
  105. package/dist/scope.js +26 -3
  106. package/dist/scout.js +2 -2
  107. package/dist/secret-mint.d.ts +1 -0
  108. package/dist/secret-mint.js +14 -0
  109. package/dist/serve.js +112 -51
  110. package/dist/server/chat.js +5 -5
  111. package/dist/server/http.d.ts +2 -0
  112. package/dist/server/pages.js +7 -3
  113. package/dist/server/remote-hooks.d.ts +1 -1
  114. package/dist/server/remote-hooks.js +5 -3
  115. package/dist/server/render-results.d.ts +2 -0
  116. package/dist/server/render-results.js +1 -0
  117. package/dist/server/render-tasks.d.ts +2 -0
  118. package/dist/server/render-tasks.js +2 -1
  119. package/dist/server/request-context.d.ts +8 -0
  120. package/dist/server/runtime.d.ts +2 -0
  121. package/dist/server/settings.js +22 -2
  122. package/dist/server/task-views.js +7 -2
  123. package/dist/server/tasks.js +4 -1
  124. package/dist/setup-guide.d.ts +2 -2
  125. package/dist/storage-sweep.js +1 -1
  126. package/dist/storage.d.ts +5 -0
  127. package/dist/storage.js +28 -1
  128. package/dist/store.d.ts +135 -2
  129. package/dist/store.js +275 -13
  130. package/dist/surface.js +3 -1
  131. package/dist/task-base.d.ts +4 -0
  132. package/dist/task-base.js +22 -2
  133. package/dist/teardown-git.d.ts +52 -3
  134. package/dist/teardown-git.js +145 -33
  135. package/dist/test-infra.js +1 -1
  136. package/dist/toolroll-update.js +2 -2
  137. package/dist/up-processes.d.ts +137 -0
  138. package/dist/up-processes.js +165 -0
  139. package/dist/up-role-child.d.ts +1 -0
  140. package/dist/up-role-child.js +81 -0
  141. package/dist/verification-evidence.d.ts +1 -0
  142. package/dist/verification-evidence.js +13 -1
  143. package/dist/workspace-revision.js +3 -1
  144. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,278 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.78 — 2026-10-29
4
+
5
+ - **Toolroll says when it is restarting.** The worker and console now record
6
+ when they start, keep a heartbeat every 15 seconds, and record when they
7
+ begin stopping and when they have stopped. While one is stopping it takes no
8
+ new work, `/healthz` answers "draining" at once, pages say "Toolroll is
9
+ restarting", and a new start waits only until the old one has finished
10
+ stopping instead of a fixed window. A worker that stopped cleanly never
11
+ holds its projects. A second live `up` is still refused.
12
+ - **The database is copied every few minutes.** Besides scheduled backups,
13
+ the worker copies the database online every 5 minutes (change it, or the
14
+ folder, in Settings → Backups), checks each copy and keeps the newest 24
15
+ and one a day for a week. It never holds up the console or the worker. A
16
+ deploy uses the latest copy when it is recent and matches the database
17
+ exactly, instead of making a new one. Restoring stays a manual
18
+ `toolroll restore`.
19
+ - **See where the time went.** A task's page has a "Where the time went"
20
+ section under Details: when each part of its release check started and
21
+ ended, and each step of its deploys, including what a deploy waited for.
22
+ Console pages that take over a second keep a short record of where the
23
+ time went, which an instance operator reads on `/health`. Only page names,
24
+ step names and reasons are kept, never addresses or form contents.
25
+ - **A crashed worker is noticed in a minute, not three.** Workers check in
26
+ every 15 seconds and are treated as gone after a minute of silence. A
27
+ project lease is never taken for longer than a minute.
28
+ - **A release's version bump alone no longer owes the upgrade path.** Toolroll
29
+ now decides from both commits' verified files that package.json and
30
+ package-lock.json changed only in the project's own version (the check's
31
+ own rule); any other change to them still owes it.
32
+ - **The macOS suite runs on the candidate before it merges.** Pull requests
33
+ and main pushes now run the unit suite on hosted macOS with Node 22 and 24,
34
+ in four shards each, joined into `node 22 · macos-latest` and
35
+ `node 24 · macos-latest` exactly like the Linux checks. `toolroll release`
36
+ waits for them; main's protection not requiring them yet is reported, not
37
+ refused. After the merge, publication waits only for the tag's publish job,
38
+ no longer for main's own CI run on the merge commit. The nightly and tag
39
+ macOS jobs are renamed so no other workflow shares a required name.
40
+ - **A release is published without waiting for Homebrew.** `toolroll release`
41
+ ends with "Published; Homebrew pending" once npm and the GitHub release are
42
+ verified, instead of waiting up to 45 minutes for the tap's checks. Homebrew
43
+ is its own state in the release journal (pending, failed or current), and
44
+ running the same command again finishes it: it waits for the tap change's
45
+ checks, then merges exactly that change, with the checksum still taken from
46
+ npm's tarball. A tap already on a newer version is never moved back, checked
47
+ again right before the merge.
48
+ - **A release whose checks run outside the sandbox now reruns its own check
49
+ after Toolroll's machine fails.** Such a release can't carry later commits,
50
+ so it used to file without release terms, and a check stopped by Toolroll's
51
+ own shutdown, a lost build or a command that never started waited for a
52
+ person. Its approval now seals a release check purpose instead: the check
53
+ is held to the release's rules (a complete report, no CI standing in for
54
+ its units), and only a failure Toolroll itself confirmed as its own
55
+ infrastructure reruns, on the same commit under the same approval, at most
56
+ twice. Another commit, a changed scope, release check or check access, or
57
+ an approval that no longer stands never reruns. It never carries a later
58
+ commit. (Schema v138 adds the column; saved scopes are unchanged.)
59
+ - **A deploy no longer gets stuck behind an `up` that won't stop.** When the
60
+ old service's processes outlive their stop (a supervisor that exited while
61
+ its `up` ignored SIGTERM), the deployment stops them itself, but only
62
+ processes proved, by exact runtime path and arguments, to be that service's
63
+ supervisor, `up` or console and worker. Anything it can't prove refuses and
64
+ nothing is signalled.
65
+ - **A killed `up` no longer blocks the next start for three minutes.** `up`
66
+ records each process it gives its worker credential to. A new start that
67
+ proves every one of them has exited signs the old worker off and takes over
68
+ at once; a second live `up` is still refused.
69
+ - **`up` signs off however it is stopped.** SIGTERM, Ctrl-C, its supervisor
70
+ dying or its channel closing all end with the worker retired. A stop that
71
+ hangs past the worker's grace stops the console and worker outright
72
+ instead of waiting for launchd to kill it.
73
+ - **Deployments wait long enough, and say why.** Restoring the previous
74
+ service, starting the new one and waiting for its project leases now allow
75
+ two minutes (past a stale worker heartbeat's one) and name what they are
76
+ waiting for. "Current work must finish first" now says what is running and
77
+ what to do.
78
+ - **One command puts back a deployment that stopped half way.**
79
+ `toolroll release --recover-deploy` shows what recovery will do for the
80
+ unfinished deployment; `--yes` does it: stops only that service's proved
81
+ leftovers, starts the previous service again and lifts the update pause.
82
+ It is safe to run again.
83
+
84
+ ## 0.9.77 — 2026-10-29
85
+
86
+ - **CI finishes in about a third of the time.** The unit suite runs in four
87
+ shards per Node version instead of one 14–21 minute job. The required
88
+ checks keep their names ("node 22 · ubuntu-latest", "node 24 ·
89
+ ubuntu-latest") and pass only when every shard passed on the tested commit
90
+ and the shards together ran every test file once, so main's branch
91
+ protection needs no change. A flaky shard reruns in minutes, not twenty.
92
+ - **The release check's real-model journeys start sooner.** They run in the
93
+ browser lanes as soon as the scripted journeys end, instead of also waiting
94
+ for the unit suite; the check's load budget is unchanged.
95
+ - **An automatic release checks while CI runs.** With "Approve automatically
96
+ when checks pass" on, the release files its task and runs its check beside
97
+ CI instead of after it. Toolroll approves it only once every pull request
98
+ check passed on the commit.
99
+ - **A release asks for your approval while CI runs.** `toolroll release` files
100
+ the release task and asks you right away instead of waiting for GitHub's
101
+ checks; after you approve, the release check runs beside CI. The approval
102
+ says "GitHub's checks are still running; nothing deploys until they pass."
103
+ Nothing is deployed, merged, tagged or published until every required check
104
+ passed on the exact commit, read again right before the deploy; if one
105
+ fails, the release stops before the deploy. A check that already failed
106
+ still stops the release before anyone is asked.
107
+ - **The console stays responsive while builds and checks run.** `toolroll up`
108
+ now runs two processes: the console (pages, sign-in and approvals) and the
109
+ worker (builds, checks, follow-ups and housekeeping). On a copy of a real
110
+ database, the slowest console answer during a heavy check fell from about
111
+ 5–6 seconds to under 0.3 seconds. A worker that crashes is started again
112
+ and recovers its interrupted work as a restarted `up` does; a stop still
113
+ lets running builds finish first. A running check's progress is saved at
114
+ most once a second, and its final result at once. `--single-process` keeps
115
+ the old one-process layout.
116
+ - **Console pages open faster and stay quick while work runs.** The sidebar,
117
+ the Tasks count and the crew list are read once per database change for each
118
+ person instead of up to four times per page, and before a page is drawn they
119
+ are read on a background thread so the console keeps answering meanwhile.
120
+ The chat landing's lists are kept the same way, and the task page reuses its
121
+ "behind main" line for 15 seconds instead of asking Git twice per view. On a
122
+ copy of a real database: /board 348 → 26 ms, /agents 320 → 4 ms, /chat 905 →
123
+ 19 ms, a task page 1.5 s → 0.2 s; with the database changing 40 times a
124
+ second, pages take 0.27–0.47 s and the console answers other requests in
125
+ about 1 ms (before: 0.35 s typical, up to 1.4 s). A page that re-reads after
126
+ a live update gets "unchanged" when nothing it shows changed.
127
+ - **Fixes from the first review of the above.** CI's required checks now
128
+ judge the shards with the verifier from the base branch, run none of the
129
+ change's code, and fail when a shard executed no tests, its counts don't
130
+ add up, or it skips tests the base always ran; a required check counts only
131
+ from the CI workflow, never a same-named job elsewhere. A release filed
132
+ while CI runs can't be approved by anyone until Toolroll itself saw CI pass,
133
+ and you're asked only then. A worker that crashes while a removed project is
134
+ still finishing recovers that project's work before anything new starts. A
135
+ deploy waits until the console and the worker have both exited. A worker or
136
+ console that refuses to start again stops `toolroll up` instead of
137
+ restarting forever. A check's final progress is kept until it's saved.
138
+
139
+
140
+ - **Sandboxed commands no longer hang on macOS 26.5.** The fence denies most
141
+ macOS system services and, by default, asked macOS to report each denial; on
142
+ macOS 26.5.2 about one denied lookup in 70 never returned while that report
143
+ was made, so a sandboxed `npm ci`, `npm pack` or any Node program setting up
144
+ DNS could hang forever (a release carry until its 20-minute timeout). Denials
145
+ are now made without the report; nothing the fence allows or denies changed.
146
+ - **The console is fast again.** 0.9.76's release-approval setting was read by
147
+ walking a project's whole action ledger once per task row, and every page's
148
+ sidebar runs that list four times, so each page blocked the console for about
149
+ 3 seconds (more with tabs open while anything builds). It now reads the
150
+ setting rows through an existing index: pages went from about 2 s to about
151
+ 0.4 s on a real database. Sizing build checkouts every half hour no longer
152
+ freezes the console either.
153
+ - **A release check's result is always saved.** The release check's streamed
154
+ part lines now carry a tag no Toolroll version misreads as a journey's own
155
+ line; an older console counted them twice, built a progress snapshot it then
156
+ refused, and the refusal stopped the check's log and result from being saved
157
+ (the check read as "unavailable"). A progress update that fails to save can
158
+ no longer stop sealing, and a result that can't be saved says so on the run.
159
+ - **A renamed or dropped CI job no longer hides its failure.** Every failed
160
+ attempt on a commit is judged before newer runs replace older ones, so a
161
+ job a newer run renamed or dropped can't erase a test failure from an
162
+ automatic approval. The Windows containment repeat job keeps one name
163
+ whatever its count.
164
+ - **Toolroll decides what a Full release check owes.** Before the check runs,
165
+ Toolroll works out from its own record of the last release which parts it
166
+ owes (units, scripted and real-model journeys, upgrades), seals that, and
167
+ tells the check, which runs at least those. Every owed part needs a result,
168
+ whatever the check's report says it selected. CI never stands in for a
169
+ release's unit suite, and a journey or upgrade run that listed nothing it
170
+ ran is no result.
171
+ - **Missing evidence is unknown, not clean.** What passed only on a retry is
172
+ sealed in the check's receipt. If a check's report is lost or damaged, its
173
+ retries are unknown and nothing approves itself; the receipt's report and
174
+ inventory files are checked byte for byte. A check that passed only when
175
+ rerun after the approved setup counts as a retry, and the setup rerun only
176
+ happens when the check never started.
177
+ - **Merging checks main on GitHub itself.** A release reads main's head and
178
+ compares the branch through GitHub's API, never a local ref, and reads
179
+ main again right before merging. Releases approve themselves only while
180
+ main's branch protection requires branches to be up to date before
181
+ merging; without it, a person's release says which setting to turn on.
182
+ Toolroll only reads that setting.
183
+ - **No lazy fetch on any supported git.** Protected Git reads (the release's
184
+ object reader and extraction, project knowledge and learning, the task
185
+ base, release coverage, review screenshots, grants) run through a frozen
186
+ copy of the repository's config on every git version, refuse every
187
+ network transport and are time-bounded, so an include, a partial clone or
188
+ a planted filter can't fetch, run or hang anything. The deployment script
189
+ reads a commit's files with no filter or attribute applied. CI's oldest-git
190
+ lane now tests git 2.41.0 exactly.
191
+ - **Main's protection must actually hold.** A release now reads main's whole
192
+ branch protection and rulesets and counts them only when they require
193
+ branches to be up to date, require the release's CI checks, and can't be
194
+ bypassed by the account that merges ("Do not allow bypassing the above
195
+ settings"; no ruleset bypass for a role, team, app or organization admin).
196
+ A person's release names exactly what is missing, and says which new CI
197
+ checks main's protection doesn't require yet.
198
+ - **Deployment reads only verified objects, with none of the repository's
199
+ config.** The deployment script hashes the commit and every tree as well
200
+ as every file against their ids, and runs git on a copy of the
201
+ repository's layout with a config of its own, so a corrupted tree, a
202
+ config change mid-read or a custom transport helper can't substitute files
203
+ or fetch anything.
204
+ - **An unreadable last release never shrinks a check.** When the last
205
+ release Toolroll recorded can't be verified, the release check doesn't
206
+ run and says so, and the release card's change list reads "couldn't be
207
+ read", instead of acting as if there were no previous release.
208
+ - **Setup recovery needs proof the check never started.** The approved setup
209
+ reruns a check only when Toolroll itself found the check's program missing
210
+ before starting it and the shell said exactly that and nothing else. Any
211
+ other failure is the check's own result for a person to judge, and a
212
+ release after a setup rerun says why the first attempt failed.
213
+ - **Dispatch and coding handoffs can't be held up by a project's Git
214
+ config.** The worktree binding reads Git's registry as plain files, and the
215
+ handoff's branch read is bounded and runs outside its database
216
+ transactions.
217
+ - **A release check that's cut short still leaves its evidence.** Every
218
+ part of the release check (typecheck, build, units, journeys, upgrades)
219
+ now prints its output as it runs, each line tagged with its part (and
220
+ upgrade version), instead of only at the very end. Each part's end is
221
+ recorded with its start time, exit code, signal, timeout and whether it
222
+ was stopped, and a failing upgrade version's own log is printed the moment
223
+ it fails. Release check 3261 was stored as "exit 1" with nothing but its
224
+ header: a signal had ended it, the plane recorded the signal as a plain
225
+ exit 1, and the parts' output was still waiting to be printed. A command
226
+ ended by a signal, or by Toolroll's own shutdown, now says so.
227
+ - **No complete result, no pass.** A release's Full check counts only with
228
+ its valid report (carrying that run's secret), a result for every part,
229
+ unit file, journey group and upgrade version it owed, and a successful
230
+ exit. A missing, malformed, cut-short or incomplete report is an error
231
+ ("no result"), never a pass.
232
+ - **Failures are classed, and only Toolroll's own infrastructure is rerun.**
233
+ The flat "two flakes" allowance is gone. A failing test, typecheck, build,
234
+ attack test or upgrade is never rerun to pass: fix the branch. A failure
235
+ Toolroll itself confirms as infrastructure (the check never started, the
236
+ worker lost the build, Toolroll's shutdown stopped it, or a GitHub job
237
+ whose runner failed before any of its steps ran) is checked again on the
238
+ same commit under the same approval, at most once per incident and twice
239
+ per commit, with no new approval. An unexplained failure blocks: a
240
+ diagnostic rerun may run, but its pass doesn't erase the failure, so you
241
+ decide. Real-model journeys are counted, never rerun until they pass, and
242
+ one that passed only on a retry is now reported (an automatic approval
243
+ won't approve it). `toolroll release` says which of these happened and
244
+ what to do next, instead of always asking for `--new`.
245
+ - **A password that starts with "--" works on the command line.** About one
246
+ minted password in 4096 began with "--", and `toolroll release publish-ok`
247
+ (and other commands) read it as a missing value. A `--token` or
248
+ `--password` value is now always the next argument, `--token=<value>`
249
+ works everywhere, and new passwords, invites and tokens never start with
250
+ "-".
251
+ - **CI tests the commit a release trusts.** Every pull-request job now checks
252
+ out the pull request's own head commit, not the merge commit GitHub makes,
253
+ and proves it before building.
254
+ - **Git 2.41 or newer is required, and tested.** Toolroll's hardened git reads
255
+ need `git --attr-source`, which arrived in git 2.41. `toolroll up`, `serve`
256
+ and `watch` won't start on an older git, and `toolroll doctor` names it,
257
+ each with the installed and the required version. Git 2.41 to 2.44 has no
258
+ `--no-lazy-fetch`: there Toolroll leaves it out and instead refuses a
259
+ project that is a partial clone for protected reads ("this project is a
260
+ partial clone; protected reads need git 2.45 or newer"), judged from the
261
+ frozen copy of the config git then runs with (never checked, then run),
262
+ and every read still refuses all network protocols.
263
+ Two CI jobs run the git attack and worktree tests on git 2.41 and 2.47, as
264
+ an unprivileged user, fail if any test is skipped, and are now required
265
+ for a release.
266
+ - **A release branch must contain main.** CI tests the pull request's own
267
+ commit, so `toolroll release` refuses a branch that doesn't contain main's
268
+ head ("the branch is behind main; update it and push"), at the gate before
269
+ anything is filed and again right before the merge.
270
+ - **Tests can't lean on a guessed git identity.** Each test's home tells git
271
+ never to guess a name and email from the host, so a test that commits
272
+ without setting one fails on a Mac just as it would on Linux CI.
273
+ - **Two timing-sensitive tests now wait for events.** The read-worker test and
274
+ the watch recovery test no longer fail when a busy machine runs slowly.
275
+
3
276
  ## 0.9.76 — 2026-10-28
4
277
 
5
278
  - **Releases can approve themselves when they're green.** A new per-project
package/README.md CHANGED
@@ -34,7 +34,7 @@ npx toolroll up
34
34
  ```
35
35
 
36
36
  That starts the app and a builder for your projects, then opens the console
37
- at http://127.0.0.1:4180. Runs on macOS or Linux with Node.js 22.13+ and git.
37
+ at http://127.0.0.1:4180. Runs on macOS or Linux with Node.js 22.13+ and git 2.41+.
38
38
  Try the sandbox first with `npx toolroll demo`: ask its scripted lead for a
39
39
  change in Chat, approve the plan, and watch it land Ready. It never calls a
40
40
  model, spends or reaches outside.
@@ -275,9 +275,16 @@ npx toolroll up --project-root ~/Projects # or: bunx toolroll up
275
275
 
276
276
  That is the install and the setup. It needs Node 22.13 or newer on the
277
277
  machine (Bun's runtime has no `node:sqlite`; `bunx` hands the shebang to
278
- Node, so it works too). `npm install -g toolroll` gives you the
279
- bare `toolroll` command for later (`standing-orders`, the older name, still
280
- works).
278
+ Node, so it works too), and git 2.41 or newer: Toolroll reads agent-written
279
+ checkouts with `git --attr-source`, which older git refuses, and `toolroll
280
+ up`, `serve`, `watch` and `doctor` say so when the installed git is older.
281
+ Ubuntu 24.04 ships 2.43, which works; Debian 12 ships 2.39: install a newer
282
+ one there. On git older than 2.45 (no `git --no-lazy-fetch`), a project that
283
+ is a partial clone (`git clone --filter=…`) is refused for Toolroll's
284
+ protected reads: use git 2.45 or newer for those, or a full clone. CI runs
285
+ the git tests on 2.41 and on 2.47.
286
+ `npm install -g toolroll` gives you the bare `toolroll` command for later
287
+ (`standing-orders`, the older name, still works).
281
288
 
282
289
  `up` prints your login once (and saves it beside the database as
283
290
  `up-login.txt`), opens the app in your browser, and connects this machine as
@@ -247,14 +247,14 @@ limitations under the License.
247
247
 
248
248
  ## shadcn/ui — MIT
249
249
 
250
- Button and Dialog composition in `src/browser/ui/primitives.tsx` adapts:
250
+ Dialog composition in `src/browser/ui/primitives.tsx` adapts:
251
251
  https://github.com/shadcn-ui/ui/tree/a87a63b2ca25143d26c8bd0903e4e9bc77b3f824/apps/v4/registry/new-york-v4/ui
252
252
 
253
253
  Source inspected 20 September 2026, revision
254
- `a87a63b2ca25143d26c8bd0903e4e9bc77b3f824`, files `button.tsx` and
255
- `dialog.tsx`. Adaptations replace Tailwind/CVA with local class variants, use
256
- individual Radix packages, default buttons to type=button, and leave dialog
257
- close controls to the calling surface. Radix supplies focus and dismissal behavior.
254
+ `a87a63b2ca25143d26c8bd0903e4e9bc77b3f824`, file `dialog.tsx`. Adaptations
255
+ replace Tailwind/CVA with local class variants, use individual Radix packages,
256
+ and leave dialog close controls to the calling surface. Radix supplies focus
257
+ and dismissal behavior.
258
258
 
259
259
  The rebuilt views (23 September 2026) add `src/browser/components/ui/`:
260
260
  button, badge, card, tabs, radio-group, select, switch, label, input,
@@ -25,7 +25,7 @@ import { INSTANCE_DIR } from "./console-instance.js";
25
25
  import { ACP_AGENTS, ACP_PROVIDER_IDS } from "./acp-agents.js";
26
26
  import { spawnSync } from "node:child_process";
27
27
  import { createHash } from "node:crypto";
28
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
28
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
29
29
  import { createRequire } from "node:module";
30
30
  import { fileURLToPath } from "node:url";
31
31
  import { homedir, tmpdir } from "node:os";
@@ -33,6 +33,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
33
33
  import { readMonitoring } from "./monitoring-settings.js";
34
34
  import { existingFolders } from "./names.js";
35
35
  import { ensureGitViewsRoot, gitViewsRoot } from "./git-views.js";
36
+ import { ensureRunnerProcessFolder, RUNNER_PROCESS_DIR } from "./runner-process.js";
36
37
  import { DEPS_FOLDER, linkedKey, readyCopy, sharedCopies } from "./shared-deps.js";
37
38
  import { SEATBELT_PROBE, sandboxRuntimeProblem, srtSpawn } from "./sandbox-runtime.js";
38
39
  import { protectedStorageDenies, protectedStorageNames } from "./protected-storage.js";
@@ -53,6 +54,22 @@ function inside(child, parent) {
53
54
  }
54
55
  export class CheckoutBindingError extends Error {
55
56
  }
57
+ /** One small file read only as the regular file it is: never through a link, never blocking on a FIFO; null otherwise. */
58
+ function regularText(path) {
59
+ let fd = null;
60
+ try {
61
+ fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
62
+ const held = fstatSync(fd);
63
+ return held.isFile() && held.size <= 64 * 1024 ? readFileSync(fd, "utf8") : null;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ finally {
69
+ if (fd !== null)
70
+ closeSync(fd);
71
+ }
72
+ }
56
73
  export const HARDENED_GIT_ARGS = ["--no-optional-locks", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null"];
57
74
  const GIT_ENV_NAMES = ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "SystemRoot", "LANG", "LC_ALL", "CODEX_HOME"];
58
75
  export function checkoutGitEnv(base = process.env) {
@@ -86,18 +103,20 @@ export function trustedWorktreeBinding(repository, worktree, poolRoots = []) {
86
103
  }
87
104
  if (!lstatSync(join(main, ".git")).isDirectory() || !lstatSync(registry).isDirectory())
88
105
  throw fail();
89
- // Query the trusted checkout, never the candidate's configurable Git pointer.
90
- const listed = spawnSync("git", [...HARDENED_GIT_ARGS, "worktree", "list", "--porcelain", "-z"], { cwd: main, env: checkoutGitEnv(), encoding: "utf8" });
91
- if (listed.status !== 0 || !listed.stdout.split("\0").includes(`worktree ${checkout}`))
92
- throw fail();
106
+ // The trusted checkout's own worktree registry, never the candidate's configurable Git pointer — read as files, with
107
+ // no git at all (eval-harness review 2, finding 6): the repository's config is shared with every build, so an
108
+ // include naming a FIFO could hold a git read (and dispatch) up; a FIFO or link planted as a registry file is never
109
+ // opened as one (regularText).
93
110
  for (const entry of readdirSync(registry)) {
94
111
  const path = join(registry, entry);
95
112
  if (!lstatSync(path).isDirectory() || !inside(real(path), real(registry)))
96
113
  continue;
97
- if (real(readFileSync(join(path, "gitdir"), "utf8").trim()) !== join(checkout, ".git"))
114
+ const pointer = regularText(join(path, "gitdir"));
115
+ if (pointer === null || real(pointer.trim()) !== join(checkout, ".git"))
98
116
  continue;
99
117
  const gitdir = real(path);
100
- if (worktreeGitDir(checkout) !== gitdir || real(join(gitdir, readFileSync(join(gitdir, "commondir"), "utf8").trim())) !== real(join(main, ".git")))
118
+ const common = regularText(join(gitdir, "commondir"));
119
+ if (common === null || worktreeGitDir(checkout) !== gitdir || real(join(gitdir, common.trim())) !== real(join(main, ".git")))
101
120
  throw fail();
102
121
  return { main, gitdir };
103
122
  }
@@ -149,6 +168,21 @@ export function agentFence(options) {
149
168
  catch { /* fenced below either way */ }
150
169
  }
151
170
  fence.add(runFolder);
171
+ const runnerFolder = join(state, RUNNER_PROCESS_DIR);
172
+ try {
173
+ ensureRunnerProcessFolder(runnerFolder);
174
+ }
175
+ catch { /* both the name and its target stay denied */ }
176
+ fence.add(runnerFolder);
177
+ fence.add(real(runnerFolder));
178
+ // A copy may first appear after launch. Fence its permanent namespace even when empty.
179
+ const backups = join(state, "backups");
180
+ try {
181
+ mkdirSync(backups, { mode: 0o700 });
182
+ }
183
+ catch { /* name and target remain denied */ }
184
+ fence.add(backups);
185
+ fence.add(real(backups));
152
186
  // Only a folder that is Toolroll's own (~/.config/toolroll or an older name,
153
187
  // the desktop app's "Toolroll") is fenced whole: a database kept
154
188
  // in a shared folder (a projects folder, a test's temp folder) must not
@@ -196,7 +230,7 @@ export function agentFence(options) {
196
230
  // Secret, state, database, explicit-extra and home-config denies are never exemptible.
197
231
  for (const path of options.boundary?.protectedShell ?? [])
198
232
  fence.add(real(path));
199
- const protectedPaths = new Set([...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]));
233
+ const protectedPaths = new Set([...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups"].includes(basename(path)) ? [path, real(path)] : [real(path)]));
200
234
  // Protected storage (chat apps, browser profiles, password managers): every launch, every policy, never exemptible,
201
235
  // kept by the name it was resolved under as well as where it leads.
202
236
  const storage = options.protectedStorage ?? protectedStorageDenies(home);
@@ -208,7 +242,7 @@ export function agentFence(options) {
208
242
  fence.add(path);
209
243
  const binding = options.binding;
210
244
  const needed = binding === undefined || binding === null ? [] : [binding.gitdir, real(join(binding.main, ".git"))];
211
- return [...new Set([...[...fence].flatMap(path => path === runFolder ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
245
+ return [...new Set([...[...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups"].includes(basename(path)) ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
212
246
  if (protectedPaths.has(path) || !needed.some(target => target === path || inside(target, path)))
213
247
  return true;
214
248
  console.warn(`Dropped build fence entry covering this checkout's own Git data: ${path}`);
@@ -670,7 +704,9 @@ export function fencedGitEnv(base = process.env) {
670
704
  /** The repository a git worktree belongs to (its `.git` file names the main repository's git folder). */
671
705
  function worktreeGitDir(worktree) {
672
706
  try {
673
- const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(join(worktree, ".git"), "utf8"))?.[1]?.trim();
707
+ // The candidate's own pointer file: a FIFO or link there is never opened (review 2, finding 6).
708
+ const text = regularText(join(worktree, ".git"));
709
+ const pointer = text === null ? undefined : /^gitdir:\s*(.+)$/m.exec(text)?.[1]?.trim();
674
710
  return pointer === undefined ? null : real(isAbsolute(pointer) ? pointer : join(worktree, pointer));
675
711
  }
676
712
  catch {
@@ -140,6 +140,9 @@ function buildChecks(store, root, runId) {
140
140
  if (receipt.result.failure === "coverage-incomplete")
141
141
  return { status: "failed", exitCode: null, command: receipt.command.command, logArtifactId: receipt.log.artifactId, level,
142
142
  detail: "Checks didn't cover everything this release owed, or a required test went missing." };
143
+ if (receipt.result.failure === "no-result")
144
+ return { status: "failed", exitCode: null, command: receipt.command.command, logArtifactId: receipt.log.artifactId, level,
145
+ detail: "Checks gave no complete result, so they can't count as a pass." };
143
146
  // The sealed receipt names the grant that ran: the quick one, or the full one.
144
147
  const sealedLevel = typeof receipt.command?.repo === "string" && receipt.command.repo.startsWith("quick:") ? "quick" : "full";
145
148
  const words = sealedLevel === "quick" ? "Quick checks" : "Checks";
@@ -4,9 +4,11 @@
4
4
  * disclosures.
5
5
  */
6
6
  import { type Html } from "./html.js";
7
- import type { BackupRun, BackupSettings } from "./store.js";
7
+ import type { BackupRun, BackupSettings, LiveBackupState } from "./store.js";
8
8
  /** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
9
9
  export declare const BACKUP_EVERY_HOURS: readonly [1, 6, 12, 24];
10
+ /** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
11
+ export declare const LIVE_EVERY: readonly [1, 5, 15, 30];
10
12
  export declare const MAX_KEEP = 100;
11
13
  export declare const BACKUP_CSS: string;
12
14
  export type BackupView = {
@@ -16,6 +18,12 @@ export type BackupView = {
16
18
  runs: BackupRun[];
17
19
  csrf: string;
18
20
  now: Date;
21
+ /** The continuous copy (live-backup.ts): its settings, latest verified copy and folder. */
22
+ live?: {
23
+ state: LiveBackupState;
24
+ folder: string;
25
+ defaultFolder: string;
26
+ };
19
27
  };
20
28
  export declare function backupHtml(view: BackupView, notice: {
21
29
  said?: string | null;
package/dist/backup-ui.js CHANGED
@@ -8,6 +8,8 @@ import { bytesWords } from "./storage.js";
8
8
  import { whenUtc } from "./when-html.js";
9
9
  /** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
10
10
  export const BACKUP_EVERY_HOURS = [1, 6, 12, 24];
11
+ /** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
12
+ export const LIVE_EVERY = [1, 5, 15, 30];
11
13
  export const MAX_KEEP = 100;
12
14
  const when = (at) => whenUtc(at);
13
15
  const whenWords = (at) => `${at.slice(0, 16).replace("T", " ")} UTC`;
@@ -23,6 +25,19 @@ export const BACKUP_CSS = `.backups{max-width:720px;min-width:0;overflow-wrap:an
23
25
  `.backups li{display:flex;gap:8px 12px;flex-wrap:wrap;padding:8px 0;border-top:1px solid var(--line, rgba(0,0,0,.08));font-size:.875rem}` +
24
26
  `.backups li .failed{color:var(--danger)}.backups code{white-space:nowrap}.backups pre{overflow-x:auto;margin:8px 0}` +
25
27
  `@media (max-width:900px){.backups .backup-state form,.backups .backup-state button{width:100%}.backups .pair{grid-template-columns:1fr}}`;
28
+ /** The continuous copy in one line: its latest verified copy, or why there is none. */
29
+ function liveLine(view) {
30
+ const live = view.live;
31
+ if (live === undefined)
32
+ return "";
33
+ if (!live.state.settings.enabled)
34
+ return html `<p class="meta">Continuous copy is off.</p>`;
35
+ if (live.state.problem !== null)
36
+ return html `<p class="problem" role="alert">The continuous copy failed: ${live.state.problem}</p>`;
37
+ if (live.state.latest === null)
38
+ return html `<p class="meta">The first continuous copy is made within a few minutes of Toolroll starting.</p>`;
39
+ return html `<p class="meta" data-live-copy>Continuous copy checked ${when(live.state.latest.madeAt)}, every ${live.state.settings.everyMinutes === 1 ? "minute" : `${live.state.settings.everyMinutes} minutes`}.</p>`;
40
+ }
26
41
  /** The state in one line, and the next backup (if any), for the top of the page. */
27
42
  function state(view) {
28
43
  const last = view.runs[0];
@@ -31,13 +46,13 @@ function state(view) {
31
46
  : good === undefined ? "The first scheduled backup runs within a minute or two of the console starting."
32
47
  : `Next one about ${whenWords(new Date(Date.parse(good.startedAt) + view.settings.everyHours * 3_600_000).toISOString())}.`;
33
48
  if (last === undefined)
34
- return html `<h2>No backups yet</h2><p class="meta">${next}</p>`;
49
+ return html `<h2>No backups yet</h2><p class="meta">${next}</p>${liveLine(view)}`;
35
50
  if (last.ok === null)
36
51
  return html `<h2>Backing up</h2><p class="meta">Started ${when(last.startedAt)}.</p>`;
37
52
  if (last.ok === false) {
38
53
  return html `<h2 data-backup-state="failed">Last backup failed</h2><p class="problem" role="alert">${when(last.startedAt)}: ${last.error ?? "no reason was recorded"}</p><p class="meta">${good === undefined ? "No backup has succeeded yet." : html `Last good backup ${when(good.startedAt)}.`} ${next}</p>`;
39
54
  }
40
- return html `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}</p>`;
55
+ return html `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}</p>${liveLine(view)}`;
41
56
  }
42
57
  export function backupHtml(view, notice) {
43
58
  const s = view.settings;
@@ -46,5 +61,15 @@ export function backupHtml(view, notice) {
46
61
  ...BACKUP_EVERY_HOURS.map(hours => html `<option value="${hours}"${s.enabled && s.everyHours === hours ? html ` selected` : ""}>${everyWords(hours)}</option>`)];
47
62
  const runs = view.runs.map(run => html `<li data-backup-run="${run.id}"><span>${when(run.startedAt)}</span>${run.ok === true ? html `<span>Succeeded${run.bytes === null ? "" : ` · ${bytesWords(run.bytes)}`}</span>${run.file === null ? "" : html `<span class="mono meta">${fileName(run.file)}</span>`}`
48
63
  : run.ok === false ? html `<span class="failed">Failed: ${run.error ?? "no reason recorded"}</span>` : html `<span>Running</span>`}<span class="meta">${run.trigger === "manual" ? "by hand" : "scheduled"}</span></li>`);
49
- return html `<section class="backups">${note}<div class="card backup-state"><div>${state(view)}</div>${postForm("/settings/backups/now", html `<button type="submit">Back up now</button>`)}</div><h2>Schedule</h2>${postForm("/settings/backups", html `<fieldset><div class="pair"><label>Back up<select name="every">${choices}</select></label><label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div><label>Folder<input type="text" name="folder" value="${s.folder ?? ""}" placeholder="${view.defaultFolder}" spellcheck="false"></label>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}</fieldset><button type="submit">Save</button>`)}<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs.length === 0 ? html `<p class="meta">None yet.</p>` : html `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${view.folder}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details><details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore &lt;file&gt; --dry-run</code></pre><p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p></details></section>`;
64
+ return html `<section class="backups">${note}<div class="card backup-state"><div>${state(view)}</div>${postForm("/settings/backups/now", html `<button type="submit">Back up now</button>`)}</div><h2>Schedule</h2>${postForm("/settings/backups", html `<fieldset><div class="pair"><label>Back up<select name="every">${choices}</select></label><label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div><label>Folder<input type="text" name="folder" value="${s.folder ?? ""}" placeholder="${view.defaultFolder}" spellcheck="false"></label>${liveFields(view)}${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}</fieldset><button type="submit">Save</button>`)}<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs.length === 0 ? html `<p class="meta">None yet.</p>` : html `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${view.folder}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details><details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore &lt;file&gt; --dry-run</code></pre><p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p>${view.live === undefined ? "" : html `<p class="meta">Continuous copies are in <span class="mono">${view.live.folder}</span>; restore one the same way.</p>`}</details></section>`;
65
+ }
66
+ /** The continuous copy's two choices, beside the schedule's: how often, and where (blank: beside the database). */
67
+ function liveFields(view) {
68
+ const live = view.live;
69
+ if (live === undefined)
70
+ return "";
71
+ const on = live.state.settings;
72
+ const choices = [html `<option value="off"${on.enabled ? "" : html ` selected`}>Off</option>`,
73
+ ...LIVE_EVERY.map(minutes => html `<option value="${minutes}"${on.enabled && on.everyMinutes === minutes ? html ` selected` : ""}>${minutes === 1 ? "Every minute" : `Every ${minutes} minutes`}</option>`)];
74
+ return html `<div class="pair"><label>Copy continuously<select name="live_every">${choices}</select></label><label>Copy folder<input type="text" name="live_folder" value="${on.folder ?? ""}" placeholder="${live.defaultFolder}" spellcheck="false"></label></div>`;
50
75
  }
package/dist/backup.js CHANGED
@@ -19,6 +19,7 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
19
19
  import { createRequire } from "node:module";
20
20
  import { BACKUP_EVERY_HOURS, MAX_KEEP } from "./backup-ui.js";
21
21
  import { verifyLedgerChain } from "./ledger-chain.js";
22
+ import { DEFAULT_LIVENESS_MS } from "./runner.js";
22
23
  import { openStore, readSchemaVersion, SCHEMA_VERSION } from "./store.js";
23
24
  /** A scheduled backup's name: the database it came from (see `databaseTag`), then when it was made. Backups made
24
25
  * before the tag was added have none. Retention only ever removes files named like this. */
@@ -28,8 +29,8 @@ export const OLDEST_RESTORABLE = 103;
28
29
  export { BACKUP_EVERY_HOURS, MAX_KEEP };
29
30
  /** How long the console's lease lasts without a renewal: a console that stopped holds it no longer than this. */
30
31
  const LEASE_MS = 3 * 60_000;
31
- /** A worker that heartbeat this recently is taken to be running. */
32
- const RUNNER_FRESH_MS = 3 * 60_000;
32
+ /** A worker that heartbeat this recently is taken to be running (runner.ts DEFAULT_LIVENESS_MS). */
33
+ const RUNNER_FRESH_MS = DEFAULT_LIVENESS_MS;
33
34
  export const defaultBackupFolder = (databaseFile) => join(dirname(databaseFile), "backups");
34
35
  export const backupFolderOf = (settings, databaseFile) => settings.folder ?? defaultBackupFolder(databaseFile);
35
36
  /** 2026-09-28T05:34:39.123Z → 2026-09-28-053439 (UTC). */
@@ -287,7 +288,7 @@ export async function restoreDatabase(options) {
287
288
  try {
288
289
  const running = runningNow(store, now);
289
290
  if (running.length > 0)
290
- refuse("running", `Toolroll is running (${running.join(" and ")}). Stop it first, then restore. If it has just stopped, wait a few minutes.`);
291
+ refuse("running", `Toolroll is running (${running.join(" and ")}). Stop it first, then restore. If it has just stopped, wait a minute.`);
291
292
  folder = backupFolderOf(store.backupSettings(), databaseFile);
292
293
  }
293
294
  finally {
@@ -247,14 +247,14 @@ limitations under the License.
247
247
 
248
248
  ## shadcn/ui — MIT
249
249
 
250
- Button and Dialog composition in `src/browser/ui/primitives.tsx` adapts:
250
+ Dialog composition in `src/browser/ui/primitives.tsx` adapts:
251
251
  https://github.com/shadcn-ui/ui/tree/a87a63b2ca25143d26c8bd0903e4e9bc77b3f824/apps/v4/registry/new-york-v4/ui
252
252
 
253
253
  Source inspected 20 September 2026, revision
254
- `a87a63b2ca25143d26c8bd0903e4e9bc77b3f824`, files `button.tsx` and
255
- `dialog.tsx`. Adaptations replace Tailwind/CVA with local class variants, use
256
- individual Radix packages, default buttons to type=button, and leave dialog
257
- close controls to the calling surface. Radix supplies focus and dismissal behavior.
254
+ `a87a63b2ca25143d26c8bd0903e4e9bc77b3f824`, file `dialog.tsx`. Adaptations
255
+ replace Tailwind/CVA with local class variants, use individual Radix packages,
256
+ and leave dialog close controls to the calling surface. Radix supplies focus
257
+ and dismissal behavior.
258
258
 
259
259
  The rebuilt views (23 September 2026) add `src/browser/components/ui/`:
260
260
  button, badge, card, tabs, radio-group, select, switch, label, input,