toolroll 0.7.0 → 0.8.1

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/dist/serve.js CHANGED
@@ -125,7 +125,6 @@ import { openRouterModelsCache, openRouterPickerScript } from "./openrouter-mode
125
125
  import { checkModels, isNewModel, livePin, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "./model-catalog.js";
126
126
  import { MODELS_CSS, modelsHtml, modelsScript } from "./models-ui.js";
127
127
  import { ASSISTANTS, modelChoices, detectPreparation, previewProjectInstructions, addProjectInstructions } from "./setup-guide.js";
128
- import { projectSkillInstalled } from "./skills.js";
129
128
  import { previewSetup, approveSetup } from "./control-setup.js";
130
129
  import { controlSetupHtml, setupPreviewHtml, connectionHtml, connectionWords, hiddenFields } from "./control-ui.js";
131
130
  import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./task-composer.js";
@@ -171,6 +170,7 @@ import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, i
171
170
  import { verifiedAuthor, LEAD_THREAD } from "./store.js";
172
171
  import { updateRepos, addRepos, removeRepos } from "./repos.js";
173
172
  import { run as execRun } from "./exec.js";
173
+ import { findFirstTasks, firstResultWords, firstRunSteps, firstTaskSuggestions, SANDBOX_COMMAND } from "./first-run.js";
174
174
  /** A user agent, reduced to safe display words — never echoed raw. */
175
175
  function oneLineUa(raw) {
176
176
  const text = Array.isArray(raw) ? (raw[0] ?? "") : (raw ?? "");
@@ -206,6 +206,9 @@ import { BACKUP_EVERY_HOURS, MAX_KEEP, backupFolderOf, backupNow, checkBackupFol
206
206
  import { prometheusMetrics } from "./metrics.js";
207
207
  import { SPEND_CSS, spendCsv, spendHtml } from "./spend-ui.js";
208
208
  import { RETENTION_CSS, retentionHtml } from "./retention-ui.js";
209
+ import { UPDATES_CSS, newerThan, updateStepsHtml, updatesHtml, updatesScript } from "./toolroll-update-ui.js";
210
+ import { abandonRuntimeUpdate, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate, requestRuntimeUpdateCancel, runningWorkWords, runtimeUpdateStatus, runtimeUpdateTerminal, currentRuntime } from "./toolroll-update.js";
211
+ import { latestVersionNow } from "./releases.js";
209
212
  import { RETENTION_KINDS, lastSweepAt, parsePeriod, retentionPlan } from "./retention.js";
210
213
  import { EXPORT_CSS, dataExportHtml } from "./export-ui.js";
211
214
  import { buildExport, exportZip } from "./export.js";
@@ -1880,7 +1883,7 @@ export function createDecisionServer(options) {
1880
1883
  })),
1881
1884
  cancelledBlockers: cancelled,
1882
1885
  gaps: project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0).slice(0, 10),
1883
- wizard: wizardSteps(now),
1886
+ wizard: firstRunStepsNow(now),
1884
1887
  worker: (() => {
1885
1888
  // The one fact the inbox must never hide (install review): with
1886
1889
  // no worker answering, nothing here will ever build, and every
@@ -3373,10 +3376,13 @@ export function createDecisionServer(options) {
3373
3376
  // A failed briefing query must not make the conversation vanish.
3374
3377
  }
3375
3378
  }
3379
+ // The first run belongs to the lead conversation, for someone who can act on it.
3380
+ const firstRun = focusTask === null && who.role === "approver" ? chatFirstRun(now, chatProject) : undefined;
3381
+ const withFirstRun = (shown) => firstRun === undefined ? shown : { ...shown, workspace: { ...shown.workspace, firstRun } };
3376
3382
  if (enabled.ok && mateSession !== null && principal !== null && !ceilingStale) {
3377
3383
  {
3378
3384
  const said = takeMateNote(who.session.csrf, mateSession.id);
3379
- return sendScreen(response, 200, matePage(chromeFor(null, "chat", undefined, "all"), {
3385
+ return sendScreen(response, 200, withFirstRun(matePage(chromeFor(null, "chat", undefined, "all"), {
3380
3386
  session: mateSession,
3381
3387
  ...mateConversationRows(who, principal, focusTask, now, chatProject),
3382
3388
  chatProject,
@@ -3394,10 +3400,10 @@ export function createDecisionServer(options) {
3394
3400
  now,
3395
3401
  resultPanel,
3396
3402
  resultRunId: resultRun?.id ?? null,
3397
- }));
3403
+ })));
3398
3404
  }
3399
3405
  }
3400
- return sendScreen(response, 200, chatPage(chromeFor(null, "chat", undefined, "all"), {
3406
+ return sendScreen(response, 200, withFirstRun(chatPage(chromeFor(null, "chat", undefined, "all"), {
3401
3407
  settingsOpen: settingsView,
3402
3408
  enabled,
3403
3409
  pending,
@@ -3430,7 +3436,7 @@ export function createDecisionServer(options) {
3430
3436
  resultPanel,
3431
3437
  ...(enabled.ok && who.role === "approver" ? { mateMint: mateMintCard(who.session.csrf, enabled, focusTask === null ? "/chat" : taskChatHref(focusTask.id)) } : {}),
3432
3438
  ...(who.role === "approver" ? { coordinatorProposals: coordinatorProposalsSection(coordinatorRows, decisionsFor(store, coordinatorRows), who.session.csrf, now, true, focusTask === null ? null : taskChatHref(focusTask.id)) } : {}),
3433
- }));
3439
+ })));
3434
3440
  }
3435
3441
  const chatAck = /^\/chat\/ack\/([0-9]{1,15})$/.exec(url.pathname);
3436
3442
  if (chatAck !== null) {
@@ -3617,6 +3623,21 @@ export function createDecisionServer(options) {
3617
3623
  const view = { settings: readMonitoring(options.configDir), status: store.monitoringStatus(), head: store.ledgerHeadId(), origin: consoleOrigin(request.headers.host) };
3618
3624
  return sendScreen(response, 200, screen("Monitoring", `<p><a href="/settings">Settings</a></p><h1>Monitoring</h1>${monitoringHtml(view, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
3619
3625
  }
3626
+ // Settings → Updates: the installed version, three ways to update, the update's steps live, then What's new. An instance operator's page.
3627
+ if (url.pathname === "/settings/updates") {
3628
+ const databaseFile = store.databaseFile();
3629
+ if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
3630
+ return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
3631
+ const status = runtimeUpdateStatus(dirname(databaseFile));
3632
+ if (url.searchParams.get("fragment") === "steps")
3633
+ return respond(response, 200, "text/html; charset=utf-8", status.journal ? updateStepsHtml(status.journal, status.running) : `<div id="update-live" data-done="1"></div>`);
3634
+ const active = status.journal !== null && !runtimeUpdateTerminal(status.journal.phase);
3635
+ const current = options.updates?.current ?? PACKAGE_VERSION;
3636
+ // While update checks are off the page asks npm nothing until Check now.
3637
+ const checksOff = updateChecksOff(process.env, options.configDir ?? dirname(databaseFile)).off && url.searchParams.get("check") !== "now";
3638
+ const view = { current, latest: active ? { version: current } : checksOff ? { off: true } : await latestReleaseFor(options.updates?.latest), method: options.updates?.method ?? installMethod(), journal: status.journal, running: status.running, whatsNew: status.whatsNew, csrf: who.session.csrf };
3639
+ return sendScreen(response, 200, screen("Updates", `<p><a href="/settings">Settings</a></p><h1>Updates</h1>${updatesHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings"), ...(active ? { functional: { script: updatesScript(), fetches: true } } : {}) }));
3640
+ }
3620
3641
  // v105: Settings → Retention, how long each kind of data is kept. An instance operator's page.
3621
3642
  if (url.pathname === "/settings/retention") {
3622
3643
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
@@ -3794,7 +3815,11 @@ export function createDecisionServer(options) {
3794
3815
  const googleView = { connected: googleConnected(options.configDir ?? null)?.address ?? null, clientId: google?.clientId ?? "", redirect: origin === null ? null : `${origin}${GOOGLE_CALLBACK}` };
3795
3816
  return email === null ? { set: false, host: "", port: 587, secure: false, user: "", from: "", imapHost: "", imapPort: 993, google: googleView }
3796
3817
  : { set: true, host: email.host, port: email.port, secure: email.secure, user: email.user, from: email.from, imapHost: email.imap?.host ?? "", imapPort: email.imap?.port ?? 993, google: googleView };
3797
- })() : null, telegramDeliveryWords(store, loadBotToken(process.env, options.telegramTokenFile)), settingsWorkers(store, now), settingsUpdates(who.name, csrf)));
3818
+ })() : null, telegramDeliveryWords(store, loadBotToken(process.env, options.telegramTokenFile)), settingsWorkers(store, now), settingsUpdates(who.name, csrf), (() => {
3819
+ // The installation fact: when the first Ready result arrived, measured from the first account.
3820
+ const at = store.firstSuccessAt(now), since = store.installationStartedAt();
3821
+ return at === null || since === null ? null : firstResultWords(since, at);
3822
+ })()));
3798
3823
  }
3799
3824
  if (url.pathname === "/push/key") {
3800
3825
  // Session-gated: the key is not secret, but strangers get nothing.
@@ -3834,62 +3859,72 @@ export function createDecisionServer(options) {
3834
3859
  return refuse(response, who, 404, "There's no page at this address.", "/chat");
3835
3860
  }
3836
3861
  /**
3837
- * The first-run checklist (adoption track, step 3) — derived from live
3838
- * state on every render, never a stored cursor, and retired PERMANENTLY
3839
- * by the first-success installation fact (Codex adoption review,
3840
- * finding 14). Every step is either something this console already has
3841
- * authority for, or the exact command where the CLI owns the act — the
3842
- * checklist instructs, it never gains authority (finding e).
3862
+ * The first run (adoption track, step 3): three plain steps derived from
3863
+ * live state on every render, never a stored cursor, and retired
3864
+ * PERMANENTLY by the first-success installation fact (Codex adoption
3865
+ * review, finding 14). Each step is either something this console can do,
3866
+ * or the exact command where only the CLI can (finding e).
3843
3867
  */
3844
- function wizardSteps(now) {
3845
- if (restricted() || store.firstSuccessAt(now) !== null)
3868
+ function firstRunStepsNow(now) {
3869
+ if (restricted() || store.isDemo() || store.firstSuccessAt(now) !== null)
3846
3870
  return null;
3847
- const repos = admissionList() ?? [];
3848
- const setupDone = repos.filter(one => store.liveWorktreeSetup(one) !== null).length;
3849
- const skillDone = repos.filter(projectSkillInstalled).length;
3850
- const counted = (done) => repos.length <= 1 ? "" : ` (${done} of ${repos.length} repos)`;
3851
- return [
3852
- {
3853
- done: !unscopedMode,
3854
- title: "name what this console may see",
3855
- detail: unscopedMode
3856
- ? `no ceiling is configured — this server currently shows everything. Restart it naming the repos: <code>toolroll serve --repo &lt;path&gt; --port …</code>`
3857
- : repos.length === 0
3858
- ? `the ceiling is configured but empty — no repository is visible here`
3859
- : `ceiling: ${repos.map(one => escape(projectName(one))).join(", ")}`,
3860
- },
3861
- {
3862
- // A fact about THIS DATABASE only: binaries and authentication live
3863
- // on the worker's machine, which may not be this one — the console
3864
- // never claims to have checked them (finding 15).
3865
- done: store.hasPhaseConfig(),
3866
- title: "route the spend to a provider",
3867
- detail: store.hasPhaseConfig()
3868
- ? `spend routing is configured in this database. Binary and authentication facts stay machine-side: run <code>toolroll providers</code> where the workers run`
3869
- : `nothing routes builds to a provider yet: <code>toolroll config set build --provider claude --as &lt;you&gt; --token &lt;t&gt;</code> — then check <code>toolroll providers</code> on the worker's machine (installed, configured, historically-successful, and authenticated are four separate facts there)`,
3870
- },
3871
- {
3872
- done: repos.length > 0 && setupDone === repos.length,
3873
- title: "say how a fresh checkout gets ready",
3874
- detail: setupDone > 0
3875
- ? `setup command set${counted(setupDone)}`
3876
- : `agents build in throwaway workspaces; give them the preparation step: <code>toolroll setup set --repo &lt;path&gt; --command "npm ci"</code>`,
3877
- },
3878
- {
3879
- done: repos.length > 0 && skillDone === repos.length,
3880
- title: "teach the repo's agents this queue exists",
3881
- detail: skillDone > 0
3882
- ? `skill installed${counted(skillDone)}`
3883
- : `<code>toolroll skills install --repo &lt;path&gt;</code> previews; add <code>--yes</code> to write the skill file`,
3884
- },
3885
- {
3886
- done: store.hasAnyWork(),
3887
- title: "file the first standing order",
3888
- detail: store.hasAnyWork()
3889
- ? `work is filed — approve its scope and the machine takes it from there`
3890
- : `start from a template below, capture a one-off task underneath, or browse <a href="/routines">routines</a>`,
3891
- },
3892
- ];
3871
+ return firstRunSteps({ agentSignedIn: agentSignedIn(now), projects: managedRepos().length, hasTask: store.hasAnyWork(), firstResultAt: null });
3872
+ }
3873
+ /**
3874
+ * Whether a coding agent is signed in where the work runs. This machine's
3875
+ * own checks (the ones Settings makes) speak only for a worker on this
3876
+ * machine; a worker elsewhere speaks through its own readiness report, and
3877
+ * the console never claims to have checked that machine (finding 15).
3878
+ * Nothing here waits on a probe: this machine's answer is the last one
3879
+ * seen, refreshed in the background.
3880
+ */
3881
+ function agentSignedIn(now) {
3882
+ const here = hostname();
3883
+ const workers = store.listRunners().filter(one => one.retiredAt === null && runnerAlive(one, now));
3884
+ if ((options.localRunner !== undefined || workers.some(one => one.host === here)) && localAgentSignedIn())
3885
+ return true;
3886
+ return workers.some(one => one.host !== here && store.providerReadiness(one.name).some(seen => seen.state === "ready"));
3887
+ }
3888
+ let localSignIn = null;
3889
+ let localSignInChecking = false;
3890
+ function localAgentSignedIn() {
3891
+ if (!localSignInChecking && (localSignIn === null || Date.now() - localSignIn.at > 30_000)) {
3892
+ localSignInChecking = true;
3893
+ void Promise.all(["claude", "codex"].map(one => connectionCheck(one).then(value => value.state, () => "unverified")))
3894
+ .then(states => { localSignIn = { at: Date.now(), signedIn: states.some(state => state === "connected" || state === "key-works" || state === "key-present") }; })
3895
+ .catch(() => { })
3896
+ .finally(() => { localSignInChecking = false; });
3897
+ }
3898
+ return localSignIn?.signedIn ?? false;
3899
+ }
3900
+ /** First tasks per project: the last ones found (safe generic ones until then), re-read in the background at most
3901
+ * every ten minutes. A render never waits on `gh` or `git grep`. */
3902
+ const firstTasks = new Map();
3903
+ function firstTasksFor(repo) {
3904
+ const at = Date.now();
3905
+ let entry = firstTasks.get(repo);
3906
+ if (entry === undefined || (entry.until <= at && !entry.reading)) {
3907
+ const reading = { until: at + 10 * 60_000, found: entry?.found ?? null, reading: true };
3908
+ firstTasks.set(repo, reading);
3909
+ entry = reading;
3910
+ void findFirstTasks(repo, options.firstTaskRunner ?? execRun)
3911
+ .then(found => { reading.found = found; }, () => { })
3912
+ .finally(() => { reading.reading = false; });
3913
+ }
3914
+ return entry.found ?? firstTaskSuggestions({ issues: [], todos: [] });
3915
+ }
3916
+ /** Chat's first run: the steps, first tasks until the first task exists, and the sandbox while no agent is signed in. */
3917
+ function chatFirstRun(now, project) {
3918
+ const steps = firstRunStepsNow(now);
3919
+ if (steps === null)
3920
+ return undefined;
3921
+ const taskFiled = steps.find(one => one.key === "task")?.done ?? true;
3922
+ const repo = project ?? managedRepos()[0] ?? null;
3923
+ return {
3924
+ steps,
3925
+ suggestions: taskFiled || repo === null ? [] : firstTasksFor(repo),
3926
+ sandbox: steps.find(one => one.key === "agent")?.done === false ? SANDBOX_COMMAND : null,
3927
+ };
3893
3928
  }
3894
3929
  // ---- fleet chat (v13) ----------------------------------------------------
3895
3930
  const chatFetcher = options.chatFetcher ?? fetch;
@@ -4473,6 +4508,7 @@ export function createDecisionServer(options) {
4473
4508
  ...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
4474
4509
  ...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
4475
4510
  ...(s.chrome.update === undefined ? {} : { update: s.chrome.update }),
4511
+ ...(extras.firstRun === undefined ? {} : { firstRun: extras.firstRun }),
4476
4512
  };
4477
4513
  if (requestFacts.workspaceRead) {
4478
4514
  const validator = requestFacts.workspaceValidator;
@@ -5808,8 +5844,8 @@ export function createDecisionServer(options) {
5808
5844
  }
5809
5845
  return redirect(response, safeReturn(body.get("return") ?? "/settings#updates"));
5810
5846
  }
5811
- // The daily check's switch: an installation setting, so an operator's.
5812
- if (url.pathname === "/settings/updates") {
5847
+ // The daily check's switch: an installation setting, so an operator's. (Its own address: POST /settings/updates starts an update.)
5848
+ if (url.pathname === "/settings/updates/checks") {
5813
5849
  if (options.configDir === undefined)
5814
5850
  return refuse(response, who, 404, "Updates are not set up on this console.", "/settings");
5815
5851
  if (!store.isInstanceOperator(who.name))
@@ -6522,6 +6558,52 @@ export function createDecisionServer(options) {
6522
6558
  store.recordAction({ at: now.toISOString(), actor: who.name, repo: null, taskId: null, runId: null, action: before === null ? "sign-in with a provider turned on" : "sign-in with a provider changed", outcome: "changed", source: "policy", detail: ssoChangeWords(before, saved.settings) });
6523
6559
  return found.ok ? back("said", `Saved. People can sign in with ${saved.settings.label}.`) : back("problem", `Saved, but ${saved.settings.label} didn't answer: ${found.said}`);
6524
6560
  }
6561
+ // Settings → Updates: start the `toolroll update` job. A step-up; the job writes the ledger entries.
6562
+ if (url.pathname === "/settings/updates" || url.pathname === "/settings/updates/seen" || url.pathname === "/settings/updates/cancel") {
6563
+ const databaseFile = store.databaseFile();
6564
+ if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
6565
+ return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
6566
+ const stateDir = dirname(databaseFile);
6567
+ const back = (key, words) => redirect(response, `/settings/updates?${key}=${encodeURIComponent(words)}`);
6568
+ if (url.pathname === "/settings/updates/seen") {
6569
+ markWhatsNewSeen(stateDir);
6570
+ return redirect(response, "/settings/updates");
6571
+ }
6572
+ if (url.pathname === "/settings/updates/cancel")
6573
+ return back("said", requestRuntimeUpdateCancel(stateDir));
6574
+ if (!authenticateApprover(store, who.name, body.get("password") ?? "").ok)
6575
+ return back("problem", "Enter your Toolroll password to update.");
6576
+ const version = body.get("version") ?? "";
6577
+ const when = { now: "now", "when-idle": "when-idle", tonight: "at" }[body.get("when") ?? ""] ?? null;
6578
+ if (!/^\d+\.\d+\.\d+$/.test(version) || when === null)
6579
+ return back("problem", "Choose when to update.");
6580
+ const current = options.updates?.current ?? PACKAGE_VERSION;
6581
+ if (!newerThan(version, current))
6582
+ return back("problem", `Toolroll ${version} is not newer than ${current}. To go back, run toolroll update --rollback.`);
6583
+ const method = options.updates?.method ?? installMethod();
6584
+ if (method.kind === "npx" || method.kind === "source" || method.kind === "desktop")
6585
+ return back("problem", method.kind === "npx" ? "npx runs the latest release each time; there is nothing to update." : method.kind === "desktop" ? "Update it from the Toolroll app." : "This runs from a source checkout. Update it with git.");
6586
+ const before = runtimeUpdateStatus(stateDir).journal;
6587
+ if (before !== null && !runtimeUpdateTerminal(before.phase))
6588
+ return back("problem", "An update is already under way.");
6589
+ if (when === "now") {
6590
+ const running = runningWorkWords(store.raw());
6591
+ if (running !== null)
6592
+ return back("problem", `Work is running: ${running}. Choose When idle to update once it finishes.`);
6593
+ }
6594
+ const prepared = prepareRuntimeUpdate({ stateDir, databaseFile, current: { version: current, dist: options.updates?.dist ?? currentRuntime(current).dist }, actor: who.name, version, when, at: "03:00" }, clock());
6595
+ if ("refused" in prepared)
6596
+ return back("problem", prepared.refused);
6597
+ try {
6598
+ await (options.updates?.launch ?? launchRuntimeUpdate)({ databaseFile, id: prepared.id });
6599
+ }
6600
+ catch (error) {
6601
+ abandonRuntimeUpdate(stateDir, prepared.id, `The update could not start: ${error.message}`, clock());
6602
+ return back("problem", `The update could not start: ${error.message}`);
6603
+ }
6604
+ // The job records its first step within moments; show it rather than the old page.
6605
+ return back("said", when === "at" ? `Update to ${version} scheduled for 03:00.` : `Updating to ${version}.`);
6606
+ }
6525
6607
  // v105: change how long each kind of data is kept. A step-up; the ledger keeps before → after for each kind.
6526
6608
  if (url.pathname === "/settings/retention") {
6527
6609
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
@@ -13984,7 +14066,7 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
13984
14066
  const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem;margin:0 0 2rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
13985
14067
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
13986
14068
  export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
13987
- const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + LIMITS_CSS + MONITORING_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + LEAD_CONTEXT_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
14069
+ const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + LEAD_CONTEXT_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
13988
14070
  /**
13989
14071
  * The board's liveness: fetch this page's own fragment on a timer and swap
13990
14072
  * it in place — no flicker, no scroll reset, no long-lived stream to manage,
@@ -14635,26 +14717,20 @@ function inboxPage(chrome, data) {
14635
14717
  `<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
14636
14718
  `<span class="right meta">how to fix \u2192</span></p>`)
14637
14719
  .join("\n");
14638
- // The first-run checklist replaces the empty-queue card while the
14639
- // installation has never succeeded; each step is live state, and the
14640
- // whole card retires permanently on the first successful run.
14720
+ // The first-run steps, until the first Ready result: each is done or
14721
+ // shows the one action that does it.
14641
14722
  const wizard = data.wizard === null
14642
14723
  ? ""
14643
- : `<div class="card">` +
14644
- `<p><strong>Getting started</strong> <span class="meta">\u2014 disappears after the first successful run</span></p>` +
14645
- `<p class="meta">Keep <span class="mono">toolroll up</span> running on this machine \u2014 it opens the app and reconnects every saved project's builder.</p>` +
14724
+ : `<div class="card" data-first-run>` +
14725
+ `<p><strong>Get to your first result</strong></p>` +
14646
14726
  data.wizard
14647
- .map(step => `<p class="row"><span class="mono">${step.done ? "\u2713" : "\u25cb"}</span> <strong>${escape(step.title)}</strong><br>` +
14648
- `<span class="meta">${step.detail}</span></p>`)
14727
+ .map(step => `<p class="row" data-step="${step.key}"><span class="mono" aria-hidden="true">${step.done ? "\u2713" : "\u25cb"}</span> <strong>${escape(step.title)}</strong>` +
14728
+ (step.action === null ? ` <span class="meta">done</span>`
14729
+ : step.action.kind === "link" ? ` <a href="${escape(step.action.href)}">${escape(step.action.label)}</a>`
14730
+ : ` <code>${escape(step.action.command)}</code>`) +
14731
+ `</p>`)
14649
14732
  .join("\n") +
14650
- `<p><a class="new-task" href="/recipes">Start with a guided recipe →</a></p>` +
14651
- `<p class="meta">templates \u2014 edit, then approve; nothing a template files carries authority: ` +
14652
- TEMPLATES.map(one => one.kind === "routine"
14653
- ? `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`
14654
- : one.kind === "task"
14655
- ? `<a href="/tasks?template=${escape(one.name)}">${escape(one.name)}</a>`
14656
- : `<span title="a recipe \u2014 walk it in the terminal: toolroll template show ${escape(one.name)}">${escape(one.name)} (recipe)</span>`).join(" \u00b7 ") +
14657
- `</p></div>`;
14733
+ `</div>`;
14658
14734
  const noWorker = data.worker.answering > 0
14659
14735
  ? ""
14660
14736
  : `<div class="card builder-notice" data-builder-status="${data.worker.registered === 0 ? "not-connected" : "disconnected"}">` +
@@ -21413,7 +21489,7 @@ function telegramDeliveryWords(store, bot) {
21413
21489
  return `Telegram pushes new messages to ${state.url}${state.problem === null ? "." : `, but ${state.problem.charAt(0).toLowerCase()}${state.problem.slice(1)}.`}`;
21414
21490
  return `Toolroll asks Telegram for new messages every few seconds${state?.problem ? ` (${state.problem})` : ""}.`;
21415
21491
  }
21416
- function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null) {
21492
+ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null) {
21417
21493
  const permissionCard = permissionDefault === null
21418
21494
  ? ""
21419
21495
  : [
@@ -21610,6 +21686,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
21610
21686
  email,
21611
21687
  workers,
21612
21688
  updates,
21689
+ firstResult,
21613
21690
  };
21614
21691
  return screen("Settings", [
21615
21692
  "<h1>Settings</h1>",
@@ -21620,6 +21697,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
21620
21697
  keysCard,
21621
21698
  workersCard(workers),
21622
21699
  updatesCard(updates, csrf),
21700
+ firstResult === null ? "" : `<p class="meta" data-first-result>${escape(firstResult)}</p>`,
21623
21701
  pushCard === "" && messagingCard === "" && digestCard === "" ? "" : `<h2>Notifications</h2>`,
21624
21702
  messagingCard,
21625
21703
  pushCard,
@@ -21686,7 +21764,7 @@ function updatesCard(updates, csrf) {
21686
21764
  (latest.notes === "" ? "" : `<details class="settings-more"><summary>What's new in ${escape(latest.version)}</summary><pre class="update-notes">${escape(latest.notes)}</pre></details>`)
21687
21765
  : "") +
21688
21766
  (updates.check.canManage && !updates.check.byEnv
21689
- ? `<form method="post" action="/settings/updates" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="check" value="${updates.check.on ? "off" : "on"}"><button type="submit">${updates.check.on ? "Turn off daily check" : "Turn on daily check"}</button></form>`
21767
+ ? `<form method="post" action="/settings/updates/checks" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="check" value="${updates.check.on ? "off" : "on"}"><button type="submit">${updates.check.on ? "Turn off daily check" : "Turn on daily check"}</button></form>`
21690
21768
  : updates.check.byEnv ? `<p class="meta">Off by TOOLROLL_NO_UPDATE_CHECK.</p>` : "") +
21691
21769
  (updates.workers.length === 0 ? "" : `<ul>${updates.workers.map(one => `<li data-worker-version="${escape(one.name)}">${escape(one.name)} <span class="mono">${escape(one.version ?? "unknown")}</span>${one.older ? " \u00b7 older" : ""}</li>`).join("")}</ul>`) +
21692
21770
  `</section>`;
@@ -21694,6 +21772,22 @@ function updatesCard(updates, csrf) {
21694
21772
  /** Choices save the moment they change; without the script the Save
21695
21773
  * button stays and the form works the same. */
21696
21774
  const SETTINGS_AUTOSAVE_SCRIPT = `(function(){document.querySelectorAll('form[data-autosave]').forEach(function(form){form.classList.add('js-autosave');form.addEventListener('change',function(ev){var t=ev.target;if(t&&(t.type==='radio'||t.tagName==='SELECT')){if(form.requestSubmit)form.requestSubmit();else form.submit();}});});})();`;
21775
+ /** The newest release, remembered for a while so the page stays quick. */
21776
+ let latestSeen = null;
21777
+ async function latestReleaseFor(latest) {
21778
+ if (latest === undefined && latestSeen !== null && Date.now() - latestSeen.at < ("problem" in latestSeen.value ? 60_000 : 900_000))
21779
+ return latestSeen.value;
21780
+ let value;
21781
+ try {
21782
+ value = { version: (await (latest ?? latestVersionNow)()).version };
21783
+ }
21784
+ catch (error) {
21785
+ value = { problem: error.message };
21786
+ }
21787
+ if (latest === undefined)
21788
+ latestSeen = { at: Date.now(), value };
21789
+ return value;
21790
+ }
21697
21791
  /** Settings destinations as a scannable grid: an icon and a name each. */
21698
21792
  function settingsTiles() {
21699
21793
  const tiles = SETTINGS_TILE_ICONS;
@@ -21716,6 +21810,7 @@ const SETTINGS_TILE_ICONS = [
21716
21810
  ["/settings/approval", "Approval rules", `<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/>`],
21717
21811
  ["/settings/monitoring", "Monitoring", `<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>`],
21718
21812
  ["/settings/retention", "Retention", `<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>`],
21813
+ ["/settings/updates", "Updates", `<path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/>`],
21719
21814
  ["/settings/backups", "Backups", `<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/>`],
21720
21815
  ["/settings/data", "Data", `<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>`],
21721
21816
  ];
package/dist/skills.d.ts CHANGED
@@ -68,6 +68,8 @@ export declare function applyInstall(repo: string, writeContext: boolean): Insta
68
68
  */
69
69
  export declare const CLAUDE_CODE_MANAGED_MARK = "<!-- standing-orders:claude-code-skill:v1 -->";
70
70
  export declare const CLAUDE_CODE_GUIDES: readonly ["console", "operating", "runner"];
71
+ /** The thin user-level skill `toolroll onboard` writes (agent-onboard.ts). */
72
+ export declare const OPERATOR_SKILL_MARK = "<!-- toolroll:operator-skill:v1";
71
73
  export type ClaudeCodeSkillFile = {
72
74
  name: string;
73
75
  path: string;
package/dist/skills.js CHANGED
@@ -162,6 +162,8 @@ export function applyInstall(repo, writeContext) {
162
162
  */
163
163
  export const CLAUDE_CODE_MANAGED_MARK = "<!-- standing-orders:claude-code-skill:v1 -->";
164
164
  export const CLAUDE_CODE_GUIDES = ["console", "operating", "runner"];
165
+ /** The thin user-level skill `toolroll onboard` writes (agent-onboard.ts). */
166
+ export const OPERATOR_SKILL_MARK = "<!-- toolroll:operator-skill:v1";
165
167
  export function defaultClaudeCodeSkillDir(home = homedir()) {
166
168
  return join(home, ".claude", "skills", "toolroll");
167
169
  }
@@ -232,7 +234,7 @@ function claudeCodeFiles(directory) {
232
234
  }
233
235
  function isClaudeCodeManaged(name, content) {
234
236
  if (name === "SKILL.md") {
235
- return ownHeader(content) && content.includes(`\n${CLAUDE_CODE_MANAGED_MARK}\n`);
237
+ return ownHeader(content) && (content.includes(`\n${CLAUDE_CODE_MANAGED_MARK}\n`) || content.includes(`\n${OPERATOR_SKILL_MARK} `));
236
238
  }
237
239
  return content.startsWith(`${CLAUDE_CODE_MANAGED_MARK}\n\n`);
238
240
  }
package/dist/store.d.ts CHANGED
@@ -4337,16 +4337,16 @@ export declare class Store {
4337
4337
  * createConsoleTask. Set-once: an already-stamped row never changes. */
4338
4338
  stampFiledVia(refId: number, via: string): void;
4339
4339
  /**
4340
- * When this installation first finished a run successfully — the
4340
+ * When this installation first finished a run with a change — the
4341
4341
  * moment the first-run checklist retires, permanently. Derived from run
4342
4342
  * history the first time it is observed, then stamped as an append-only
4343
4343
  * installation fact so later pruning of those rows cannot resurrect the
4344
4344
  * checklist (adoption review, finding 14).
4345
4345
  */
4346
4346
  firstSuccessAt(now: Date): string | null;
4347
- /** Whether ANY spend routing is configured — a fact about this database,
4348
- * not about any machine's binaries or authentication. */
4349
- hasPhaseConfig(): boolean;
4347
+ /** When this installation began: its first account. Accounts are never
4348
+ * deleted (revocation is a stamp), so this never moves. */
4349
+ installationStartedAt(): string | null;
4350
4350
  /** The jump palette's food: open (queued/running/failed) tasks, admission
4351
4351
  * inside the query before its LIMIT, newest first. */
4352
4352
  paletteTasks(repo: string | null, limit: number, admitted?: string[] | null, includeUnplaced?: boolean): {
package/dist/store.js CHANGED
@@ -14391,7 +14391,7 @@ export class Store {
14391
14391
  this.db.prepare("UPDATE task_ref SET filed_via = ? WHERE id = ? AND filed_via IS NULL").run(via, refId);
14392
14392
  }
14393
14393
  /**
14394
- * When this installation first finished a run successfully — the
14394
+ * When this installation first finished a run with a change — the
14395
14395
  * moment the first-run checklist retires, permanently. Derived from run
14396
14396
  * history the first time it is observed, then stamped as an append-only
14397
14397
  * installation fact so later pruning of those rows cannot resurrect the
@@ -14402,7 +14402,7 @@ export class Store {
14402
14402
  if (fact !== null)
14403
14403
  return fact;
14404
14404
  const row = this.db
14405
- .prepare("SELECT finished_at FROM run WHERE outcome IN ('built','no-change') AND finished_at IS NOT NULL ORDER BY id LIMIT 1")
14405
+ .prepare("SELECT finished_at FROM run WHERE outcome = 'built' AND finished_at IS NOT NULL ORDER BY id LIMIT 1")
14406
14406
  .get();
14407
14407
  if (row === undefined)
14408
14408
  return null;
@@ -14410,10 +14410,11 @@ export class Store {
14410
14410
  this.recordInstallationFact("first-success-at", when, now);
14411
14411
  return when;
14412
14412
  }
14413
- /** Whether ANY spend routing is configured — a fact about this database,
14414
- * not about any machine's binaries or authentication. */
14415
- hasPhaseConfig() {
14416
- return this.db.prepare("SELECT 1 AS hit FROM phase_config LIMIT 1").get() !== undefined;
14413
+ /** When this installation began: its first account. Accounts are never
14414
+ * deleted (revocation is a stamp), so this never moves. */
14415
+ installationStartedAt() {
14416
+ const row = this.db.prepare("SELECT MIN(added_at) AS at FROM approver").get();
14417
+ return row === undefined || row["at"] == null ? null : String(row["at"]);
14417
14418
  }
14418
14419
  /** The jump palette's food: open (queued/running/failed) tasks, admission
14419
14420
  * inside the query before its LIMIT, newest first. */
@@ -15936,6 +15937,11 @@ export class Store {
15936
15937
  // stop after an orphan or held supervisor finishes shutdown.
15937
15938
  this.recordRunProcessExits(id, result.now);
15938
15939
  this.settleRunStop(id, result.stopSettlement ?? "finished", result.now);
15940
+ // The first Ready result is an installation fact the moment it lands,
15941
+ // not only when something later looks for it. A run that changed
15942
+ // nothing is not a first result.
15943
+ if (result.outcome === "built")
15944
+ this.firstSuccessAt(result.now);
15939
15945
  // The park rate is *measured* — parked over concluded builder attempts —
15940
15946
  // and maintained where attempts conclude, because the attention budget's
15941
15947
  // gate reads it inside a claim transaction and must never trust a number
package/dist/surface.js CHANGED
@@ -105,6 +105,7 @@ export const COMMAND_GUIDE = [
105
105
  },
106
106
  operator("link", "put toolroll on PATH"),
107
107
  operator("unlink", "take it off PATH"),
108
+ operator("update", "update to the latest release: verified provenance, drained work, backup, rehearsal, health check, automatic restore; --rollback undoes it"),
108
109
  operator("demo", "a seeded throwaway sandbox"),
109
110
  { invocation: 'session capabilities', synopsis: 'Show executable session schemas and operator authority requirements; no credentials needed', audience: 'agent', agentMayInvoke: true, mutation: 'none', flags: [jsonFlag] },
110
111
  ...SESSION_DESCRIPTORS.map(spec => ({
@@ -348,5 +349,6 @@ export const COMMAND_GUIDE = [
348
349
  operator("serve", "the operations console (HTTP)"),
349
350
  operator("watch", "the unattended loop, kept running"),
350
351
  operator("up", "console + worker + browser, one command"),
352
+ operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), and print the handoff"),
351
353
  operator("daemon", "install the loop under the OS service manager"),
352
354
  ];
@@ -0,0 +1,15 @@
1
+ import { installMethod } from "./install-method.js";
2
+ import { type RuntimeRef, type UpdateSystem } from "./toolroll-update.js";
3
+ export declare const UPDATE_HELP = "toolroll update preview updating to the latest release\ntoolroll update --yes update: verify, let running work finish, back up, rehearse, switch, restart, check\n --version X a specific release instead of the latest\n --when-idle pause new work and wait for running work (the default)\n --now refuse if any work is running\n --at HH:MM wait until then (e.g. --at 03:00)\n --allow-downgrade with --version, go back to an older release\ntoolroll update --rollback [--yes] return to the previous version and its database backup\ntoolroll update --status | --resume | --cancel";
4
+ export type UpdateCliDeps = {
5
+ system?: UpdateSystem;
6
+ current?: RuntimeRef;
7
+ method?: ReturnType<typeof installMethod>;
8
+ latest?: () => Promise<{
9
+ version: string;
10
+ }>;
11
+ databaseFile?: string;
12
+ /** Where the console's one-off update job's definition lives (tests). */
13
+ home?: string;
14
+ };
15
+ export declare function runUpdateCommand(args: readonly string[], write: (line: string) => void, deps?: UpdateCliDeps): Promise<number>;