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.
- package/CHANGELOG.md +273 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +46 -10
- package/dist/assignment.js +3 -0
- package/dist/backup-ui.d.ts +9 -1
- package/dist/backup-ui.js +28 -3
- package/dist/backup.js +4 -3
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/builder.d.ts +18 -3
- package/dist/builder.js +152 -47
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +295 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +67 -6
- package/dist/check-report.js +254 -9
- package/dist/checkout-cleanup.js +2 -2
- package/dist/claim.d.ts +9 -0
- package/dist/claim.js +9 -1
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/release-terms.d.ts +41 -0
- package/dist/contracts/release-terms.js +53 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-service.js +4 -1
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.js +5 -0
- package/dist/deploy-rescue.d.ts +41 -0
- package/dist/deploy-rescue.js +94 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +9 -5
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +19 -4
- package/dist/exec.d.ts +9 -0
- package/dist/exec.js +22 -2
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/grant.js +5 -1
- package/dist/guides.js +22 -11
- package/dist/latency-timeline.d.ts +46 -0
- package/dist/latency-timeline.js +120 -0
- package/dist/live-backup.d.ts +39 -0
- package/dist/live-backup.js +271 -0
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +969 -328
- package/dist/planner.js +2 -2
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +2 -2
- package/dist/proof.js +6 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +80 -2
- package/dist/release-adapters.js +441 -16
- package/dist/release-authority.js +6 -2
- package/dist/release-auto.d.ts +38 -7
- package/dist/release-auto.js +131 -15
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-carry.d.ts +21 -1
- package/dist/release-carry.js +22 -0
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +95 -17
- package/dist/release-coverage.js +5 -2
- package/dist/release-gate.d.ts +5 -0
- package/dist/release-gate.js +1 -1
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +8 -2
- package/dist/release.d.ts +144 -14
- package/dist/release.js +421 -81
- package/dist/review-context.js +8 -6
- package/dist/runner-process.d.ts +67 -0
- package/dist/runner-process.js +219 -0
- package/dist/runner.d.ts +48 -6
- package/dist/runner.js +73 -9
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +19 -1
- package/dist/scope.js +26 -3
- package/dist/scout.js +2 -2
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +112 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/http.d.ts +2 -0
- package/dist/server/pages.js +7 -3
- package/dist/server/remote-hooks.d.ts +1 -1
- package/dist/server/remote-hooks.js +5 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +22 -2
- package/dist/server/task-views.js +7 -2
- package/dist/server/tasks.js +4 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage-sweep.js +1 -1
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +135 -2
- package/dist/store.js +275 -13
- package/dist/surface.js +3 -1
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +52 -3
- package/dist/teardown-git.js +145 -33
- package/dist/test-infra.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +81 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/workspace-revision.js +3 -1
- 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)
|
|
279
|
-
|
|
280
|
-
|
|
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
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -247,14 +247,14 @@ limitations under the License.
|
|
|
247
247
|
|
|
248
248
|
## shadcn/ui — MIT
|
|
249
249
|
|
|
250
|
-
|
|
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`,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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,
|
package/dist/agent-fence.js
CHANGED
|
@@ -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
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/assignment.js
CHANGED
|
@@ -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";
|
package/dist/backup-ui.d.ts
CHANGED
|
@@ -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 <file> --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
|
|
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 <file> --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 =
|
|
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
|
|
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
|
-
|
|
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`,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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,
|