toolroll 0.9.92 → 0.9.94
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 +87 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +4 -0
- package/dist/agent-fence.js +4 -1
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -3
- package/dist/assignment.js +16 -3
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +2 -2
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +48 -7
- package/dist/browser-workspace.js +3 -3
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +73 -25
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +12 -2
- package/dist/chat-actions.js +25 -2
- package/dist/chat-channel.js +11 -0
- package/dist/chat-decide.d.ts +7 -2
- package/dist/chat-decide.js +37 -9
- package/dist/chat-quiet.js +3 -3
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +126 -0
- package/dist/check-facts.js +318 -0
- package/dist/check-failure.d.ts +22 -3
- package/dist/check-failure.js +57 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-report.d.ts +7 -0
- package/dist/check-report.js +4 -3
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +16 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +2 -0
- package/dist/coding-provider.js +3 -2
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.js +4 -4
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -5
- package/dist/contracts/console-api.js +12 -5
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +61 -0
- package/dist/contracts/lead-tools.js +10 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/evals.js +2 -1
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +83 -0
- package/dist/flaky.js +264 -0
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/invoke.d.ts +5 -1
- package/dist/invoke.js +49 -14
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-progress.js +3 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +51 -2
- package/dist/lead.js +1 -1
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/needs-you.d.ts +32 -13
- package/dist/needs-you.js +108 -49
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +6 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +540 -70
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/planner.js +2 -1
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +7 -2
- package/dist/project-delete-ui.js +12 -1
- package/dist/project-delete.js +13 -2
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/proof.js +30 -5
- package/dist/provider-auth.js +1 -1
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +26 -3
- package/dist/push.js +55 -15
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +87 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +26 -17
- package/dist/release-card.d.ts +74 -6
- package/dist/release-card.js +179 -23
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +39 -9
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +38 -23
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +50 -1
- package/dist/release.js +118 -11
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +29 -21
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +22 -9
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +24 -18
- package/dist/server/chat.js +14 -12
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +23 -0
- package/dist/server/guards.js +4 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +19 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +2 -2
- package/dist/server/render-results.js +71 -73
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +156 -139
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +4 -1
- package/dist/server/route-table.js +52 -13
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +17 -6
- package/dist/server/tasks.js +273 -338
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +295 -0
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +148 -34
- package/dist/store.js +626 -177
- package/dist/surface.js +46 -5
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +36 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +40 -49
- package/dist/teardown-git.d.ts +16 -0
- package/dist/teardown-git.js +63 -5
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +18 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +82 -70
- package/dist/work-summary.d.ts +2 -2
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +28 -22
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +68 -13
- package/package.json +1 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.9.94 — 2026-11-07
|
|
6
|
+
|
|
7
|
+
- **Small tasks start in one step.** For small work you start yourself, New
|
|
8
|
+
task shows the exact terms and **Build** is your approval. If the change
|
|
9
|
+
grows past small, Toolroll stops and asks you to approve it to keep it.
|
|
10
|
+
In the terminal: `toolroll task new "<what should change>" --repo <project>`.
|
|
11
|
+
- **Start a task from a GitHub issue or pull request.** Paste the link in New
|
|
12
|
+
task, or use `toolroll task new --from <url>`. Toolroll quotes the text as
|
|
13
|
+
written outside Toolroll, and you read it before filing it.
|
|
14
|
+
- **Needs you now holds only what needs you.** Things that are simply waiting
|
|
15
|
+
say what they wait for, and the task page, chat, the terminal and pings all
|
|
16
|
+
say the same sentence. Pings are one line with one button ("#41 is ready to
|
|
17
|
+
review").
|
|
18
|
+
- **Fewer pages and fewer words.** Home has Needs you, Running and Waiting;
|
|
19
|
+
Tasks is the one list. Old links still land in the right place. A result has
|
|
20
|
+
Accept, Send back and Retry; a plan has Approve, Request changes and Reject.
|
|
21
|
+
One set of keyboard shortcuts works in the console and the terminal.
|
|
22
|
+
- **"Can I merge?"** One checklist on each task: plan, build, checks,
|
|
23
|
+
requirements, pull request, review comments and release check.
|
|
24
|
+
- **A code map for each project.** `/projects/<name>/map` and `toolroll map`
|
|
25
|
+
show the areas, files and exports of your main branch, generated by Toolroll
|
|
26
|
+
itself with no agent or model. Builds and the lead can read it, and the page
|
|
27
|
+
reports how builds with the map compare to builds without it. Turn it off
|
|
28
|
+
per project in its settings.
|
|
29
|
+
- **Flaky tests**: each project lists tests with mixed outcomes under recorded
|
|
30
|
+
conditions, from local checks and GitHub CI re-runs, with history and one
|
|
31
|
+
action, "Make a task to fix it". `toolroll flaky list|show`. Read only:
|
|
32
|
+
nothing is rerun and no check changes.
|
|
33
|
+
- **Lesson notes**: when a revision passes a test that failed, Toolroll
|
|
34
|
+
proposes a note with what was recorded. You put it in your own words and
|
|
35
|
+
accept it as a decision, then see how often that test fails afterwards.
|
|
36
|
+
- **Last included in a run** on lessons, references and decisions, and
|
|
37
|
+
decisions now reach runs in projects with no other knowledge.
|
|
38
|
+
- **Clean up** in Settings → Knowledge proposes retiring, merging or settling
|
|
39
|
+
memory entries. Nothing changes until you accept, and restore and undo merge
|
|
40
|
+
are there when you change your mind. `toolroll memory tidy|restore|unmerge`.
|
|
41
|
+
- **Toolroll cleans up task folders nobody is using.** A task idle for 14
|
|
42
|
+
days now loses its working folder even if its result is still waiting for
|
|
43
|
+
review or the task never finished, as long as nothing would be lost: no
|
|
44
|
+
uncommitted changes, every commit on its branch, nothing running in it.
|
|
45
|
+
Reviewing, Accept, Send back, Run checks, Retry and Resume all keep working;
|
|
46
|
+
the next run makes the folder again from its branch. Choose 7, 14 or 30
|
|
47
|
+
days or never in Settings → Storage, or with
|
|
48
|
+
`toolroll storage cleanup idle <days>`. `toolroll storage` shows what the
|
|
49
|
+
next sweep would free.
|
|
50
|
+
- **Folders with uncommitted changes are listed, never removed.** Settings →
|
|
51
|
+
Storage and `toolroll storage` now show them so you can decide. Storage
|
|
52
|
+
calls these "task folders" now.
|
|
53
|
+
- **Builds that vanish recover on their own, within limits.** A lost build
|
|
54
|
+
counts once and retries up to three times. Past that, or with no budget left,
|
|
55
|
+
the task stops and says why.
|
|
56
|
+
- **A missing check verdict gets one automatic recheck** of that exact commit,
|
|
57
|
+
with no agent, instead of waiting for you.
|
|
58
|
+
- **The builder comes back after a crash.** Toolroll restarts it only if you
|
|
59
|
+
left it on and it can prove the old process is gone. If you turned it off, it
|
|
60
|
+
stays off.
|
|
61
|
+
- **A changed route is one Approve.** When a task's model route changed since
|
|
62
|
+
you approved, you see one line saying what changed and approve once.
|
|
63
|
+
- **One release command.** `toolroll release <branch>` works out the next step,
|
|
64
|
+
picks up what a stopped deploy left, and updates a branch that is behind main.
|
|
65
|
+
- **Toolroll knows which computer it is on, even if the network renames it.**
|
|
66
|
+
A release no longer waits forever on "the computer that ran build #N".
|
|
67
|
+
- **Checks say how they ran.** Each check launch records its sandbox, and the
|
|
68
|
+
result page and release card show it. A check with no record reads unknown and
|
|
69
|
+
is never carried forward as sandboxed.
|
|
70
|
+
|
|
71
|
+
## 0.9.93 — 2026-11-06
|
|
72
|
+
|
|
73
|
+
- **Approve a release with one tap.** When a release is ready for your yes,
|
|
74
|
+
your phone gets one notification ("A release is ready for your approval")
|
|
75
|
+
that opens a single screen: what changed since the last release, the
|
|
76
|
+
GitHub checks, the full terms, and one **Approve release** button. Approve
|
|
77
|
+
and walk away: it deploys only after everything passes, and you hear back
|
|
78
|
+
only if something fails. In the terminal, `toolroll release approve` finds
|
|
79
|
+
the release itself — no task names or codes to copy.
|
|
80
|
+
- **No more "Confirm it stopped".** When a finished build's process record
|
|
81
|
+
is incomplete, Toolroll now checks the computer itself, stops anything left
|
|
82
|
+
over that it can prove is that build's, and carries on. The task says
|
|
83
|
+
"Making sure build #N stopped. This clears on its own." Releases wait
|
|
84
|
+
through it instead of stopping.
|
|
85
|
+
- **Checks record what they saw.** Each finished check now keeps an advisory
|
|
86
|
+
record of the exact code, conditions and failures, groundwork for a list
|
|
87
|
+
of flaky tests and for lessons from fixed failures. It never changes a
|
|
88
|
+
check's result.
|
|
89
|
+
|
|
3
90
|
## 0.9.92 — 2026-11-05
|
|
4
91
|
|
|
5
92
|
- **Toolroll learns which cheaper models do the job.** Every task now gets a
|
package/dist/activity-line.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* 40 s ago". Pure and shared: the server sends the facts, and each page words
|
|
4
4
|
* them against its own clock, so the age keeps counting between reads.
|
|
5
5
|
*
|
|
6
|
-
* After five minutes with no word the line
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* After five minutes with no word the line says when it last heard, in the
|
|
7
|
+
* same quiet secondary text (a quiet agent isn't something a person must do;
|
|
8
|
+
* amber is for what needs you). A worker whose heartbeat stopped says that
|
|
9
|
+
* instead, and that one is marked: the builder going away needs a person.
|
|
9
10
|
*/
|
|
10
11
|
/** The facts behind the line: our own words for the last thing done (never a tool's arguments), when, and whether
|
|
11
12
|
* the worker running it still answers. */
|
|
@@ -19,8 +20,10 @@ export declare const QUIET_AFTER_MS: number;
|
|
|
19
20
|
export declare const ACTIVITY_WORDS: Readonly<Record<string, string>>;
|
|
20
21
|
/** "just now", "40 s ago", "3 min ago", "2 h ago". */
|
|
21
22
|
export declare function agoWords(ms: number): string;
|
|
22
|
-
/** The line itself
|
|
23
|
+
/** The line itself. `quiet` (marked): the worker is offline, which needs a person. `stale`: silent five minutes or
|
|
24
|
+
* more, said in the same neutral words as any other update. */
|
|
23
25
|
export declare function activityLine(activity: RunActivity, now: number): {
|
|
24
26
|
text: string;
|
|
25
27
|
quiet: boolean;
|
|
28
|
+
stale: boolean;
|
|
26
29
|
};
|
package/dist/activity-line.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* 40 s ago". Pure and shared: the server sends the facts, and each page words
|
|
4
4
|
* them against its own clock, so the age keeps counting between reads.
|
|
5
5
|
*
|
|
6
|
-
* After five minutes with no word the line
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* After five minutes with no word the line says when it last heard, in the
|
|
7
|
+
* same quiet secondary text (a quiet agent isn't something a person must do;
|
|
8
|
+
* amber is for what needs you). A worker whose heartbeat stopped says that
|
|
9
|
+
* instead, and that one is marked: the builder going away needs a person.
|
|
9
10
|
*/
|
|
10
11
|
export const QUIET_AFTER_MS = 5 * 60_000;
|
|
11
12
|
/** The words for each fixed activity kind (live.ts's vocabulary) and for a progress checkpoint. */
|
|
@@ -35,13 +36,14 @@ export function agoWords(ms) {
|
|
|
35
36
|
return `${minutes} min ago`;
|
|
36
37
|
return `${Math.floor(minutes / 60)} h ago`;
|
|
37
38
|
}
|
|
38
|
-
/** The line itself
|
|
39
|
+
/** The line itself. `quiet` (marked): the worker is offline, which needs a person. `stale`: silent five minutes or
|
|
40
|
+
* more, said in the same neutral words as any other update. */
|
|
39
41
|
export function activityLine(activity, now) {
|
|
40
42
|
const at = Date.parse(activity.at);
|
|
41
43
|
const since = Number.isNaN(at) ? 0 : Math.max(0, now - at);
|
|
42
44
|
if (activity.worker === "offline")
|
|
43
|
-
return { text: `Worker offline · last word ${agoWords(since)}`, quiet: true };
|
|
45
|
+
return { text: `Worker offline · last word ${agoWords(since)}`, quiet: true, stale: false };
|
|
44
46
|
if (since >= QUIET_AFTER_MS)
|
|
45
|
-
return { text: `
|
|
46
|
-
return { text: `${activity.what} · ${agoWords(since)}`, quiet: false };
|
|
47
|
+
return { text: `Last update ${agoWords(since)}`, quiet: false, stale: true };
|
|
48
|
+
return { text: `${activity.what} · ${agoWords(since)}`, quiet: false, stale: false };
|
|
47
49
|
}
|
package/dist/agent-fence.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ export type FenceGrants = {
|
|
|
90
90
|
pool?: CheckoutPool;
|
|
91
91
|
network?: NetworkFence;
|
|
92
92
|
writes?: WriteAllowlist;
|
|
93
|
+
/** Which `readable` entries are one exact file: read with no listing of its folder, so a sibling stays denied. */
|
|
94
|
+
exactFiles?: string[];
|
|
93
95
|
/** Policy v1: the home configuration names denied by name, present or not (TaskBoundary.homeNames). */
|
|
94
96
|
homeNames?: string[];
|
|
95
97
|
/** macOS: the project's sealed mach services beyond the runtime's baseline (its `allowMachLookup`). */
|
|
@@ -359,6 +361,8 @@ export type FenceKind = "build" | "command" | "check" | "flow" | "tool-server";
|
|
|
359
361
|
* with no checkout), so no caller forgets one; every path comes from Toolroll's own records, never from the checkout.
|
|
360
362
|
*/
|
|
361
363
|
export type FenceContext = {
|
|
364
|
+
/** One exact file read back inside the fence (a build's code map), or null. */
|
|
365
|
+
exactFile?: string | null;
|
|
362
366
|
databaseFile: string | null;
|
|
363
367
|
worktree: string | null;
|
|
364
368
|
boundary: TaskBoundary | null;
|
package/dist/agent-fence.js
CHANGED
|
@@ -1006,7 +1006,7 @@ export function srtTermsOf(fence, grants = NO_GRANTS) {
|
|
|
1006
1006
|
const denyRead = new Set(fence.map(real));
|
|
1007
1007
|
// A readable grant's own folder is read too: bundlers (esbuild) find node_modules by listing it. The runtime can't
|
|
1008
1008
|
// grant a listing without the folder's files, so the shared copy's folder is read whole (its ready note among them).
|
|
1009
|
-
const allowRead = new Set(grants.readable.flatMap(path => [real(path), real(dirname(path))]));
|
|
1009
|
+
const allowRead = new Set(grants.readable.flatMap(path => grants.exactFiles?.includes(path) === true ? [real(path)] : [real(path), real(dirname(path))]));
|
|
1010
1010
|
const denyWrite = new Set([...fence, ...grants.readOnly].map(real));
|
|
1011
1011
|
for (const path of grants.writeDenies ?? [])
|
|
1012
1012
|
denyWrite.add(path);
|
|
@@ -1180,9 +1180,12 @@ export function fenceFor(kind, ctx) {
|
|
|
1180
1180
|
// launches: rewriting the code that fences a build would switch the fence off.
|
|
1181
1181
|
const made = { ...found, readOnly: [...new Set([...found.readOnly, ...toolrollInstall(ctx.worktree, ctx.home ?? homedir(), ctx.binding?.main ?? null)])].sort() };
|
|
1182
1182
|
const written = ctx.writes ?? null;
|
|
1183
|
+
const exact = ctx.exactFile ?? null;
|
|
1183
1184
|
const grants = {
|
|
1184
1185
|
...made,
|
|
1185
1186
|
...(written === null ? {} : { writes: written.writes, readOnly: [...new Set([...made.readOnly, ...written.readOnly])].sort() }),
|
|
1187
|
+
// One exact file, granted as the shared dependencies are, and never written (Claude's Edit rules read `readOnly`).
|
|
1188
|
+
...(exact === null ? {} : { readable: [...made.readable, exact], exactFiles: [exact], readOnly: [...new Set([...made.readOnly, ...(written?.readOnly ?? []), exact])].sort() }),
|
|
1186
1189
|
...(ctx.network === undefined ? {} : { network: ctx.network }),
|
|
1187
1190
|
...(ctx.machLookup === undefined || ctx.machLookup.length === 0 ? {} : { machLookup: [...ctx.machLookup] }),
|
|
1188
1191
|
// A project's checks may drive a browser (Playwright's Chromium), and so may a flow's script zone whose browser use
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/** SQL over a `task_scope` alias and a repo expression: whether that row's approval authority stands at `now` (an SQL
|
|
2
|
-
* expression, used exactly
|
|
2
|
+
* expression, used exactly four times: the mode and grant clauses, the release's expiry and a fast start's window). It never asks whether the row is approved.
|
|
3
3
|
* COALESCE: a NULL basis would make the OR chain NULL, and NOT NULL reads as "not dead"; NULL stands on nothing. */
|
|
4
4
|
export declare function authorityStandsSql(scope: string, repo: string, now: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* SQL, conjunctive with every alternative above (plain-toolroll.md P2.4): narrow only. A fast start's seal dispatches only
|
|
7
|
+
* inside its start window, and only as what it claims to be: a person's own ceremony yes to these exact bytes, proved at
|
|
8
|
+
* the seal's instant. Losing the marker leaves an ordinary person approval; nothing here grants anything.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fastStartStandsSql(scope: string, now: string): string;
|
|
5
11
|
/**
|
|
6
12
|
* SQL, conjunctive with every alternative above (results routing slice 3): a task whose sealed route tries a cheaper
|
|
7
13
|
* model (its build leg's selection is a trial) stands only on a PERSON's own yes to these exact bytes — the ceremony
|
|
@@ -18,7 +18,7 @@ import { autoSealSql } from "./release-authority.js";
|
|
|
18
18
|
/** The proofs a person gives (src/person-proof.ts), as an SQL list. */
|
|
19
19
|
const PERSON_PROOFS = "('password-now', 'sso-now', 'fresh-password', 'fresh-sso', 'session', 'cli-login', 'chat', 'passkey', 'local-owner')";
|
|
20
20
|
/** SQL over a `task_scope` alias and a repo expression: whether that row's approval authority stands at `now` (an SQL
|
|
21
|
-
* expression, used exactly
|
|
21
|
+
* expression, used exactly four times: the mode and grant clauses, the release's expiry and a fast start's window). It never asks whether the row is approved.
|
|
22
22
|
* COALESCE: a NULL basis would make the OR chain NULL, and NOT NULL reads as "not dead"; NULL stands on nothing. */
|
|
23
23
|
export function authorityStandsSql(scope, repo, now) {
|
|
24
24
|
return `COALESCE((((COALESCE(${scope}.approval_basis, '') IN ('password', 'chat') AND NOT ${machineSealSql(scope)}
|
|
@@ -31,9 +31,18 @@ export function authorityStandsSql(scope, repo, now) {
|
|
|
31
31
|
OR (${scope}.approval_basis = 'grant' AND ${grantStandsSql(`${scope}.mode_digest`, now, repo)})
|
|
32
32
|
OR (${scope}.approval_basis = 'schedule' AND ${approverStandsSql(scope, repo)})
|
|
33
33
|
OR (${scope}.approval_basis IN ('auto-check', 'auto-green') AND ${autoSealSql(scope, repo)} AND ${approverStandsSql(scope, repo)}))
|
|
34
|
-
AND ${releaseStandsSql(scope, now)} AND ${localSealStandsSql(scope, repo)}
|
|
34
|
+
AND ${releaseStandsSql(scope, now)} AND ${localSealStandsSql(scope, repo)} AND ${fastStartStandsSql(scope, now)}
|
|
35
35
|
AND (instr(COALESCE(${scope}.approved_route_json, ''), '"trial"') = 0 OR ${trialSealStandsSql(scope)})), 0)`;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* SQL, conjunctive with every alternative above (plain-toolroll.md P2.4): narrow only. A fast start's seal dispatches only
|
|
39
|
+
* inside its start window, and only as what it claims to be: a person's own ceremony yes to these exact bytes, proved at
|
|
40
|
+
* the seal's instant. Losing the marker leaves an ordinary person approval; nothing here grants anything.
|
|
41
|
+
*/
|
|
42
|
+
export function fastStartStandsSql(scope, now) {
|
|
43
|
+
return `NOT EXISTS (SELECT 1 FROM task_ref fsr WHERE fsr.backend = 'built-in' AND fsr.external_id = ${scope}.task_id AND fsr.fast_start_until IS NOT NULL
|
|
44
|
+
AND (fsr.fast_start_until <= ${now} OR COALESCE(${scope}.approval_basis, '') <> 'password' OR ${machineSealSql(scope)} OR NOT ${personSealSql(scope)}))`;
|
|
45
|
+
}
|
|
37
46
|
/**
|
|
38
47
|
* SQL, conjunctive with every alternative above (results routing slice 3): a task whose sealed route tries a cheaper
|
|
39
48
|
* model (its build leg's selection is a trial) stands only on a PERSON's own yes to these exact bytes — the ceremony
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Scope } from "./scope.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
export type ApprovalPasswordField = {
|
|
4
|
+
field: "none";
|
|
5
|
+
why: "passwordless" | "local-owner";
|
|
6
|
+
} | {
|
|
7
|
+
field: "remembered";
|
|
8
|
+
since: string;
|
|
9
|
+
} | {
|
|
10
|
+
field: "required";
|
|
11
|
+
} | {
|
|
12
|
+
field: "local-refused";
|
|
13
|
+
message: string;
|
|
14
|
+
href: string;
|
|
15
|
+
action: string;
|
|
16
|
+
};
|
|
17
|
+
export type ApprovalPasswordFacts = {
|
|
18
|
+
/** The approver. */
|
|
19
|
+
name: string;
|
|
20
|
+
taskId: string;
|
|
21
|
+
scope: Scope;
|
|
22
|
+
/** Passwordless is on for this browser session (html.ts passwordlessRendering). */
|
|
23
|
+
passwordless: boolean;
|
|
24
|
+
/** The request is this laptop's local owner (easy-start C5). */
|
|
25
|
+
localOwner: boolean;
|
|
26
|
+
/** When a password was last typed on this session, if it may still stand (scope.ts freshnessFor), else null. */
|
|
27
|
+
freshAt: number | null;
|
|
28
|
+
/** Where a refused local owner goes back to. */
|
|
29
|
+
returnTo: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function approvalPasswordField(store: Store, facts: ApprovalPasswordFacts): ApprovalPasswordField;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** When an approval form asks for a password (docs/plans/plain-toolroll.md P1.3): one function every approve form reads,
|
|
2
|
+
* so the task page, the focused chat and the shared-action review never disagree. It only decides what a form shows;
|
|
3
|
+
* approve() proves the person, the freshness, the local owner and the digest again inside its own transaction, with
|
|
4
|
+
* the same two rules this reads (freshlyApprovable, localApprovalEligible).
|
|
5
|
+
*
|
|
6
|
+
* - `none`: Passwordless is on for this session, or this computer's local owner may approve this exact work.
|
|
7
|
+
* - `remembered`: a password typed on this session in the last fifteen minutes may stand for these terms; the field
|
|
8
|
+
* shows, and may be left empty.
|
|
9
|
+
* - `required`: a password typed now: protected or escalated work, terms an agent wrote, or nothing remembered.
|
|
10
|
+
* - `local-refused`: a local owner who can't approve this here, with where to go instead. */
|
|
11
|
+
import { freshlyApprovable, localApprovalEligible } from "./scope.js";
|
|
12
|
+
import { localOwnerRefusal } from "./local-owner.js";
|
|
13
|
+
export function approvalPasswordField(store, facts) {
|
|
14
|
+
if (facts.localOwner) {
|
|
15
|
+
if (localApprovalEligible(store, facts.name, facts.taskId).ok)
|
|
16
|
+
return { field: "none", why: "local-owner" };
|
|
17
|
+
return { field: "local-refused", ...localOwnerRefusal(store.accountOf(facts.name)?.passwordSet === true, facts.returnTo) };
|
|
18
|
+
}
|
|
19
|
+
if (facts.passwordless)
|
|
20
|
+
return { field: "none", why: "passwordless" };
|
|
21
|
+
if (facts.freshAt === null)
|
|
22
|
+
return { field: "required" };
|
|
23
|
+
const gate = store.approvalGate(facts.taskId, facts.name, "person");
|
|
24
|
+
return freshlyApprovable(store, facts.taskId, facts.scope, gate.verdict === "seal" && !gate.protectedWork)
|
|
25
|
+
? { field: "remembered", since: new Date(facts.freshAt).toISOString() } : { field: "required" };
|
|
26
|
+
}
|
|
@@ -23,7 +23,7 @@ export declare function assignmentStatusBrief(assignment: AssignmentSnapshot): {
|
|
|
23
23
|
} | null;
|
|
24
24
|
runId: number | null;
|
|
25
25
|
primaryAction: {
|
|
26
|
-
code: "answer-agent" | "
|
|
26
|
+
code: "answer-agent" | "inspect-decisions" | "inspect-run" | "inspect-stop" | "inspect-task" | "open-pr" | "recheck-stopped" | "reconcile-run" | "sign-in" | import("./dispatch.js").DispatchAction;
|
|
27
27
|
label: string;
|
|
28
28
|
target: import("./work-summary.js").WorkTarget;
|
|
29
29
|
} | null;
|
package/dist/assignment-ui.js
CHANGED
|
@@ -31,8 +31,8 @@ export function assignmentActionHref(assignment) {
|
|
|
31
31
|
if (action.code === 'sign-in')
|
|
32
32
|
return '/settings/agents#providers';
|
|
33
33
|
// The confirmation sits on the task page, behind the password.
|
|
34
|
-
if (action.code === '
|
|
35
|
-
return `/t/${encodeURIComponent(assignment.rootId)}#
|
|
34
|
+
if (action.code === 'recheck-stopped')
|
|
35
|
+
return `/t/${encodeURIComponent(assignment.rootId)}#recheck-stopped`;
|
|
36
36
|
if (action.code === 'open-pr' && /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+$/.test(assignment.publication?.prUrl ?? ''))
|
|
37
37
|
return assignment.publication.prUrl;
|
|
38
38
|
if (runId !== null && (action.code === 'open-result' || action.code === 'open-pr' || action.code === 'retry-review')) {
|
|
@@ -63,7 +63,7 @@ export function assignmentAttemptsOf(assignment) {
|
|
|
63
63
|
export function assignmentAttemptsHtml(assignment) {
|
|
64
64
|
if (assignment.attempts.length === 0)
|
|
65
65
|
return html ``;
|
|
66
|
-
return html `<details class="assignment-attempts"><summary>Attempts <span class="meta">${assignment.attempts.length}</span></summary><ol>${assignmentAttemptsOf(assignment).map(attempt => html `<li data-attempt-task="${attempt.taskId}" data-history-version="${attempt.taskId}"><a href="${attempt.href}">${attempt.label}</a>${attempt.runId === null ? '' : html ` <a href="/r/${attempt.runId}">
|
|
66
|
+
return html `<details class="assignment-attempts"><summary>Attempts <span class="meta">${assignment.attempts.length}</span></summary><ol>${assignmentAttemptsOf(assignment).map(attempt => html `<li data-attempt-task="${attempt.taskId}" data-history-version="${attempt.taskId}"><a href="${attempt.href}">${attempt.label}</a>${attempt.runId === null ? '' : html ` <a href="/r/${attempt.runId}">Build #${attempt.runId}</a>`}${attempt.detail === null ? '' : html `<p class="meta">${attempt.detail}</p>`}</li>`)}</ol>${assignment.owner === null ? '' : html `<p class="meta">Lead: ${assignment.owner.label}${assignment.owner.active ? '' : ' · access ended'}</p>`}<p class="work-meta work-id">Task <span class="mono">${assignment.rootId}</span></p></details>`;
|
|
67
67
|
}
|
|
68
68
|
/** Only recognized storage limits and explicitly identified older records are
|
|
69
69
|
* secondary. Unknown findings and damaged current material stay in view. */
|
package/dist/assignment.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** One assignment follows the existing task family. It owns no execution,
|
|
2
2
|
* approval or proof state. Lead ownership and exact receipt acknowledgments
|
|
3
3
|
* are append-only actions; handoffs use the existing durable outbox. */
|
|
4
|
+
import { fastCompletionProblem } from "./fast-start.js";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import { currentActor, withActor } from "./actor.js";
|
|
6
7
|
import { replacedWords } from "./task-status.js";
|
|
@@ -252,13 +253,14 @@ export function assignmentOf(store, taskId, now, access, root, memo) {
|
|
|
252
253
|
: { code: "inspect-task", label: `Open ${successor}`, target: { taskId: successor, runId: null, decisionId: null }, access: "read", retry: "read-again" };
|
|
253
254
|
}
|
|
254
255
|
else if (!live && processFact !== null && result !== null) {
|
|
255
|
-
// Toolroll
|
|
256
|
-
//
|
|
256
|
+
// Toolroll hasn't yet proven the build stopped. It proves that itself
|
|
257
|
+
// (stop-proof.ts), so the work waits, needing no one; only when this
|
|
258
|
+
// computer can't look is it held, offering Check again — never a vouch.
|
|
257
259
|
const process = processNeedOf(processFact);
|
|
258
260
|
if ("need" in process) {
|
|
259
261
|
state = "needs-decision";
|
|
260
262
|
detail = NEEDS[process.need].sentence({ build: process.build });
|
|
261
|
-
primaryAction = { code: "
|
|
263
|
+
primaryAction = { code: "recheck-stopped", label: NEEDS[process.need].action.label, target: { taskId: current.id, runId: process.build, decisionId: null },
|
|
262
264
|
access: access.principal === "operator" ? "operator-control" : "operator-handoff", retry: "refresh-before-acting" };
|
|
263
265
|
need = { key: process.need, build: process.build };
|
|
264
266
|
}
|
|
@@ -480,6 +482,11 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
|
|
|
480
482
|
// The nightly check of main is a check only (release-once.md, part 3): nobody completes it.
|
|
481
483
|
if (store.handle.prepare("SELECT 1 AS hit FROM nightly_check WHERE task = ?").get(current.receipt.taskId) !== undefined)
|
|
482
484
|
return { ok: false, reason: "check-only", message: "The nightly check of main is only a check; it isn't completed." };
|
|
485
|
+
// Toolroll is still proving its build stopped (stop-proof.ts): a wait that clears on its own, said as one, so a
|
|
486
|
+
// release waits for it instead of stopping.
|
|
487
|
+
if (current.state !== "ready-to-check" && current.state !== "complete" && current.need != null && "wait" in current.need && STOP_WAITS.has(current.need.wait)) {
|
|
488
|
+
return { ok: false, reason: "stopping", message: `Toolroll is still making sure ${current.need.build === null ? "the build" : `build #${current.need.build}`} stopped. This clears on its own.` };
|
|
489
|
+
}
|
|
483
490
|
if (current.state !== "ready-to-check" && current.state !== "complete")
|
|
484
491
|
return { ok: false, reason: "not-ready", message: "This assignment still has unresolved execution, scope or decisions." };
|
|
485
492
|
// A release task completes only while its release approval stands for it (release-once.md, liveReleaseOf).
|
|
@@ -490,6 +497,10 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
|
|
|
490
497
|
// token, a remote API run) that may complete ordinary work but is never the second person on protected work.
|
|
491
498
|
const person = actor.startsWith("operator:") && !machineActing() ? actor.slice("operator:".length) : null;
|
|
492
499
|
const marked = actor.startsWith("operator:") ? (person === null ? COMPLETION_BY_MACHINE : COMPLETION_BY_PERSON) : null;
|
|
500
|
+
// A fast start's Accept backstop (fast-start.ts): a person only, and only a change that stayed small.
|
|
501
|
+
const grown = fastCompletionProblem(store, receipt.taskId, receipt.runId, person);
|
|
502
|
+
if (grown !== null)
|
|
503
|
+
return { ok: false, reason: "not-ready", message: grown };
|
|
493
504
|
if (current.state !== "complete") {
|
|
494
505
|
// The one readiness guard: a failed or missing required check, an unresolved requirement or HIGH finding refuses.
|
|
495
506
|
const blockers = current.readiness?.blockers ?? [];
|
|
@@ -674,3 +685,5 @@ export function assignmentUpdates(store, now, access, query, root) {
|
|
|
674
685
|
});
|
|
675
686
|
return { events, nextCursor: page.length === 0 ? after : Number(page.at(-1)["id"]), hasMore: rows.length > limit };
|
|
676
687
|
}
|
|
688
|
+
/** The waits of a build Toolroll is still proving stopped by itself (needs-you.ts processNeedOf). */
|
|
689
|
+
const STOP_WAITS = new Set(["build-finishing", "build-checking", "build-stopping"]);
|
package/dist/boot-identity.d.ts
CHANGED
|
@@ -48,10 +48,11 @@ export declare function injectBootIdentity(identity: BootIdentity | null): void;
|
|
|
48
48
|
export type BootWitness = {
|
|
49
49
|
host: string;
|
|
50
50
|
bootId: string | null;
|
|
51
|
+
machineId?: string | null;
|
|
51
52
|
};
|
|
52
53
|
/**
|
|
53
54
|
* THE ONE RULE (plan §login/reboot recovery): a witnessed process is
|
|
54
|
-
* proven dead by boot change only when the witness names THIS
|
|
55
|
+
* proven dead by boot change only when the witness names THIS machine (isThisMachine), the
|
|
55
56
|
* witness carries a well-formed boot id, this host's current boot id is
|
|
56
57
|
* well-formed and known, and the two differ. Same boot, a legacy witness
|
|
57
58
|
* without an id, a foreign host, an unknown or malformed current id — each
|
package/dist/boot-identity.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
23
|
import { readFileSync } from "node:fs";
|
|
24
|
-
import {
|
|
24
|
+
import { isThisMachine } from "./machine-identity.js";
|
|
25
25
|
/** RFC 4122 shape, any case; the kernel tokens are exactly this. */
|
|
26
26
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27
27
|
/** Normalize a candidate token: exactly one UUID, lower-cased; anything else is malformed. */
|
|
@@ -79,15 +79,17 @@ export function injectBootIdentity(identity) {
|
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* THE ONE RULE (plan §login/reboot recovery): a witnessed process is
|
|
82
|
-
* proven dead by boot change only when the witness names THIS
|
|
82
|
+
* proven dead by boot change only when the witness names THIS machine (isThisMachine), the
|
|
83
83
|
* witness carries a well-formed boot id, this host's current boot id is
|
|
84
84
|
* well-formed and known, and the two differ. Same boot, a legacy witness
|
|
85
85
|
* without an id, a foreign host, an unknown or malformed current id — each
|
|
86
86
|
* answers false, and the caller keeps its PID probe or its refusal.
|
|
87
87
|
*/
|
|
88
88
|
export function provenDeadByBootChange(witness, current = { bootId: currentBootId() }) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// A caller naming the host compares exactly (restart certification, tests); otherwise THIS machine is decided by
|
|
90
|
+
// its stable id, or for a witness without one by this machine's own names (machine-identity.ts), never by the
|
|
91
|
+
// network hostname alone: a boot id from another machine always differs from ours.
|
|
92
|
+
if (current.host !== undefined ? witness.host !== current.host : !isThisMachine(witness))
|
|
91
93
|
return false;
|
|
92
94
|
if (witness.bootId === null || current.bootId === null)
|
|
93
95
|
return false;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type BoundedStart = {
|
|
2
|
+
pid: number;
|
|
3
|
+
pgid: number;
|
|
4
|
+
startedAt: string | null;
|
|
5
|
+
};
|
|
6
|
+
export type BoundedOptions = {
|
|
7
|
+
cwd: string;
|
|
8
|
+
/** The child's whole environment. */
|
|
9
|
+
env: Record<string, string>;
|
|
10
|
+
label: string;
|
|
11
|
+
stdin?: Buffer;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
/** Past this the tree is killed (`killed: "output"`). */
|
|
14
|
+
maxStdout: number;
|
|
15
|
+
/** Kept to this many bytes; more is dropped. */
|
|
16
|
+
maxStderr: number;
|
|
17
|
+
memoryBytes?: number;
|
|
18
|
+
diskBytes?: number;
|
|
19
|
+
/** The largest file any process may write (RLIMIT_FSIZE). */
|
|
20
|
+
fileBytes?: number;
|
|
21
|
+
/** The folder whose total `diskBytes` bounds. */
|
|
22
|
+
folder?: string;
|
|
23
|
+
onStart?: (started: BoundedStart) => void;
|
|
24
|
+
sampleMs?: number;
|
|
25
|
+
};
|
|
26
|
+
export type BoundedResult = {
|
|
27
|
+
code: number | null;
|
|
28
|
+
signal: NodeJS.Signals | null;
|
|
29
|
+
stdout: Buffer;
|
|
30
|
+
stderr: string;
|
|
31
|
+
killed: "time" | "memory" | "disk" | "output" | null;
|
|
32
|
+
enforced: "system" | "checked";
|
|
33
|
+
peakBytes: number;
|
|
34
|
+
};
|
|
35
|
+
export type BoundedRunner = (file: string, args: readonly string[], options: BoundedOptions) => Promise<BoundedResult>;
|
|
36
|
+
/** Tests: forget the probed machine. */
|
|
37
|
+
export declare function resetBoundedContainmentForTests(): void;
|
|
38
|
+
/** A process's own start time, as `ps` reports it: what tells a live process from a reused pid. */
|
|
39
|
+
export declare function processStartedAt(pid: number): string | null;
|
|
40
|
+
/** The folder's total size, without following a link. */
|
|
41
|
+
export declare function folderBytes(folder: string): number;
|
|
42
|
+
export declare const boundedRun: BoundedRunner;
|
|
43
|
+
export type SpawnRecord = {
|
|
44
|
+
pid: number | null;
|
|
45
|
+
pgid: number | null;
|
|
46
|
+
startedAt: string | null;
|
|
47
|
+
folder: string | null;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* What a crashed build left: its group is killed only if the leader is still the process recorded (the pid and its start
|
|
51
|
+
* time, so a reused pid isn't killed), and its folder goes if it sits under `root`. On Linux the cgroup janitor has
|
|
52
|
+
* done this already.
|
|
53
|
+
*/
|
|
54
|
+
export declare function cleanupLeftover(record: SpawnRecord, root: string): {
|
|
55
|
+
killed: boolean;
|
|
56
|
+
removed: boolean;
|
|
57
|
+
};
|