toolroll 0.9.74 → 0.9.75
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 +31 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/browser/workspace.js +27 -27
- package/dist/browser-workspace.d.ts +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +16 -4
- package/dist/contracts/console-api.d.ts +5 -1
- package/dist/contracts/console-api.js +10 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/desktop-update.d.ts +3 -0
- package/dist/desktop-update.js +43 -9
- package/dist/first-run.d.ts +8 -5
- package/dist/first-run.js +39 -6
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +2 -0
- package/dist/project-learning.js +44 -6
- package/dist/project-tools.js +19 -1
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/repository-context.js +6 -1
- package/dist/result-follow-ups.js +20 -1
- package/dist/result-review.js +5 -1
- package/dist/serve.js +162 -6
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-room.js +4 -1
- package/dist/server/chat.js +31 -23
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/pages.js +18 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +28 -24
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.js +7 -4
- package/dist/server/route-table.js +14 -1
- package/dist/server/runtime.d.ts +16 -1
- package/dist/server/settings.js +82 -29
- package/dist/server/task-views.js +0 -1
- package/dist/server/tasks.js +11 -2
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.js +33 -9
- package/dist/subagent-work.js +3 -0
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +27 -1
- package/dist/teardown-git.js +185 -39
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/package.json +1 -1
package/dist/server/settings.js
CHANGED
|
@@ -122,14 +122,14 @@ import { teamsSettingsHtml } from "../teams-settings.js";
|
|
|
122
122
|
import { telegramSettingsHtml } from "../telegram-settings.js";
|
|
123
123
|
import { hashPairingCode, loadBotToken, mintPairingCode, PAIRING_TTL_MS, redactToken, saveBotToken } from "../telegram.js";
|
|
124
124
|
import { newerThan, updatesHtml, updatesScript, updateStepsHtml } from "../toolroll-update-ui.js";
|
|
125
|
-
import { abandonRuntimeUpdate, currentRuntime, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate,
|
|
125
|
+
import { abandonRuntimeUpdate, currentRuntime, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate, requestRuntimeUpdateCancel, runningWorkWords, runtimeUpdateStatus, runtimeUpdateTerminal } from "../toolroll-update.js";
|
|
126
126
|
import { TOOLS_PROJECT_SCRIPT, toolsHtml, toolsProjectPicker } from "../tools-ui.js";
|
|
127
127
|
import { effectivePrimary, isMessagingChannel, savePrimary } from "../webhooks.js";
|
|
128
128
|
import { personChip, ROLE_TITLES } from "./chrome.js";
|
|
129
129
|
import { chatReturnWithSaid, goOutside } from "./http.js";
|
|
130
|
-
import {
|
|
130
|
+
import { leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
|
|
131
131
|
export function createSettingsHandlers(runtime) {
|
|
132
|
-
const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
132
|
+
const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, connectionKnown, lastChecks, checkRelease, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, chatCatalogKnown, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
133
133
|
// v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
|
|
134
134
|
async function spendPage(ctx) {
|
|
135
135
|
const { url, who, response, now } = ctx;
|
|
@@ -225,10 +225,11 @@ ${hasGrant ? "" : html `<span class="meta">self-merging needs a merge-capable pu
|
|
|
225
225
|
if (!isProviderId(asked))
|
|
226
226
|
return refuse(response, who, 400, "Choose a known provider.", "/control");
|
|
227
227
|
const task = url.searchParams.get("task") ?? "";
|
|
228
|
-
|
|
228
|
+
// The last sign-in check: Check again posts (docs/plans/get-side-effects.md).
|
|
229
|
+
const connection = connectionKnown(asked);
|
|
229
230
|
if (view === "connection") {
|
|
230
231
|
const command = asked === "claude" ? "claude auth login" : asked === "codex" ? "codex login" : asked === "gemini" ? "gemini" : null;
|
|
231
|
-
return sendScreen(response, 200, screen(`${ASSISTANTS[asked].name} connection`, html `<h1>${ASSISTANTS[asked].name} ${connection
|
|
232
|
+
return sendScreen(response, 200, screen(`${ASSISTANTS[asked].name} connection`, html `<h1>${ASSISTANTS[asked].name} ${connection?.state === "connected" ? "is connected" : "connection"}</h1>\
|
|
232
233
|
${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On the computer running Toolroll, sign in with:</p><pre class="recap">${command}</pre>`}\
|
|
233
234
|
<p><a href="/settings#providers">Manage API keys and authentication mode</a></p><input type="hidden" name="resume-task" value="${task}">`, { chrome: chromeFor(project, "settings") }));
|
|
234
235
|
}
|
|
@@ -241,20 +242,61 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
241
242
|
const models = live.length === 0 ? modelChoices(asked, configured, providerHome)
|
|
242
243
|
: configured && !live.some(one => one.value === configured) ? [{ value: configured, label: `${configured} — current choice` }, ...live] : live;
|
|
243
244
|
const instructions = previewProjectInstructions(project);
|
|
244
|
-
|
|
245
|
+
// The model list as last loaded; Refresh models posts, and the background checks keep it current.
|
|
246
|
+
const catalog = asked === "openrouter" ? modelCatalog.known(readProviderKey("openrouter", providerHome)) ?? { ok: false, problem: "The model list has not loaded yet." } : null;
|
|
245
247
|
const inputs = { provider: asked, model: saved?.provider === asked ? saved.model ?? "" : models[0]?.value ?? "", command: setup?.command ?? (saved === null ? preparation?.command ?? "" : ""), seconds: String((setup?.timeoutMs ?? 300_000) / 1000) };
|
|
246
248
|
return sendScreen(response, 200, screen("Project setup", controlSetupHtml({ repo: project, csrf: who.via === "cookie" ? who.session.csrf : "", provider: asked, inputs, models, connection, catalog, task, preparation,
|
|
247
249
|
instructions: instructions.ok ? { installed: instructions.installed } : { installed: false, message: instructions.message } }), { chrome: chromeFor(project, "settings"), ...(catalog === null ? {} : { functional: { script: openRouterPickerScript() } }) }));
|
|
248
250
|
};
|
|
251
|
+
// Settings → Projects → Pull requests → Check setup: git and GitHub, for one project, by an approver who may turn it on.
|
|
252
|
+
async function pullRequestsCheck(ctx) {
|
|
253
|
+
const { who, response, posted } = ctx;
|
|
254
|
+
const repo = readForm(posted, CONSOLE_FORMS.pullRequestsCheck).get("repo") ?? "";
|
|
255
|
+
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
256
|
+
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
257
|
+
return refuse(response, who, 404, "No such project.", "/projects");
|
|
258
|
+
if (who.role !== "approver" || store.isDemo())
|
|
259
|
+
return refuse(response, who, 403, "An approver can turn on pull requests.", "/projects");
|
|
260
|
+
await lastChecks.run(`publishing:${repo}`, () => checkPublishing(repo, options.publishExec === undefined ? {} : { exec: options.publishExec }));
|
|
261
|
+
return redirect(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}`);
|
|
262
|
+
}
|
|
263
|
+
// Settings → Storage → Check now: measure the checkouts again.
|
|
264
|
+
async function storageCheck(ctx) {
|
|
265
|
+
const databaseFile = store.databaseFile();
|
|
266
|
+
if (databaseFile !== null)
|
|
267
|
+
await measureStorage(databaseFile);
|
|
268
|
+
return redirect(ctx.response, "/settings/storage");
|
|
269
|
+
}
|
|
270
|
+
const measureStorage = (databaseFile) => lastChecks.run("storage", () => checkoutPlan(store, storagePool(databaseFile), clock(), { manual: true }));
|
|
271
|
+
// Settings → Updates → Check now: ask npm once, whether or not automatic checks are on.
|
|
272
|
+
async function updatesCheck(ctx) {
|
|
273
|
+
await checkRelease();
|
|
274
|
+
return redirect(ctx.response, "/settings/updates");
|
|
275
|
+
}
|
|
276
|
+
// Check again (Settings → providers, Project setup): ask this computer's CLI, or test the saved key, now.
|
|
277
|
+
async function connectionCheckNow(ctx) {
|
|
278
|
+
const { response, posted } = ctx;
|
|
279
|
+
const body = readForm(posted, CONSOLE_FORMS.connectionCheck);
|
|
280
|
+
const provider = body.get("provider") ?? "";
|
|
281
|
+
const back = safeReturn(body.get("return") ?? "/settings#providers");
|
|
282
|
+
if (!isProviderId(provider))
|
|
283
|
+
return redirect(response, back);
|
|
284
|
+
await connectionCheck(provider, true).catch(() => undefined);
|
|
285
|
+
return redirect(response, back);
|
|
286
|
+
}
|
|
287
|
+
// Refresh models (Project setup, OpenRouter): load the list again, then show the setup page with it.
|
|
288
|
+
async function modelsRefresh(ctx) {
|
|
289
|
+
const { response, posted } = ctx;
|
|
290
|
+
const body = readForm(posted, CONSOLE_FORMS.modelsRefresh);
|
|
291
|
+
await modelCatalog(readProviderKey("openrouter", providerHome), true);
|
|
292
|
+
const query = new URLSearchParams({ provider: "openrouter", ...(body.get("task") ? { task: body.get("task") } : {}) });
|
|
293
|
+
return redirect(response, `/control?${query.toString()}`);
|
|
294
|
+
}
|
|
249
295
|
async function modelsPage(ctx) {
|
|
250
296
|
const { url, who, response, project } = ctx;
|
|
251
297
|
const now = clock();
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
// the check sends nothing about the person or their projects.
|
|
255
|
-
if (who.role === "approver" && (watch.checkedAt === null || now.getTime() - Date.parse(watch.checkedAt) > 3_600_000)) {
|
|
256
|
-
await checkModels(store, now, modelSeams).catch(() => undefined);
|
|
257
|
-
}
|
|
298
|
+
// The page shows the last check and when it ran (docs/plans/get-side-effects.md): Check now, or the watch loop's
|
|
299
|
+
// automatic checks, fetch the lists and may queue a notice; opening the page never does.
|
|
258
300
|
const view = modelsView(who.role === "approver", who.via === "cookie" ? who.session.csrf : "", now, url.searchParams.get("said"), url.searchParams.get("problem"));
|
|
259
301
|
return sendScreen(response, 200, screen("Models", html `<p><a href="/settings">Settings</a></p><h1>Models</h1>${modelsHtml(view)}`, { chrome: chromeFor(project, "settings"), functional: { script: modelsScript() } }));
|
|
260
302
|
}
|
|
@@ -290,7 +332,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
290
332
|
if (chosen) {
|
|
291
333
|
try {
|
|
292
334
|
const approver = who.role === "approver" && who.via === "cookie";
|
|
293
|
-
|
|
335
|
+
// What Codex had configured at the last background check (docs/plans/get-side-effects.md): the page starts no codex.
|
|
336
|
+
const view = toolsViewOf(chosen, approver ? lastChecks.known(`codex-tools:${chosen}`)?.value ?? null : null, kit, wanted);
|
|
294
337
|
if (approver)
|
|
295
338
|
view.others = projects.filter(one => one !== chosen).map(one => ({ repo: one, project: projectName(one), open: connectionsOf(store, one).filter(c => c.state === "open").map(c => c.id) }));
|
|
296
339
|
content = (toolsHtml(view, who.via === "cookie" ? who.session.csrf : "", approver, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }));
|
|
@@ -376,19 +419,16 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
376
419
|
const databaseFile = store.databaseFile();
|
|
377
420
|
if (databaseFile === null)
|
|
378
421
|
return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
|
|
379
|
-
// A
|
|
380
|
-
try {
|
|
381
|
-
releaseStalledUpdate(dirname(databaseFile), clock());
|
|
382
|
-
}
|
|
383
|
-
catch { /* shown as last saved */ }
|
|
422
|
+
// A stalled update is released by the background pass, never by opening this page (docs/plans/get-side-effects.md).
|
|
384
423
|
const status = runtimeUpdateStatus(dirname(databaseFile));
|
|
385
424
|
if (url.searchParams.get("fragment") === "steps")
|
|
386
425
|
return respond(response, 200, "text/html; charset=utf-8", status.journal ? updateStepsHtml(status.journal, status.running) : html `<div id="update-live" data-done="1"></div>`);
|
|
387
426
|
const active = status.journal !== null && !runtimeUpdateTerminal(status.journal.phase);
|
|
388
427
|
const current = options.updates?.current ?? PACKAGE_VERSION;
|
|
389
|
-
//
|
|
390
|
-
const checksOff = updateChecksOff(process.env, options.configDir ?? dirname(databaseFile)).off
|
|
391
|
-
const
|
|
428
|
+
// The last answer npm gave (the background checks ask while update checks are on; Check now asks once).
|
|
429
|
+
const checksOff = updateChecksOff(process.env, options.configDir ?? dirname(databaseFile)).off;
|
|
430
|
+
const known = lastChecks.known("release")?.value ?? null;
|
|
431
|
+
const view = { current, latest: active ? { version: current } : known ?? (checksOff ? { off: true } : { pending: true }), method: options.updates?.method ?? installMethod(), journal: status.journal, running: status.running, whatsNew: status.whatsNew, rollbackTo: status.lastUpdate?.to === current ? status.lastUpdate.from : null, csrf: who.session.csrf };
|
|
392
432
|
return sendScreen(response, 200, screen("Updates", html `<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 } } : {}) }));
|
|
393
433
|
}
|
|
394
434
|
// v105: Settings → Retention, how long each kind of data is kept. An instance operator's page.
|
|
@@ -406,9 +446,16 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
406
446
|
const databaseFile = store.databaseFile();
|
|
407
447
|
if (databaseFile === null)
|
|
408
448
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
409
|
-
|
|
449
|
+
// The last measurement (docs/plans/get-side-effects.md): the background checks measure the checkouts, and so do
|
|
450
|
+
// Check now and every clean-up; opening the page runs no du or git.
|
|
451
|
+
const measured = lastChecks.known("storage");
|
|
452
|
+
const check = postForm("/settings/storage/check", html `<button type="submit" class="secondary">Check now</button>`, { attrs: { class: "inline" } });
|
|
453
|
+
if (measured === null) {
|
|
454
|
+
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1><section class="storage"><div class="card storage-state"><h2>Checkouts not measured yet</h2>${check}</div></section>`, { chrome: chromeFor(project, "settings") }));
|
|
455
|
+
}
|
|
456
|
+
const plan = { ...measured.value, cleanup: store.checkoutCleanup() };
|
|
410
457
|
const sweep = { last: lastSweep(store), off: storageSweepOff(), schedule: sweepSchedule(store, now) };
|
|
411
|
-
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep, landed: landedSummary(store) }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
458
|
+
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep, landed: landedSummary(store), measured: html `<p class="meta">Measured ${new Date(measured.at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })} ${check}</p>` }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
412
459
|
}
|
|
413
460
|
// Settings → Projects → Pull requests: one project's setup. Off, the checks run on open, so the page says
|
|
414
461
|
// exactly what turning on would do, or what to fix first.
|
|
@@ -420,7 +467,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
420
467
|
return refuse(response, who, 404, "No such project.", "/projects");
|
|
421
468
|
const publishing = publishingOf(store, repo);
|
|
422
469
|
const canChange = who.role === "approver" && !store.isDemo();
|
|
423
|
-
|
|
470
|
+
// The last setup check for this project (Check setup posts); opening the page asks git and GitHub nothing.
|
|
471
|
+
const check = publishing.on || !canChange ? null : lastChecks.known(`publishing:${repo}`)?.value ?? "unchecked";
|
|
424
472
|
const body = (pullRequestSettingsHtml({ repo, name: projectName(repo), csrf: who.session.csrf, canChange, publishing, check,
|
|
425
473
|
said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }));
|
|
426
474
|
return sendScreen(response, 200, screen("Pull requests", html `<p><a href="/projects">Projects</a></p><h1>Pull requests</h1>${body}`, { chrome: chromeFor(project, "projects") }));
|
|
@@ -563,15 +611,13 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
563
611
|
async function leadPage(ctx) {
|
|
564
612
|
const { url, response, now, project } = ctx;
|
|
565
613
|
const who = browserCaller(ctx.who);
|
|
566
|
-
if (runtime.localSignIn === null)
|
|
567
|
-
await Promise.race([checkLocalAgents(), new Promise(done => setTimeout(done, 6_000).unref?.())]);
|
|
568
614
|
const config = store.getChatConfig();
|
|
569
615
|
const facts = {
|
|
570
616
|
keyFacts: ["anthropic-api", "openrouter-api"].map(one => {
|
|
571
617
|
const found = chatKeyFor(one);
|
|
572
618
|
return { provider: one, state: found === null ? "none" : found.source, tail: found === null || found.source === "environment" ? null : redactToken(found.key) };
|
|
573
619
|
}),
|
|
574
|
-
openrouterModels: (
|
|
620
|
+
openrouterModels: chatCatalogKnown()?.map(one => one.id) ?? null,
|
|
575
621
|
liveModels: [...modelOptions(store, "claude", now, providerHome), ...modelOptions(store, "codex", now, providerHome)],
|
|
576
622
|
csrf: who.session.csrf,
|
|
577
623
|
returnTo: "/settings/lead",
|
|
@@ -646,7 +692,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
646
692
|
provider,
|
|
647
693
|
envName: PROVIDER_KEY_ENV[provider],
|
|
648
694
|
...keyStatus(provider, providerHome),
|
|
649
|
-
|
|
695
|
+
...(connectionKnown(provider) === null ? {} : { connection: connectionKnown(provider) }),
|
|
650
696
|
ambient: (process.env[PROVIDER_KEY_ENV[provider]] ?? "") !== "",
|
|
651
697
|
mode: readAuthMode(provider, providerHome) === "api-key" ? "api-key" : "subscription",
|
|
652
698
|
subscriptionCapable: SUBSCRIPTION_CAPABLE[provider],
|
|
@@ -1114,7 +1160,9 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1114
1160
|
const databaseFile = store.databaseFile();
|
|
1115
1161
|
if (databaseFile === null)
|
|
1116
1162
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
1117
|
-
|
|
1163
|
+
// What the page shows next is measured again after the change.
|
|
1164
|
+
const said = sayTo(response, "/settings/storage");
|
|
1165
|
+
const back = async (kind, words) => { await measureStorage(databaseFile).catch(() => undefined); return said(kind, words); };
|
|
1118
1166
|
const what = op === "clean" ? "clean up" : op === "discard" ? "discard a checkout" : "change checkout cleanup";
|
|
1119
1167
|
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
1120
1168
|
return back("problem", `Enter your Toolroll password to ${what}.`);
|
|
@@ -2468,6 +2516,11 @@ ${modeWords(terms)
|
|
|
2468
2516
|
"mode": modePage,
|
|
2469
2517
|
"control": controlPage("setup"),
|
|
2470
2518
|
"control.connection": controlPage("connection"),
|
|
2519
|
+
"settings.updates-check-send": updatesCheck,
|
|
2520
|
+
"settings.storage-check-send": storageCheck,
|
|
2521
|
+
"settings.pull-requests-check-send": pullRequestsCheck,
|
|
2522
|
+
"settings.connection-check": connectionCheckNow,
|
|
2523
|
+
"control.models-refresh": modelsRefresh,
|
|
2471
2524
|
"settings.models": modelsPage,
|
|
2472
2525
|
"settings.tools": toolsPage,
|
|
2473
2526
|
"settings.project": projectPage,
|
|
@@ -487,7 +487,6 @@ export function createTaskViews(runtime) {
|
|
|
487
487
|
coordinatorProposals: (() => {
|
|
488
488
|
if (ref === null || ref.repo === null || who.role !== "approver")
|
|
489
489
|
return null;
|
|
490
|
-
runtime.store.sweepCoordinatorProposals(now);
|
|
491
490
|
const rows = runtime.store.listCoordinatorProposals({ repos: [ref.repo], taskId });
|
|
492
491
|
return { rows, decisions: decisionsFor(runtime.store, rows), now };
|
|
493
492
|
})(),
|
package/dist/server/tasks.js
CHANGED
|
@@ -12,7 +12,7 @@ import { handlersOf } from './handler-registry.js';
|
|
|
12
12
|
import { closedQuestionWords } from "../decision.js";
|
|
13
13
|
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
14
14
|
/** tasks handlers, moved without changing their route bodies. */
|
|
15
|
-
import { html, htmlString, joinHtml, textHtml } from "../html.js";
|
|
15
|
+
import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
|
|
16
16
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
17
17
|
import { lstatSync, realpathSync } from "node:fs";
|
|
18
18
|
import {} from "node:http";
|
|
@@ -75,7 +75,9 @@ export function createTasksHandlers(runtime) {
|
|
|
75
75
|
function noPage(who, response) {
|
|
76
76
|
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
// `posted`: the session page's own POST (form token). A GET of changes or the shipping preview runs no git in the
|
|
79
|
+
// agent's checkout: changes go back to the session page, the preview asks for one click.
|
|
80
|
+
const showCode = (part, posted = false) => async (ctx) => {
|
|
79
81
|
const { url, who, response, project } = ctx;
|
|
80
82
|
if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
|
|
81
83
|
return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/work');
|
|
@@ -86,6 +88,11 @@ export function createTasksHandlers(runtime) {
|
|
|
86
88
|
const selected = sessionId !== null && runtime.coding ? runtime.coding.get(sessionId, actor) : null;
|
|
87
89
|
if (selected && !codingProjectAllowed(selected.repo))
|
|
88
90
|
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
91
|
+
if (!posted && part === 'changes' && selected)
|
|
92
|
+
return redirect(response, `/code/${selected.id}`);
|
|
93
|
+
if (!posted && part === 'ship' && selected) {
|
|
94
|
+
return sendScreen(response, 200, screen('Review for shipping', html `<section class="coding-workspace coding-handoff"><h1>Review for shipping</h1><p>${selected.title}</p>${postForm(`/code/${selected.id}/ship/preview`, html `<button type="submit">Review the committed change</button>`)}<p><a href="/code/${selected.id}">Back to coding</a></p></section>`, { chrome: chromeFor(selected.repo, 'code') }));
|
|
95
|
+
}
|
|
89
96
|
if (part === 'ship' && selected) {
|
|
90
97
|
const preview = codingHandoffPreview(store, { sessionId: selected.id, actor: who.name });
|
|
91
98
|
return sendScreen(response, 200, screen('Review for shipping', codingShippingHtml(preview), { chrome: chromeFor(selected.repo, 'code') }));
|
|
@@ -2996,6 +3003,8 @@ export function createTasksHandlers(runtime) {
|
|
|
2996
3003
|
"code.state": showCode("state"),
|
|
2997
3004
|
"code.changes": showCode("changes"),
|
|
2998
3005
|
"code.ship": showCode("ship"),
|
|
3006
|
+
"code.changes-read": showCode("changes", true),
|
|
3007
|
+
"code.ship-preview": showCode("ship", true),
|
|
2999
3008
|
"code.other": showCode("unknown"),
|
|
3000
3009
|
"inbox": showInbox,
|
|
3001
3010
|
"work": showWork,
|
package/dist/storage-ui.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type StorageView = {
|
|
|
13
13
|
schedule?: SweepSchedule;
|
|
14
14
|
};
|
|
15
15
|
landed?: LandedSummary;
|
|
16
|
+
/** When the checkouts were last measured, and its Check now (docs/plans/get-side-effects.md). */
|
|
17
|
+
measured?: Html;
|
|
16
18
|
};
|
|
17
19
|
export declare function storageHtml(view: StorageView, notice: {
|
|
18
20
|
said?: string | null;
|
package/dist/storage-ui.js
CHANGED
|
@@ -85,5 +85,5 @@ export function storageHtml(view, notice) {
|
|
|
85
85
|
const preview = html `<details class="clean-up" data-clean-up${notice.problem?.startsWith("Enter your Toolroll password to clean") ? html ` open` : ""}><summary>Clean up</summary><div class="preview">${plan.go.length === 0 ? html `<p>Nothing to remove now.</p>` :
|
|
86
86
|
html `<h3>Removes ${plural(plan.go.length, "checkout")}, about ${bytesWords(plan.freeBytes)}</h3><p class="meta">Their branches stay, so nothing committed is lost.</p><ul class="checkouts" data-goes>${plan.go.map(one => item(one))}</ul>`}${plan.stay.length === 0 ? "" : html `<h3>Stays (${plan.stay.length})</h3><ul class="checkouts" data-stays>${plan.stay.map(one => item(one))}</ul>`}${remove}</div></details>`;
|
|
87
87
|
const options = CLEANUP_CHOICES.map(one => html `<option value="${one.value}"${one.value === plan.cleanup ? html ` selected` : ""}>${one.label}</option>`);
|
|
88
|
-
return html `<section class="storage">${note}<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${bytesWords(plan.totalBytes)}</h2><p class="meta">${facts}</p
|
|
88
|
+
return html `<section class="storage">${note}<div class="card storage-state"><h2 data-checkout-bytes="${plan.totalBytes}">Checkouts use ${bytesWords(plan.totalBytes)}</h2><p class="meta">${facts}</p>${view.measured ?? ""}<p data-clean-bytes="${plan.freeBytes}">${frees}</p>${view.sweep === undefined ? "" : sweepHtml(view.sweep)}${view.landed === undefined ? "" : landedHtml(view.landed)}${preview}</div><h2>Automatic cleanup</h2>${postForm("/settings/storage", html `<label>Remove a finished task's clean checkout<select name="cleanup">${options}</select></label><p class="meta">Its branch stays. Checkouts with changes, commits on no branch, in use, or waiting for review are kept. When a task's work is already in main, Toolroll marks it complete and removes its checkout and local branches.</p>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Save</button>`, { attrs: { class: "setting" } })}</section>`;
|
|
89
89
|
}
|
package/dist/storage.js
CHANGED
|
@@ -101,7 +101,8 @@ export function diskBytes(paths) {
|
|
|
101
101
|
if (process.platform !== "win32") {
|
|
102
102
|
for (let at = 0; at < paths.length; at += 200) {
|
|
103
103
|
const batch = paths.slice(at, at + 200);
|
|
104
|
-
|
|
104
|
+
// Bounded: a slow disk leaves the rest unmeasured rather than holding everything up.
|
|
105
|
+
const du = spawnSync("du", ["-sk", ...batch], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 60_000 });
|
|
105
106
|
for (const line of (du.stdout ?? "").split("\n")) {
|
|
106
107
|
const match = /^(\d+)\t(.+)$/.exec(line);
|
|
107
108
|
if (match !== null && batch.includes(match[2]))
|
package/dist/store.d.ts
CHANGED
|
@@ -2397,6 +2397,9 @@ export declare class Store {
|
|
|
2397
2397
|
signedBy: string;
|
|
2398
2398
|
absoluteExpiry: string;
|
|
2399
2399
|
} | null;
|
|
2400
|
+
/** Close every operating mode past its expiry (the background pass). Returns how many were closed. */
|
|
2401
|
+
closeExpiredModes(now: Date): number;
|
|
2402
|
+
private closeExpiredMode;
|
|
2400
2403
|
/** Sign (or renew) a mode: closes any predecessor with a typed event and
|
|
2401
2404
|
* ATOMICALLY reconciles the repo's nonterminal merge intents to the new
|
|
2402
2405
|
* posture (F2). The caller has already run the password ceremony. */
|
package/dist/store.js
CHANGED
|
@@ -97,6 +97,7 @@ import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
|
|
|
97
97
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, subagentFilers, usd } from "./spend.js";
|
|
98
98
|
import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
|
|
99
99
|
import { CHECKOUT_CLEANUP_SCHEMA, LANDED_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
100
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
100
101
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
101
102
|
import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
|
|
102
103
|
import { configBase, envValue, namedPath } from "./names.js";
|
|
@@ -7163,15 +7164,10 @@ export class Store {
|
|
|
7163
7164
|
if (row === undefined)
|
|
7164
7165
|
return null;
|
|
7165
7166
|
if (Date.parse(String(row["absolute_expiry"])) <= now.getTime()) {
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
this.db
|
|
7171
|
-
.prepare("INSERT INTO operating_mode_event (mode, kind, actor, at) VALUES (?, 'expired-closed', 'the-clock', ?)")
|
|
7172
|
-
.run(Number(row["id"]), now.toISOString());
|
|
7173
|
-
this.reconcileIntentsForMode(String(row["repo"]), null, now);
|
|
7174
|
-
});
|
|
7167
|
+
// A page visit only reads (docs/plans/get-side-effects.md): it sees no mode, and the background pass or the
|
|
7168
|
+
// next path that acts closes it.
|
|
7169
|
+
if (!inReadOnlyRequest())
|
|
7170
|
+
this.closeExpiredMode(Number(row["id"]), String(row["repo"]), now);
|
|
7175
7171
|
return null;
|
|
7176
7172
|
}
|
|
7177
7173
|
return {
|
|
@@ -7183,6 +7179,34 @@ export class Store {
|
|
|
7183
7179
|
absoluteExpiry: String(row["absolute_expiry"]),
|
|
7184
7180
|
};
|
|
7185
7181
|
}
|
|
7182
|
+
/** Close every operating mode past its expiry (the background pass). Returns how many were closed. */
|
|
7183
|
+
closeExpiredModes(now) {
|
|
7184
|
+
// The same rows activeMode would close: unrevoked, signed by a standing approver, past their expiry.
|
|
7185
|
+
const rows = this.db.prepare(`SELECT operating_mode.id, operating_mode.repo, operating_mode.absolute_expiry FROM operating_mode
|
|
7186
|
+
JOIN approver ON approver.name = operating_mode.signed_by AND approver.revoked_at IS NULL AND approver.role = 'approver'
|
|
7187
|
+
WHERE operating_mode.revoked_at IS NULL`).all();
|
|
7188
|
+
let closed = 0;
|
|
7189
|
+
for (const row of rows) {
|
|
7190
|
+
if (Date.parse(String(row["absolute_expiry"])) > now.getTime())
|
|
7191
|
+
continue;
|
|
7192
|
+
this.closeExpiredMode(Number(row["id"]), String(row["repo"]), now);
|
|
7193
|
+
closed++;
|
|
7194
|
+
}
|
|
7195
|
+
return closed;
|
|
7196
|
+
}
|
|
7197
|
+
closeExpiredMode(id, repo, now) {
|
|
7198
|
+
this.transact(() => {
|
|
7199
|
+
const closed = this.db
|
|
7200
|
+
.prepare("UPDATE operating_mode SET revoked_at = ?, revoked_by = ?, revoke_reason = 'expired' WHERE id = ? AND revoked_at IS NULL")
|
|
7201
|
+
.run(now.toISOString(), "the-clock", id);
|
|
7202
|
+
if (Number(closed.changes) !== 1)
|
|
7203
|
+
return;
|
|
7204
|
+
this.db
|
|
7205
|
+
.prepare("INSERT INTO operating_mode_event (mode, kind, actor, at) VALUES (?, 'expired-closed', 'the-clock', ?)")
|
|
7206
|
+
.run(id, now.toISOString());
|
|
7207
|
+
this.reconcileIntentsForMode(repo, null, now);
|
|
7208
|
+
});
|
|
7209
|
+
}
|
|
7186
7210
|
/** Sign (or renew) a mode: closes any predecessor with a typed event and
|
|
7187
7211
|
* ATOMICALLY reconciles the repo's nonterminal merge intents to the new
|
|
7188
7212
|
* posture (F2). The caller has already run the password ceremony. */
|
package/dist/subagent-work.js
CHANGED
|
@@ -235,6 +235,9 @@ function carryOut(store, flow, definition, stage, card, mate, context, answer, n
|
|
|
235
235
|
}
|
|
236
236
|
if (answer.action === "cant") {
|
|
237
237
|
event("handled", `Couldn't handle “${card.title}”: ${answer.note}`);
|
|
238
|
+
// A zone that answers whoever asked tells them it couldn't, in its own words when it gave some: they always hear back.
|
|
239
|
+
if (stage.reply === true)
|
|
240
|
+
replyToAsker(store, flow, card, mate, answer.note || (answer.reason === "" ? `${context.name} couldn't handle this.` : `${context.name} couldn't handle this: ${answer.reason}`), now);
|
|
238
241
|
if (stage.onFail !== null)
|
|
239
242
|
store.moveFlowCard(card.id, { to: stage.onFail, outcome: "fail", actor, note: answer.note === "" ? null : note(answer.note), expectEntry: card.entry }, now);
|
|
240
243
|
else
|
package/dist/task-status.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type TaskStatus = {
|
|
|
43
43
|
key: NeedKey;
|
|
44
44
|
action: NeedAction;
|
|
45
45
|
} | null;
|
|
46
|
+
/** The approved check as the Checks row shows it (`approvedCheckStateOf`): the road reads this, never the row's words. */
|
|
47
|
+
check?: ApprovedCheckState | null;
|
|
46
48
|
};
|
|
47
49
|
/** `reviewing`: the one automatic review (build-review.ts) is reading a finished build; it reads Building, not a headline of its own. */
|
|
48
50
|
export type TaskStage = "queued" | "planning" | "needs-you" | "waiting" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
@@ -61,6 +63,8 @@ export type ChecksFact = {
|
|
|
61
63
|
run: number;
|
|
62
64
|
head: string;
|
|
63
65
|
} | null;
|
|
66
|
+
/** Why it can't be read, when that is the project's check command approved again since it ran. */
|
|
67
|
+
cause?: "command-changed" | null;
|
|
64
68
|
};
|
|
65
69
|
/** What checks Off leaves for a release: `pending` until a passing full release check contains the commit, then
|
|
66
70
|
* `covered`. `running`: a check is waiting for its batch or running on this commit. */
|
|
@@ -70,6 +74,25 @@ export declare function releaseStateOf(checks: Pick<ChecksFact, "status" | "leve
|
|
|
70
74
|
/** How the project check stands for requirements that rest on it, read now: `covered` once a check passed on this
|
|
71
75
|
* commit (its own, a follow-up, or a release check containing it), `pending` while checks Off wait for a release. */
|
|
72
76
|
export declare function checkBackingOf(checks: Pick<ChecksFact, "status" | "level" | "release" | "running" | "batch"> | null | undefined): ReleaseState | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Did the approved check pass for this result? The ONE answer the status headline and sentence, the Project checks
|
|
79
|
+
* row, the road's Checks step and the result page all read, so none of them can say "passed" while another says the
|
|
80
|
+
* check couldn't be read. Two records speak to it: the machine verdict sealed when the build finished, and the saved
|
|
81
|
+
* check receipt read now (with any follow-up check on the same commit). Fail closed: the check passed only when the
|
|
82
|
+
* receipt read now says so. A verdict that recorded a pass whose receipt is missing, says nothing ran, or can't be
|
|
83
|
+
* read is unreadable; a verdict that recorded a failure stays failed unless a later check on the same commit passed.
|
|
84
|
+
* `checks` undefined or null: nothing was read. Returns the check as every surface shows it (null: nothing to show).
|
|
85
|
+
*/
|
|
86
|
+
export declare function approvedCheckOf(verdict: string | null | undefined, reasons: readonly string[], checks: ChecksFact | null | undefined): ChecksFact | null;
|
|
87
|
+
/** How the approved check reads, in one word every surface shares (from `approvedCheckOf`'s answer): `running` while a
|
|
88
|
+
* check is underway on the commit, else what it recorded. `unreadable`: it couldn't be read or the records disagree. */
|
|
89
|
+
export type ApprovedCheckState = "passed" | "failed" | "running" | "not-run" | "off" | "unreadable";
|
|
90
|
+
export declare function approvedCheckStateOf(checks: Pick<ChecksFact, "status" | "level" | "release" | "running" | "batch"> | null | undefined): ApprovedCheckState | null;
|
|
91
|
+
/** A saved check that can't be read, in the words every surface uses. */
|
|
92
|
+
export declare const CHECK_UNREADABLE = "The check result couldn't be read.";
|
|
93
|
+
export declare const CHECK_UNREADABLE_ASK = "The check result couldn't be read. Run checks on it, or open the result to see why.";
|
|
94
|
+
/** A saved check whose files are intact but whose command was approved again since it ran. */
|
|
95
|
+
export declare const CHECK_COMMAND_CHANGED = "The check command changed since this check; run checks again.";
|
|
73
96
|
/** What stands between a finished result and Complete (`completionBlockersOf`): each with the exact next step. */
|
|
74
97
|
export type CompletionBlocker = {
|
|
75
98
|
key: "check-failed" | "check-missing" | "criteria" | "high";
|
|
@@ -224,7 +247,7 @@ export declare function requirementsOf(matrix: readonly RequirementRow[] | null
|
|
|
224
247
|
* the rest); an unresolved HIGH review finding. A release check that covers the commit counts as passed. */
|
|
225
248
|
export declare function completionBlockersOf(f: {
|
|
226
249
|
report: boolean;
|
|
227
|
-
checks: Pick<ChecksFact, "status" | "exitCode" | "level" | "release" | "running" | "batch"> | null;
|
|
250
|
+
checks: Pick<ChecksFact, "status" | "exitCode" | "level" | "release" | "running" | "batch" | "cause"> | null;
|
|
228
251
|
checkRequired: boolean;
|
|
229
252
|
matrix: readonly RequirementRow[] | null | undefined;
|
|
230
253
|
verdict: string | null | undefined;
|
|
@@ -235,6 +258,10 @@ export declare function completionBlockersOf(f: {
|
|
|
235
258
|
export declare function unverifiedWhenRefuted(requirements: NonNullable<TaskStatusFacts["requirements"]> | null, verdict: string | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
|
|
236
259
|
/** Saved evidence health from the receipt's artifact list and its caveats. */
|
|
237
260
|
export declare function evidenceOf(receipt: AssignmentSnapshot["receipt"], problems?: readonly string[]): NonNullable<TaskStatusFacts["evidence"]> | null;
|
|
261
|
+
/** A saved check reading as the shared fact (head, level, a follow-up running, a batch, release coverage). */
|
|
262
|
+
export declare function checksFactOf(checks: NonNullable<AssignmentSnapshot["receipt"]>["checks"], head: string | null): ChecksFact;
|
|
263
|
+
/** A receipt's approved check, as every surface reads it (`approvedCheckOf` over its verdict and its saved check). */
|
|
264
|
+
export declare function receiptChecksOf(receipt: NonNullable<AssignmentSnapshot["receipt"]>): ChecksFact | null;
|
|
238
265
|
/** The status facts of one assignment, as every surface reads them. */
|
|
239
266
|
export declare function assignmentStatusFacts(assignment: AssignmentSnapshot, options?: {
|
|
240
267
|
work?: {
|