toolroll 0.9.88 → 0.9.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +16 -10
  3. package/THIRD_PARTY_NOTICES.md +37 -0
  4. package/dist/agent-board.d.ts +9 -0
  5. package/dist/agent-board.js +20 -0
  6. package/dist/agent-fence.js +20 -2
  7. package/dist/approval-authority.d.ts +7 -0
  8. package/dist/approval-authority.js +13 -2
  9. package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
  10. package/dist/browser/terminal.js +42 -42
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +30 -30
  13. package/dist/browser-workspace.d.ts +13 -0
  14. package/dist/builder.js +5 -2
  15. package/dist/chat-actions.d.ts +5 -0
  16. package/dist/chat-actions.js +7 -1
  17. package/dist/chat-channel.js +11 -1
  18. package/dist/chat-delivery.js +2 -0
  19. package/dist/chat-task-actions.d.ts +1 -0
  20. package/dist/check-access.d.ts +3 -0
  21. package/dist/check-access.js +4 -1
  22. package/dist/cli-help.d.ts +1 -1
  23. package/dist/cli-help.js +6 -2
  24. package/dist/cli.d.ts +6 -0
  25. package/dist/cli.js +23 -0
  26. package/dist/coding-ui.js +4 -2
  27. package/dist/connect-cli.js +2 -1
  28. package/dist/console-instance.d.ts +34 -1
  29. package/dist/console-instance.js +124 -15
  30. package/dist/contracts/cli.d.ts +72 -0
  31. package/dist/contracts/cli.js +13 -0
  32. package/dist/contracts/console-api.d.ts +9 -2
  33. package/dist/contracts/console-api.js +9 -2
  34. package/dist/desktop-host.js +8 -3
  35. package/dist/desktop-update.js +2 -1
  36. package/dist/dispatch.d.ts +3 -0
  37. package/dist/dispatch.js +22 -2
  38. package/dist/easy-start-args.d.ts +13 -0
  39. package/dist/easy-start-args.js +24 -0
  40. package/dist/easy-start.d.ts +77 -0
  41. package/dist/easy-start.js +321 -0
  42. package/dist/egress.js +5 -0
  43. package/dist/first-run.d.ts +77 -0
  44. package/dist/first-run.js +45 -0
  45. package/dist/flow-starters.js +3 -0
  46. package/dist/flow-triggers.js +4 -0
  47. package/dist/folder-gate.d.ts +32 -0
  48. package/dist/folder-gate.js +43 -0
  49. package/dist/guides.js +1 -1
  50. package/dist/invoke.js +11 -6
  51. package/dist/laptop-background.d.ts +171 -0
  52. package/dist/laptop-background.js +550 -0
  53. package/dist/laptop-service.d.ts +111 -0
  54. package/dist/laptop-service.js +268 -0
  55. package/dist/lead-cli.js +2 -1
  56. package/dist/lead-doors.d.ts +6 -0
  57. package/dist/lead-doors.js +20 -2
  58. package/dist/live-records.d.ts +1 -1
  59. package/dist/live-records.js +3 -12
  60. package/dist/local-owner.d.ts +124 -0
  61. package/dist/local-owner.js +212 -0
  62. package/dist/login-secrets.d.ts +60 -0
  63. package/dist/login-secrets.js +195 -0
  64. package/dist/mcp-oauth.d.ts +4 -0
  65. package/dist/mcp-oauth.js +13 -7
  66. package/dist/model-choices.d.ts +43 -0
  67. package/dist/model-choices.js +80 -0
  68. package/dist/operate.d.ts +46 -1
  69. package/dist/operate.js +461 -40
  70. package/dist/person-proof.js +3 -0
  71. package/dist/project-access.js +15 -1
  72. package/dist/project-admission.d.ts +6 -1
  73. package/dist/project-admission.js +44 -7
  74. package/dist/project-identity.d.ts +8 -0
  75. package/dist/project-identity.js +43 -0
  76. package/dist/project-knowledge.d.ts +7 -7
  77. package/dist/project.d.ts +1 -0
  78. package/dist/project.js +6 -0
  79. package/dist/proposal.js +11 -1
  80. package/dist/quick-start.d.ts +3 -1
  81. package/dist/quick-start.js +6 -1
  82. package/dist/recipes.js +12 -1
  83. package/dist/release-cli.js +2 -1
  84. package/dist/runner-process.d.ts +9 -0
  85. package/dist/runner-process.js +18 -0
  86. package/dist/sample-project.d.ts +31 -0
  87. package/dist/sample-project.js +141 -0
  88. package/dist/scope.d.ts +24 -2
  89. package/dist/scope.js +74 -3
  90. package/dist/serve.js +157 -28
  91. package/dist/server/chat.js +10 -3
  92. package/dist/server/chrome.d.ts +10 -2
  93. package/dist/server/chrome.js +7 -5
  94. package/dist/server/console.css +10 -0
  95. package/dist/server/guards.d.ts +3 -0
  96. package/dist/server/guards.js +22 -2
  97. package/dist/server/http.d.ts +22 -0
  98. package/dist/server/http.js +58 -0
  99. package/dist/server/live.js +4 -14
  100. package/dist/server/mutation-key.d.ts +70 -0
  101. package/dist/server/mutation-key.js +142 -0
  102. package/dist/server/pages.js +12 -9
  103. package/dist/server/people-tokens.js +240 -20
  104. package/dist/server/remote.d.ts +2 -0
  105. package/dist/server/remote.js +19 -4
  106. package/dist/server/render-new-task.js +5 -1
  107. package/dist/server/render-pages.d.ts +2 -1
  108. package/dist/server/render-pages.js +1 -1
  109. package/dist/server/render-people.d.ts +36 -0
  110. package/dist/server/render-people.js +65 -1
  111. package/dist/server/render-projects.js +4 -2
  112. package/dist/server/render-tasks.d.ts +12 -0
  113. package/dist/server/render-tasks.js +16 -6
  114. package/dist/server/request-context.d.ts +6 -0
  115. package/dist/server/route-table.d.ts +16 -0
  116. package/dist/server/route-table.js +121 -0
  117. package/dist/server/runtime.d.ts +6 -4
  118. package/dist/server/session.d.ts +31 -1
  119. package/dist/server/session.js +12 -2
  120. package/dist/server/settings.js +63 -8
  121. package/dist/server/task-views.js +10 -1
  122. package/dist/server/tasks.d.ts +10 -0
  123. package/dist/server/tasks.js +59 -9
  124. package/dist/server/terminals.js +15 -2
  125. package/dist/server/welcome.d.ts +22 -16
  126. package/dist/server/welcome.js +150 -52
  127. package/dist/settings-groups-ui.d.ts +7 -1
  128. package/dist/settings-groups-ui.js +23 -2
  129. package/dist/settings-proofs.js +1 -0
  130. package/dist/setup-guide.d.ts +2 -2
  131. package/dist/setup-guide.js +6 -0
  132. package/dist/store.d.ts +124 -1
  133. package/dist/store.js +245 -12
  134. package/dist/surface.d.ts +10 -0
  135. package/dist/surface.js +40 -1
  136. package/dist/task-text.d.ts +6 -0
  137. package/dist/task-text.js +17 -0
  138. package/dist/teardown-git.js +16 -1
  139. package/dist/telegram-lead.js +2 -0
  140. package/dist/terminal-text.d.ts +11 -0
  141. package/dist/terminal-text.js +26 -0
  142. package/dist/toolroll-update.d.ts +8 -1
  143. package/dist/toolroll-update.js +151 -4
  144. package/dist/tui-contract.d.ts +118 -0
  145. package/dist/tui-contract.js +9 -0
  146. package/dist/tui-source.d.ts +14 -0
  147. package/dist/tui-source.js +182 -0
  148. package/dist/tui.js +129 -0
  149. package/dist/up-processes.d.ts +13 -1
  150. package/dist/up-processes.js +13 -3
  151. package/dist/welcome-facts.d.ts +18 -0
  152. package/dist/welcome-facts.js +79 -0
  153. package/dist/youiyoux-intake.js +11 -1
  154. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.90 — 2026-11-04
4
+
5
+ - **Type `toolroll` and you're in.** At your own terminal, bare `toolroll`
6
+ starts Toolroll on this computer (or opens the one already running) and
7
+ shows your tasks, what each agent is doing and what needs you, right in the
8
+ terminal: file a task, read the full plan and approve it, stop, revise,
9
+ complete, answer a question, or press `w` for the browser. `toolroll --web`
10
+ opens the browser instead, `toolroll open` gets you back in signed in, and
11
+ `toolroll welcome` walks the three setup steps. Scripts, agents and
12
+ `--json` see exactly what they did before.
13
+ - **No password on your own computer.** A new install started with
14
+ `toolroll` has one owner and no password or login file. You can file
15
+ tasks, chat with the lead, approve plans for work you start, answer
16
+ questions, stop, revise and complete work, and add projects. Hands-off
17
+ modes, merging, releases, people and access, and most settings ask you to
18
+ set a password first: the page says **Set a password**, or run
19
+ `toolroll password set`. Agents always run inside the fence while this is
20
+ on. Existing installs keep their passwords and work as before.
21
+ - **A first result in three steps.** The welcome page (and
22
+ `toolroll welcome`) asks for your agent (Claude Code or Codex, with the
23
+ command that signs each in), a project (a folder, one of your GitHub
24
+ repositories, or a small sample project) and a first task from a few
25
+ starters.
26
+ - **One-line install opens Toolroll.** `curl … | sh` now opens Toolroll in
27
+ your terminal when it finishes. Without a terminal it prints the exact
28
+ command to start it.
29
+ - **Keep it running, or stop it.** Quitting asks once whether Toolroll
30
+ should keep running in the background. `toolroll background on|off|show`
31
+ changes that later (a login item on macOS, a user service on Linux), and
32
+ `toolroll stop` stops it with running work saved.
33
+ - **Browser changes need this browser's key.** On an install that only
34
+ listens on this computer, a signed-in cookie alone can no longer change
35
+ anything: a page on another port can read but not act. After Toolroll
36
+ restarts, an open tab says **Reconnect**: run `toolroll open`, or type
37
+ your password. The first start of this version asks open tabs to reconnect
38
+ once.
39
+ - **Port 4180 already taken?** A new install picks the next free port and
40
+ keeps it, instead of failing to start.
41
+ - **Your saved login stays out of every build.** Builds could never read your
42
+ saved login, but one saved after a build had already started was missed.
43
+ Saved logins now live in a private folder next to the database that every
44
+ build is denied as a whole, on macOS, Linux and with Codex. The next start
45
+ moves an existing saved login there for you.
46
+
47
+ ## 0.9.89 — 2026-11-03
48
+
49
+ - **The app no longer freezes when a drive stops answering.** If an external
50
+ or network drive holding one of your projects stalls, every other page keeps
51
+ working: that project's pages say its folder is unavailable instead of
52
+ hanging the whole app. Filing a task into a project whose folder isn't
53
+ answering is refused with a plain explanation, so nothing waits forever.
54
+ - **A flow's main button no longer changes under your tap.** When a card starts
55
+ waiting on you, the flow page's main button turns from New card into Review.
56
+ A tap at that moment now does what the button showed when you pressed it,
57
+ and the switch happens a moment later with a short note ("Review is next.").
58
+ - **Every Settings save goes to the right place.** Each Settings form, on
59
+ every page, is now checked to post where it should, so a save can't quietly
60
+ go to the wrong address again.
61
+
62
+ ### For developers
63
+
64
+ - **Every walkthrough runs on every pull request.** All the scripted
65
+ end-to-end journeys (task, stop, mail and onboarding included) now run on
66
+ Linux in pull request CI, not only in the Mac release check.
67
+
3
68
  ## 0.9.88 — 2026-11-03
4
69
 
5
70
  - **Every text box looks the same, and calmer.** Wherever you type or pick
package/README.md CHANGED
@@ -29,24 +29,30 @@ Approve the exact scope once. Get results back Ready with the diff, the passing
29
29
 
30
30
  ## Quick start
31
31
 
32
- macOS or Linux, Node.js 22.13+, git 2.41+, and a signed-in `claude`, `codex` or
33
- `gemini` CLI.
32
+ macOS or Linux, Node.js 22.13+, git 2.41+, and Claude Code (`claude`) or
33
+ Codex (`codex`) signed in on this computer.
34
34
 
35
35
  ```sh
36
- npm install -g toolroll # or: brew install ap9000/toolroll/toolroll
37
- toolroll demo # optional: a seeded sandbox, no model calls, no spend
38
- toolroll up # the app and a builder; opens http://127.0.0.1:4180
36
+ curl -fsSL https://raw.githubusercontent.com/ap9000/toolroll/main/install.sh | sh
39
37
  ```
40
38
 
41
- `up` saves your password in `up-login.txt` and prints the command that shows
42
- it (never the password itself), then keeps running. Then, in a repository:
39
+ or
43
40
 
44
41
  ```sh
45
- toolroll onboard --yes # add it as a project, install the agent skill
42
+ npm install -g toolroll # or: brew install ap9000/toolroll/toolroll
43
+ toolroll
46
44
  ```
47
45
 
48
- Open **Chat**, say what you want, approve the plan. The result comes back
49
- **Ready**. More: [Getting started](docs/guide/getting-started.md).
46
+ Toolroll opens in your terminal and runs while that window is open. No
47
+ password and no login file: on your own computer you're already in. Pick an
48
+ agent, a project (or the sample one) and a first task; approve the plan, and
49
+ the result comes back **Ready**. Press `w` for the browser, or run
50
+ `toolroll --web`. To look around first, `toolroll demo` opens a seeded
51
+ sandbox with no model calls and no spend.
52
+
53
+ **Letting agents work on their own.** Approving work you start on this
54
+ computer needs no password. Hands-off modes, merging, releasing and most
55
+ settings ask you to set one first: `toolroll password set`. More: [Getting started](docs/guide/getting-started.md).
50
56
 
51
57
  ## How it works
52
58
 
@@ -431,6 +431,43 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
431
431
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
432
432
  SOFTWARE.
433
433
 
434
+ ## Terminal UI bundle (`dist/tui.js`)
435
+
436
+ The terminal UI is one esbuild bundle of Ink 6.8.0 (MIT, Vadim Demedes), React 19 and react-reconciler (MIT, Meta),
437
+ yoga-layout 3.2.1 (MIT, Meta; its WebAssembly ships inlined as base64 in the bundle) and their small MIT/ISC helper
438
+ packages (chalk, ansi-styles, wrap-ansi, string-width, emoji-regex, signal-exit and others). They are development
439
+ dependencies only: nothing extra is installed with Toolroll. The build appends each bundled package's own license file
440
+ to `dist/browser/THIRD_PARTY_NOTICES.txt`, next to the browser bundle's, and fails if one is missing.
441
+
442
+ ## yoga-layout 3.2.1 — MIT
443
+
444
+ This Ink dependency declares MIT in its package metadata but omits its license file from the published npm package.
445
+ The upstream license was preserved from the repository at tag `v3.2.1`, revision
446
+ `042f5013152eb81c1552dec945b88f7b95ca350f`, inspected 3 November 2026:
447
+ https://raw.githubusercontent.com/facebook/yoga/042f5013152eb81c1552dec945b88f7b95ca350f/LICENSE
448
+
449
+ MIT License
450
+
451
+ Copyright (c) Facebook, Inc. and its affiliates.
452
+
453
+ Permission is hereby granted, free of charge, to any person obtaining a copy
454
+ of this software and associated documentation files (the "Software"), to deal
455
+ in the Software without restriction, including without limitation the rights
456
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
457
+ copies of the Software, and to permit persons to whom the Software is
458
+ furnished to do so, subject to the following conditions:
459
+
460
+ The above copyright notice and this permission notice shall be included in all
461
+ copies or substantial portions of the Software.
462
+
463
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
464
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
465
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
466
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
467
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
468
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
469
+ SOFTWARE.
470
+
434
471
  ## Geist Sans and Geist Mono (@fontsource/geist-sans, @fontsource/geist-mono 5.3.0) — SIL OFL 1.1
435
472
 
436
473
  The console embeds the latin woff2 subsets (weights 400, 500 and 600) in
@@ -218,3 +218,12 @@ export declare function watchedPing(store: Store, row: {
218
218
  }, account: string, now: Date): {
219
219
  hold: string;
220
220
  } | "skip" | "send";
221
+ /** A page's own watch id: 24 hex characters the shell makes (never a database row id). */
222
+ export declare const AGENT_VIEW_ID: RegExp;
223
+ /**
224
+ * POST /agents/watch, hold: this person's page watches a run (renewed on each keep-alive). A view id another person
225
+ * already holds is never taken over or renewed: only the holder's own row moves. False when nothing was held.
226
+ */
227
+ export declare function holdOwnAgentView(store: Store, view: string, account: string, run: number, at: Date): boolean;
228
+ /** POST /agents/watch, release: this person's page went away. Only their own view is released (and lapsed ones swept). */
229
+ export declare function releaseOwnAgentView(store: Store, view: string, account: string, at: Date): void;
@@ -472,3 +472,23 @@ export function watchedPing(store, row, account, now) {
472
472
  const seen = store.handle.prepare("SELECT 1 AS hit FROM agent_seen WHERE account = ? AND run = ?").get(account, boardRunOf(store, row.run));
473
473
  return seen === undefined ? "send" : "skip";
474
474
  }
475
+ // ---- easy-start C1 (N1): the watch lease and the seen mark, from keyed POSTs only -------------------------------------
476
+ /** A page's own watch id: 24 hex characters the shell makes (never a database row id). */
477
+ export const AGENT_VIEW_ID = /^[0-9a-f]{24}$/;
478
+ /**
479
+ * POST /agents/watch, hold: this person's page watches a run (renewed on each keep-alive). A view id another person
480
+ * already holds is never taken over or renewed: only the holder's own row moves. False when nothing was held.
481
+ */
482
+ export function holdOwnAgentView(store, view, account, run, at) {
483
+ if (!AGENT_VIEW_ID.test(view))
484
+ return false;
485
+ return Number(store.handle.prepare(`INSERT INTO agent_view (view, account, run, expires_at) VALUES (?, ?, ?, ?)
486
+ ON CONFLICT (view) DO UPDATE SET expires_at = excluded.expires_at WHERE agent_view.account = excluded.account AND agent_view.run = excluded.run`)
487
+ .run(view, account, boardRunOf(store, run), new Date(at.getTime() + VIEW_LEASE_MS).toISOString()).changes) > 0;
488
+ }
489
+ /** POST /agents/watch, release: this person's page went away. Only their own view is released (and lapsed ones swept). */
490
+ export function releaseOwnAgentView(store, view, account, at) {
491
+ if (!AGENT_VIEW_ID.test(view))
492
+ return;
493
+ store.handle.prepare("DELETE FROM agent_view WHERE (view = ? AND account = ?) OR expires_at < ?").run(view, account, at.toISOString());
494
+ }
@@ -37,6 +37,7 @@ import { ensureRunnerProcessFolder, RUNNER_PROCESS_DIR } from "./runner-process.
37
37
  import { DEPS_FOLDER, linkedKey, readyCopy, sharedCopies } from "./shared-deps.js";
38
38
  import { SEATBELT_PROBE, sandboxRuntimeProblem, srtSpawn } from "./sandbox-runtime.js";
39
39
  import { protectedStorageDenies, protectedStorageNames } from "./protected-storage.js";
40
+ import { ensureSecretsDir, legacyLoginFileOf, SECRETS_DIR, secretsDirOf } from "./login-secrets.js";
40
41
  import { getDefaultWritePaths } from "@anthropic-ai/sandbox-runtime";
41
42
  /** Names that are secrets wherever the database lives (used when its folder is shared with other things). */
42
43
  const SENSITIVE_NAME = /(^up-login\.txt$|token|secret|login|password|credential|vapid|keys?\b|\.pem$|\.key$|^backups$|^evidence$|^remote$|^sign-in\.json$|^monitoring\.json$)/i;
@@ -183,6 +184,23 @@ export function agentFence(options) {
183
184
  catch { /* name and target remain denied */ }
184
185
  fence.add(backups);
185
186
  fence.add(real(backups));
187
+ // Saved credentials (the remembered login, login-secrets.ts) live in a reserved owner-only folder, made here before
188
+ // any launch and denied whole, so one first written after this launch is behind this fence too: a listing can't
189
+ // name a file that isn't there yet, and bubblewrap can't deny one. If it can't be made or proven, its name is
190
+ // denied anyway (and nothing is saved there).
191
+ const secrets = secretsDirOf(state);
192
+ try {
193
+ ensureSecretsDir(state);
194
+ }
195
+ catch { /* name and target remain denied */ }
196
+ fence.add(secrets);
197
+ fence.add(real(secrets));
198
+ // An older version's login beside the database: listed below when it is there; by name, there or not, where the
199
+ // sandbox denies a missing path without making it (Seatbelt; Codex's profile gets the same list). On Linux a
200
+ // missing path would be stubbed into being for the launch, and nothing writes that name any more.
201
+ const legacyLogin = legacyLoginFileOf(state);
202
+ if (process.platform === "darwin" || existsSync(legacyLogin))
203
+ fence.add(legacyLogin);
186
204
  // Only a folder that is Toolroll's own (~/.config/toolroll or an older name,
187
205
  // the desktop app's "Toolroll") is fenced whole: a database kept
188
206
  // in a shared folder (a projects folder, a test's temp folder) must not
@@ -230,7 +248,7 @@ export function agentFence(options) {
230
248
  // Secret, state, database, explicit-extra and home-config denies are never exemptible.
231
249
  for (const path of options.boundary?.protectedShell ?? [])
232
250
  fence.add(real(path));
233
- const protectedPaths = new Set([...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups"].includes(basename(path)) ? [path, real(path)] : [real(path)]));
251
+ const protectedPaths = new Set([...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups", SECRETS_DIR].includes(basename(path)) ? [path, real(path)] : [real(path)]));
234
252
  // Protected storage (chat apps, browser profiles, password managers): every launch, every policy, never exemptible,
235
253
  // kept by the name it was resolved under as well as where it leads.
236
254
  const storage = options.protectedStorage ?? protectedStorageDenies(home);
@@ -242,7 +260,7 @@ export function agentFence(options) {
242
260
  fence.add(path);
243
261
  const binding = options.binding;
244
262
  const needed = binding === undefined || binding === null ? [] : [binding.gitdir, real(join(binding.main, ".git"))];
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 => {
263
+ return [...new Set([...[...fence].flatMap(path => path === runFolder || [RUNNER_PROCESS_DIR, "backups", SECRETS_DIR].includes(basename(path)) ? [path, real(path)] : [real(path)]), ...storage.flatMap(path => [path, real(path)])])].filter(path => {
246
264
  if (protectedPaths.has(path) || !needed.some(target => target === path || inside(target, path)))
247
265
  return true;
248
266
  console.warn(`Dropped build fence entry covering this checkout's own Git data: ${path}`);
@@ -2,6 +2,13 @@
2
2
  * expression, used exactly three times: the mode and grant clauses, and the release's expiry). 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 const authorityStandsSql: (scope: string, repo: string, now: string) => string;
5
+ /**
6
+ * SQL, conjunctive with every alternative above (easy-start plan C5): a seal a local owner gave (its proof row at the
7
+ * seal's instant says `local-owner`) stands only while local mode is on, for that owner, at the local generation its
8
+ * proof names (approvalProofDetail's " · local <generation> <transport> · "), and while its project's checks aren't
9
+ * stored as unsandboxed. Turning local mode off, or moving its generation, stops every such seal dispatching.
10
+ */
11
+ export declare const localSealStandsSql: (scope: string, repo: string) => string;
5
12
  /** SQL: a `carried` seal stands only on its own carry row — this task, this approved digest, this candidate, and the
6
13
  * reference `<authority>:<carry>` the carry transaction wrote (release-once.md). */
7
14
  export declare const carrySealSql: (scope: string) => string;
@@ -16,7 +16,7 @@ import { grantStandsSql } from "./flow-unattended.js";
16
16
  import { accountReachesSql } from "./project-access.js";
17
17
  import { autoSealSql } from "./release-authority.js";
18
18
  /** The proofs a person gives (src/person-proof.ts), as an SQL list. */
19
- const PERSON_PROOFS = "('password-now', 'sso-now', 'fresh-password', 'fresh-sso', 'session', 'cli-login', 'chat', 'passkey')";
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
21
  * expression, used exactly three times: the mode and grant clauses, and the release's expiry). 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. */
@@ -30,7 +30,18 @@ export const authorityStandsSql = (scope, repo, now) => `COALESCE((((COALESCE(${
30
30
  OR (${scope}.approval_basis = 'grant' AND ${grantStandsSql(`${scope}.mode_digest`, now, repo)})
31
31
  OR (${scope}.approval_basis = 'schedule' AND ${approverStandsSql(scope, repo)})
32
32
  OR (${scope}.approval_basis IN ('auto-check', 'auto-green') AND ${autoSealSql(scope, repo)} AND ${approverStandsSql(scope, repo)}))
33
- AND ${releaseStandsSql(scope, now)}), 0)`;
33
+ AND ${releaseStandsSql(scope, now)} AND ${localSealStandsSql(scope, repo)}), 0)`;
34
+ /**
35
+ * SQL, conjunctive with every alternative above (easy-start plan C5): a seal a local owner gave (its proof row at the
36
+ * seal's instant says `local-owner`) stands only while local mode is on, for that owner, at the local generation its
37
+ * proof names (approvalProofDetail's " · local <generation> <transport> · "), and while its project's checks aren't
38
+ * stored as unsandboxed. Turning local mode off, or moving its generation, stops every such seal dispatching.
39
+ */
40
+ export const localSealStandsSql = (scope, repo) => `NOT EXISTS (SELECT 1 FROM action_ledger lp WHERE lp.task_id = ${scope}.task_id AND lp.action = 'approval proved'
41
+ AND lp.actor = ${scope}.approved_by AND lp.at = ${scope}.approved_at AND lp.outcome = 'local-owner'
42
+ AND NOT (EXISTS (SELECT 1 FROM local_mode lm WHERE lm.id = 1 AND lm.enabled = 1 AND lm.owner = lp.actor
43
+ AND lp.detail LIKE '% · local ' || lm.generation || ' %')
44
+ AND NOT EXISTS (SELECT 1 FROM project_check_access lca WHERE lca.repo = ${repo} AND lca.level = 'unsandboxed')))`;
34
45
  /** SQL: a `carried` seal stands only on its own carry row — this task, this approved digest, this candidate, and the
35
46
  * reference `<authority>:<carry>` the carry transaction wrote (release-once.md). */
36
47
  export const carrySealSql = (scope) => `EXISTS (SELECT 1 FROM release_carry rc2 JOIN release_authority ra2 ON ra2.id = rc2.authority