toolroll 0.9.7 → 0.9.9

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 (97) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/dist/assignment-adapters.d.ts +1 -0
  3. package/dist/assignment-adapters.js +2 -0
  4. package/dist/assignment-brief.d.ts +2 -0
  5. package/dist/assignment-brief.js +2 -1
  6. package/dist/assignment-presentation.js +3 -1
  7. package/dist/assignment-status.d.ts +1 -1
  8. package/dist/assignment-ui.js +5 -0
  9. package/dist/assignment.d.ts +15 -0
  10. package/dist/assignment.js +56 -9
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +18 -18
  13. package/dist/browser-crew.js +4 -0
  14. package/dist/browser-workspace.d.ts +40 -0
  15. package/dist/build-review.d.ts +38 -0
  16. package/dist/build-review.js +341 -0
  17. package/dist/builder.d.ts +2 -0
  18. package/dist/builder.js +114 -8
  19. package/dist/chat-controls.d.ts +1 -1
  20. package/dist/chat-controls.js +1 -1
  21. package/dist/check-levels-ui.d.ts +5 -0
  22. package/dist/check-levels-ui.js +16 -2
  23. package/dist/check-progress.js +4 -3
  24. package/dist/checkout-cleanup.d.ts +40 -2
  25. package/dist/checkout-cleanup.js +129 -9
  26. package/dist/cli.js +2 -0
  27. package/dist/desktop-update.d.ts +52 -0
  28. package/dist/desktop-update.js +147 -5
  29. package/dist/dispatch.js +2 -1
  30. package/dist/dispose.d.ts +4 -3
  31. package/dist/dispose.js +12 -4
  32. package/dist/evidence.d.ts +6 -0
  33. package/dist/evidence.js +8 -0
  34. package/dist/exec.d.ts +5 -0
  35. package/dist/exec.js +30 -4
  36. package/dist/first-run.d.ts +1 -1
  37. package/dist/first-run.js +8 -6
  38. package/dist/invoke.js +5 -0
  39. package/dist/lead-context.d.ts +1 -1
  40. package/dist/lead-context.js +4 -2
  41. package/dist/lead-status.d.ts +10 -1
  42. package/dist/lead-status.js +21 -24
  43. package/dist/modes.d.ts +3 -1
  44. package/dist/modes.js +3 -1
  45. package/dist/needs-you.d.ts +55 -0
  46. package/dist/needs-you.js +83 -0
  47. package/dist/operate.d.ts +2 -0
  48. package/dist/operate.js +231 -36
  49. package/dist/plane-review.js +4 -0
  50. package/dist/process-custody.js +8 -0
  51. package/dist/process-recovery-eligibility.d.ts +1 -0
  52. package/dist/project-learning.d.ts +3 -3
  53. package/dist/project-learning.js +28 -17
  54. package/dist/proof.d.ts +3 -1
  55. package/dist/proof.js +4 -2
  56. package/dist/provider.js +16 -0
  57. package/dist/result-completion.d.ts +4 -0
  58. package/dist/result-completion.js +11 -0
  59. package/dist/retention-ui.d.ts +3 -1
  60. package/dist/retention-ui.js +4 -4
  61. package/dist/retention.d.ts +13 -6
  62. package/dist/retention.js +50 -12
  63. package/dist/review-context.d.ts +3 -0
  64. package/dist/review-context.js +45 -5
  65. package/dist/review-switch.d.ts +41 -0
  66. package/dist/review-switch.js +88 -0
  67. package/dist/reviewer.d.ts +24 -2
  68. package/dist/reviewer.js +49 -1
  69. package/dist/run-closeout.d.ts +89 -0
  70. package/dist/run-closeout.js +302 -0
  71. package/dist/serve.d.ts +15 -1
  72. package/dist/serve.js +158 -18
  73. package/dist/setup-guide.d.ts +2 -2
  74. package/dist/shared-deps.d.ts +94 -0
  75. package/dist/shared-deps.js +365 -0
  76. package/dist/storage.js +2 -0
  77. package/dist/store.d.ts +72 -1
  78. package/dist/store.js +258 -31
  79. package/dist/surface.js +5 -1
  80. package/dist/task-status.d.ts +22 -5
  81. package/dist/task-status.js +45 -24
  82. package/dist/telegram-progress.js +2 -1
  83. package/dist/telegram-status.js +3 -3
  84. package/dist/test-temp.d.ts +19 -0
  85. package/dist/test-temp.js +98 -0
  86. package/dist/toolroll-update-ui.js +7 -2
  87. package/dist/toolroll-update.d.ts +21 -1
  88. package/dist/toolroll-update.js +135 -5
  89. package/dist/work-index.d.ts +11 -0
  90. package/dist/work-index.js +103 -16
  91. package/dist/work-summary.d.ts +3 -1
  92. package/dist/work-summary.js +2 -1
  93. package/dist/workspace-ui.d.ts +3 -1
  94. package/dist/workspace-ui.js +10 -9
  95. package/dist/worktree.d.ts +16 -0
  96. package/dist/worktree.js +47 -0
  97. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -71,8 +71,9 @@ import { hasDisguisedText } from './decision.js';
71
71
  import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequestBlocker, publishingOf, saveMergeSettings, savePublishing, newestPullRequestOf, MERGE_METHODS } from './pull-request-flow.js';
72
72
  import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
73
73
  import { assignmentCatchUp } from './assignment-brief.js';
74
- import { assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
74
+ import { assignmentActionHref, assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
75
75
  import { assignmentStageOf, pullRequestFactOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
76
+ import { NEEDS } from './needs-you.js';
76
77
  import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
77
78
  import { ledgerBody } from "./ledger-view.js";
78
79
  import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
@@ -227,13 +228,14 @@ import { STORAGE_CSS, storageHtml } from "./storage-ui.js";
227
228
  import { bytesWords, parseCleanup } from "./storage.js";
228
229
  import { checkoutPlan, cleanCheckouts, discardCheckout, previewDigest } from "./checkout-cleanup.js";
229
230
  import { UPDATES_CSS, newerThan, updateStepsHtml, updatesHtml, updatesScript } from "./toolroll-update-ui.js";
230
- import { abandonRuntimeUpdate, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate, requestRuntimeUpdateCancel, runningWorkWords, runtimeUpdateStatus, runtimeUpdateTerminal, currentRuntime } from "./toolroll-update.js";
231
+ import { abandonRuntimeUpdate, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate, requestRuntimeUpdateCancel, runningWorkWords, runtimeUpdateStatus, runtimeUpdateTerminal, currentRuntime, releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
231
232
  import { latestVersionNow } from "./releases.js";
232
- import { RETENTION_KINDS, lastSweepAt, parsePeriod, retentionPlan } from "./retention.js";
233
+ import { RETENTION_KINDS, lastSweepAt, parsePeriod, periodChoices, retentionPlan } from "./retention.js";
233
234
  import { EXPORT_CSS, dataExportHtml } from "./export-ui.js";
234
235
  import { buildExport, exportZip } from "./export.js";
235
236
  import { PROJECT_DELETE_CSS, projectDeleteConfirmHtml, projectSettingsHtml } from "./project-delete-ui.js";
236
237
  import { parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, projectConcurrency, saveProjectConcurrency } from "./project-concurrency.js";
238
+ import { buildReviewOf, findingWords } from "./review-switch.js";
237
239
  import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
238
240
  import { POLICY_CSS, policyHtml } from "./policy-ui.js";
239
241
  import { checkPolicy, parseList, policyParts } from "./policy.js";
@@ -1695,7 +1697,7 @@ export function createDecisionServer(options) {
1695
1697
  }
1696
1698
  const read = new Set(["/settings/flows", "/settings/skills", "/settings/knowledge", "/settings", "/settings/learning", "/recipes", "/recipes/run", "/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task", "/recipes/preview", "/recipes/export", "/", "/inbox", "/work", "/projects", "/people", "/ledger", "/ledger/export", "/next", "/board", "/tasks", "/tasks/new", "/runs", "/review", "/done", "/routines", "/menu"]);
1697
1699
  const write = new Set(["/settings/flows/on", "/settings/skills/import", "/settings/skills/change", "/settings/skills/revise", "/settings/knowledge/change", "/settings/knowledge/refresh", "/settings/learning/change", "/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/projects/select", "/tasks/add", "/routines/add"]);
1698
- const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|stop|resume-arm|resume)$");
1700
+ const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
1699
1701
  const resource = request.method === "GET"
1700
1702
  ? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/export|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path)
1701
1703
  : /^\/d\/[0-9]{1,15}\/answer$/.test(path) || /^\/routines\/[0-9]{1,15}\/(approve|refresh|pause|resume|run-now)$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path) || path === "/flows/import" || /^\/flows\/[0-9]{1,15}\/(save|cards|archive|scripts)$/.test(path) || /^\/flows\/[0-9]{1,15}\/cards\/[0-9]{1,15}\/(move|decide|cancel|comment|assign|watch)$/.test(path) || /^\/flows\/[0-9]{1,15}\/triggers(\/[0-9]{1,15}\/(pause|resume|remove|check|press|renew|secret|share|unshare))?$/.test(path) || /^\/r\/[0-9]{1,15}\/(note|comment|revise|draft-repair|checks|add-tests)$/.test(path);
@@ -3884,6 +3886,11 @@ export function createDecisionServer(options) {
3884
3886
  const databaseFile = store.databaseFile();
3885
3887
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
3886
3888
  return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
3889
+ // A record it cannot release is shown as it is; the page never fails over it.
3890
+ try {
3891
+ releaseStalledUpdate(dirname(databaseFile), clock());
3892
+ }
3893
+ catch { /* shown as last saved */ }
3887
3894
  const status = runtimeUpdateStatus(dirname(databaseFile));
3888
3895
  if (url.searchParams.get("fragment") === "steps")
3889
3896
  return respond(response, 200, "text/html; charset=utf-8", status.journal ? updateStepsHtml(status.journal, status.running) : `<div id="update-live" data-done="1"></div>`);
@@ -3898,7 +3905,7 @@ export function createDecisionServer(options) {
3898
3905
  if (url.pathname === "/settings/retention") {
3899
3906
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
3900
3907
  return refuse(response, who, 403, "An instance operator sets retention.", "/settings");
3901
- const view = { periods: store.retentionPeriods(), next: retentionPlan(store, evidenceRoot, now).counts, lastSweep: lastSweepAt(store), csrf: who.session.csrf };
3908
+ const view = { periods: store.retentionPeriods(), chosen: Object.keys(store.retentionChosen()), next: retentionPlan(store, evidenceRoot, now).counts, lastSweep: lastSweepAt(store), csrf: who.session.csrf };
3902
3909
  return sendScreen(response, 200, screen("Retention", `<p><a href="/settings">Settings</a></p><h1>Retention</h1>${retentionHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
3903
3910
  }
3904
3911
  // Settings → Storage: what task checkouts use, cleaning up now (the preview behind the password), and when a
@@ -3933,6 +3940,7 @@ export function createDecisionServer(options) {
3933
3940
  const quick = liveQuickCommand(store, repo);
3934
3941
  const html = checkSettingsHtml({ repo, name: projectName(repo), csrf: who.session.csrf, canChange: who.role === "approver" && !store.isDemo(),
3935
3942
  level: projectCheckLevel(store, repo).level, full: store.liveVerifyCommand(repo), quick, suggestion: quick === null ? suggestQuickCommand(repo) : null,
3943
+ review: (({ on, source }) => ({ on, source }))(store.reviewSwitch(repo, clock())),
3936
3944
  said: url.searchParams.get("said"), problem: url.searchParams.get("problem") });
3937
3945
  return sendScreen(response, 200, screen("Checks", `<p><a href="/projects">Projects</a></p><h1>Checks</h1>${html}`, { chrome: chromeFor(project, "projects") }));
3938
3946
  }
@@ -4887,6 +4895,8 @@ export function createDecisionServer(options) {
4887
4895
  notices.unshift(DEMO_BANNER);
4888
4896
  if (s.chrome.modeBanner)
4889
4897
  notices.push(s.chrome.modeBanner.words);
4898
+ if (s.chrome.updateWaiting)
4899
+ notices.push(s.chrome.updateWaiting.words);
4890
4900
  let crew = { crew: [], crewTruncated: false };
4891
4901
  try {
4892
4902
  const project = s.chrome.active === 'chat' ? null : s.chrome.project;
@@ -5016,14 +5026,20 @@ export function createDecisionServer(options) {
5016
5026
  const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
5017
5027
  const counts = [
5018
5028
  { key: "working", label: "Working now", value: agents.length, href: "/work?view=running" },
5019
- { key: "waiting", label: "Waiting on you", value: needs.items.filter(one => one.assignmentState === "needs-decision").length, href: "/work?view=needs-you" },
5020
- { key: "ready", label: "Ready to review", value: needs.items.filter(one => one.assignmentState === "ready-to-check").length, href: "/work?view=needs-you" },
5029
+ // Each by its shared headline, as the Tasks list words it: a result whose checks failed waits on you, it isn't ready.
5030
+ { key: "waiting", label: "Waiting on you", value: needs.items.filter(one => one.status.label !== "Ready for review").length, href: "/work?view=needs-you" },
5031
+ { key: "ready", label: "Ready to review", value: needs.items.filter(one => one.status.label === "Ready for review").length, href: "/work?view=needs-you" },
5021
5032
  { key: "done", label: "Done this week", value: done.items.filter(one => (one.completion?.at ?? one.updatedAt) >= weekAgo).length, href: "/work?view=completed" },
5022
5033
  ];
5023
- const tabOf = (state) => state === "needs-decision" ? "needs-you" : state === "ready-to-check" ? "ready" : state === "working" || state === "checking" ? "running" : "finished";
5024
- const catchUp = all.items.filter(one => tabOf(one.assignmentState) !== "finished" || one.updatedAt >= weekAgo).slice(0, 40).map(one => ({
5025
- id: one.rootId, title: one.title, href: taskHref(one.rootId), project: one.repo === null ? null : projectName(one.repo), tab: tabOf(one.assignmentState),
5034
+ const tabOf = (one) => one.assignmentState === "ready-to-check" ? one.status.label === "Ready for review" ? "ready" : "needs-you"
5035
+ : one.assignmentState === "needs-decision" ? "needs-you" : one.assignmentState === "working" || one.assignmentState === "checking" ? "running" : "finished";
5036
+ const catchUp = all.items.filter(one => tabOf(one) !== "finished" || one.updatedAt >= weekAgo).slice(0, 40).map(one => ({
5037
+ id: one.rootId, title: one.title, href: taskHref(one.rootId), project: one.repo === null ? null : projectName(one.repo), tab: tabOf(one),
5026
5038
  label: one.status.label, tone: one.status.tone, detail: one.status.detail, at: one.updatedAt,
5039
+ action: (() => {
5040
+ const href = one.status.label === "Needs you" && one.primaryAction !== null ? browserWorkActionHref(one) : null;
5041
+ return href === null ? null : { label: one.primaryAction.label, href };
5042
+ })(),
5027
5043
  }));
5028
5044
  const windows = who.via === "cookie" && store.isInstanceOperator(who.name) ? limitsView(store.providerLimits(), [], { project: projectName, teammate: id => `Teammate ${id}` }, now) : null;
5029
5045
  return {
@@ -5103,6 +5119,14 @@ export function createDecisionServer(options) {
5103
5119
  return {};
5104
5120
  return { update: { version: update.release.version, security: update.release.security, href: "/settings#updates", dismissHref: "/settings/updates/dismiss" } };
5105
5121
  })(),
5122
+ ...(() => {
5123
+ const databaseFile = store.databaseFile();
5124
+ // Settings → Updates says a `toolroll update` in full; every other page (and the app's update everywhere) names it once.
5125
+ if (!actor || databaseFile === null || !store.isInstanceOperator(actor))
5126
+ return {};
5127
+ const waiting = waitingUpdate(databaseFile, run => store.stopQuiescenceProblem(run) !== null, clock());
5128
+ return waiting === null || (!waiting.app && facts?.returnTo?.startsWith("/settings/updates")) ? {} : { updateWaiting: { words: waiting.short } };
5129
+ })(),
5106
5130
  ...(liveMode === null || liveModeTerms === null
5107
5131
  ? {}
5108
5132
  : {
@@ -5187,7 +5211,7 @@ export function createDecisionServer(options) {
5187
5211
  const reviewFactsFor = (runId) => reviewFactsOf(store.reviewRetryStateOf(runId), runner => {
5188
5212
  const one = store.getRunner(runner)?.runner;
5189
5213
  return one !== undefined && runnerAlive(one, clock());
5190
- });
5214
+ }, buildReviewOf(store, runId)?.state === "pending");
5191
5215
  /** Keep detailed receipt diagnostics; shared assignment reads own readiness. */
5192
5216
  function freshAssignment(assignment, receipt) {
5193
5217
  if (assignment === null)
@@ -7264,7 +7288,9 @@ export function createDecisionServer(options) {
7264
7288
  if (!authenticateApprover(store, who.name, body.get("password") ?? "").ok)
7265
7289
  return back("problem", "Enter your Toolroll password to change retention.");
7266
7290
  const before = store.retentionPeriods();
7267
- const chosen = RETENTION_KINDS.map(({ kind }) => ({ kind, days: body.has(kind) ? parsePeriod(body.get(kind) ?? "") : before[kind] }));
7291
+ // Only what the page offers (or the period already set): a hand-made value doesn't save.
7292
+ const offered = (kind, days) => days !== undefined && periodChoices(kind, before[kind]).includes(days) ? days : undefined;
7293
+ const chosen = RETENTION_KINDS.map(({ kind }) => ({ kind, days: body.has(kind) ? offered(kind, parsePeriod(body.get(kind) ?? "")) : before[kind] }));
7268
7294
  if (chosen.some(one => one.days === undefined))
7269
7295
  return back("problem", "Choose a period for each kind of data.");
7270
7296
  const changed = chosen.filter(one => one.days !== before[one.kind]);
@@ -7354,6 +7380,12 @@ export function createDecisionServer(options) {
7354
7380
  if (!authenticateApprover(store, who.name, body.get("password") ?? "", repo).ok)
7355
7381
  return back("problem", "Enter your Toolroll password to change checks.");
7356
7382
  const act = body.get("act");
7383
+ // The automatic review switch: the policy log keeps before → after.
7384
+ if (act === "review") {
7385
+ const on = body.get("on") === "1";
7386
+ store.setReviewSwitch(repo, on, who.name, now);
7387
+ return back("said", on ? "Automatic review is on." : "Automatic review is off.");
7388
+ }
7357
7389
  if (act === "level") {
7358
7390
  const level = body.get("level");
7359
7391
  if (!isCheckLevel(level))
@@ -8772,7 +8804,38 @@ export function createDecisionServer(options) {
8772
8804
  if (attendAct !== null) {
8773
8805
  return attendMutation(response, who, attendAct.taskId, attendAct.verb, body, now);
8774
8806
  }
8775
- const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|stop|resume-arm|resume)$");
8807
+ const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
8808
+ if (act !== null && act.verb === "confirm-stopped") {
8809
+ // Confirming a finished build stopped is an approver's act behind the password, typed again — the same
8810
+ // record as `toolroll run settle`, refused while anything of the run may still be running.
8811
+ const back = taskHref(act.taskId);
8812
+ if (who.via !== "cookie" || who.role !== "approver")
8813
+ return refuse(response, who, 403, "Only an approver can confirm a build stopped.", back);
8814
+ const ref = store.lookupRef(act.taskId);
8815
+ if (ref === null || !visible(ref.repo))
8816
+ return refuse(response, who, 404, "no such task", "/tasks");
8817
+ const named = (body.get("run") ?? "").trim();
8818
+ const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
8819
+ const runTask = run === null ? null : store.externalIdFor(run.taskRef);
8820
+ if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
8821
+ return taskScreen(response, who, act.taskId, "That build isn't part of this task.", 409);
8822
+ }
8823
+ if (!authenticateApprover(store, who.name, body.get("token") ?? "", ref.repo).ok || !store.accountCanAccess(who.name, ref.repo)) {
8824
+ return taskScreen(response, who, act.taskId, "That password didn't match. Nothing changed.", 403);
8825
+ }
8826
+ // When Toolroll can't check at all, the approver also says they checked: never a one-click confirmation.
8827
+ if (store.stopQuiescenceFact(run.id)?.kind === "unknown" && body.get("checked") !== "yes") {
8828
+ return taskScreen(response, who, act.taskId, `Make sure nothing from build #${run.id} is running, then tick the box to confirm.`, 409);
8829
+ }
8830
+ const settled = store.settleRunWitnessesByApprover({ runId: run.id, by: who.name, why: "Confirmed in the console that nothing from this build is running." }, now);
8831
+ if (!settled.ok) {
8832
+ return taskScreen(response, who, act.taskId, settled.reason === "alive" || settled.reason === "still-running"
8833
+ ? `Something from build #${run.id} may still be running, so it can't be confirmed stopped yet.` : `Build #${run.id} can't be confirmed stopped.`, 409);
8834
+ }
8835
+ bustBadge();
8836
+ const to = resultReturnTarget(body.get("return"), run.id);
8837
+ return redirect(response, body.get("return") ? to : back);
8838
+ }
8776
8839
  if (act !== null && act.verb === "merge") {
8777
8840
  // Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
8778
8841
  // result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
@@ -11423,6 +11486,7 @@ export function createDecisionServer(options) {
11423
11486
  comments: store.liveDiffComments(run.id).filter(isRevisionFeedback),
11424
11487
  pastComments: store.allDiffComments(run.id).filter(one => one.reviewerRun === null && (one.consumedBy !== null || one.supersededBy !== null)),
11425
11488
  reviewerFindings: store.allDiffComments(run.id).filter(one => one.reviewerRun !== null),
11489
+ automaticReview: buildReviewOf(store, run.id),
11426
11490
  revisions: store.revisionsFromRun(run.id).flatMap(one => {
11427
11491
  const child = store.getTask(one.id);
11428
11492
  if (child === null || !visible(store.lookupRef(one.id)?.repo ?? null))
@@ -11934,6 +11998,7 @@ function criterionMatrixHtml(matrix, options = {}) {
11934
11998
  (state !== "pass" || detail.length === 0 ? "" : `<div class="requirement-evidence-group"><strong>Verification notes</strong><ul>${detail.map(line => `<li>${escape(line)}</li>`).join("")}</ul></div>`) +
11935
11999
  reviewDetails +
11936
12000
  (coverage === undefined ? "" : `<p class="meta"${coverage.state === "gap" ? "" : ` data-context-coverage="${escape(coverage.state)}"`}>Review context: ${escape(coverageStateWords(coverage))}</p>`) +
12001
+ (coverage?.assets === undefined ? "" : `<p class="meta" data-context-assets>${escape(coverage.assets)}</p>`) +
11937
12002
  `<p class="meta">Requirement ID: <code>${escape(row.id)}</code></p></div></details></li>`;
11938
12003
  }).join("");
11939
12004
  return `<div class="result-section criterion-matrix"><strong>Requirements · ${matrix.length}</strong><ol class="requirement-list">${rows}</ol></div>`;
@@ -15493,6 +15558,9 @@ function shell(title, body, options = {}) {
15493
15558
  : `<div class="banner"><span class="badge badge-running">mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`) +
15494
15559
  (chrome.signIn ?? []).map(one => `<div class="banner sign-in-banner" data-sign-in="${escape(one.provider)}"><strong>${escape(one.title)}</strong> \u00b7 run <code>${escape(one.command)}</code> on this computer, then resume.${one.detail === "" ? "" : ` ${escape(one.detail)}`}` +
15495
15560
  `<form method="post" action="${escape(one.resumeHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><button type="submit">${escape(one.resumeLabel)}</button></form></div>`).join("") +
15561
+ (chrome.updateWaiting === undefined
15562
+ ? ""
15563
+ : `<div class="banner update-waiting" role="status">${escape(chrome.updateWaiting.words)} \u00b7 <a href="/settings/updates">Update status</a></div>`) +
15496
15564
  (chrome.update === undefined
15497
15565
  ? ""
15498
15566
  : `<div class="banner update-banner" data-update="${escape(chrome.update.version)}">${escape(updateNoticeWords(chrome.update))} \u00b7 <a href="${escape(chrome.update.href)}">What's new</a>` +
@@ -16506,7 +16574,7 @@ function taskChatApproval(focus, csrf) {
16506
16574
  `<p class="chat-plan-outcome">${escape(oneLineOf(scope.goal, 200))}</p>` +
16507
16575
  `<p class="chat-plan-facts">${facts.map(escape).join(" · ")}</p>` +
16508
16576
  `<details class="chat-approval">` +
16509
- `<summary data-primary-action><span class="button-link">Review plan</span></summary>` +
16577
+ `<summary data-primary-action><span class="button-link">Approve plan</span></summary>` +
16510
16578
  `<form method="post" action="${taskHref(focus.executionId)}/approve" class="chat-approval-form approve-form">` +
16511
16579
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
16512
16580
  `<input type="hidden" name="nonce" value="${escape(approval.nonce)}">` +
@@ -16537,7 +16605,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
16537
16605
  const approvalContent = (inert && focus.approval !== null && focus.plan !== "requested"
16538
16606
  ? `<section class="card chat-action-card"><span class="eyebrow">approval ready</span><h2>Finish the current chat response first</h2><p class="meta">The secure approval step appears here as soon as this response lands.</p></section>`
16539
16607
  : fragment && focus.approval !== null && focus.plan !== "requested"
16540
- ? `<section class="card chat-action-card chat-refresh-action"><a class="button-link" href="${escape(approvalHref)}" data-primary-action>Review plan</a></section>`
16608
+ ? `<section class="card chat-action-card chat-refresh-action"><a class="button-link" href="${escape(approvalHref)}" data-primary-action>Approve plan</a></section>`
16541
16609
  : taskChatApproval(focus, csrf));
16542
16610
  const approvalCard = focus.approval !== null && focus.dispatch?.action !== "approve-scope"
16543
16611
  ? `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approvalContent}</details>` : approvalContent;
@@ -20439,7 +20507,7 @@ function taskBodyParts(data) {
20439
20507
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20440
20508
  : "";
20441
20509
  const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : data.dispatch?.action === "approve-scope"
20442
- ? `<details class="task-plan-review"><summary data-primary-action><span class="button-link">Review plan</span></summary>${approveForm}</details>`
20510
+ ? `<details class="task-plan-review"><summary data-primary-action><span class="button-link">Approve plan</span></summary>${approveForm}</details>`
20443
20511
  : `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
20444
20512
  const optionsHtml = `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
20445
20513
  const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
@@ -20640,6 +20708,10 @@ function taskBodyParts(data) {
20640
20708
  status: data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null,
20641
20709
  statusHtml,
20642
20710
  approval: approvalHtml,
20711
+ confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
20712
+ ? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
20713
+ rebuild: data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
20714
+ ? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null,
20643
20715
  // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
20644
20716
  lead: [
20645
20717
  { key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
@@ -22080,6 +22152,13 @@ function resultPanelParts(detail, o) {
22080
22152
  const status = presentation?.status ?? resultHeadlineOf(receiptStatusOf(receipt));
22081
22153
  const stored = receiptStatusOf(receipt, null);
22082
22154
  const humanReview = manualReviewOnly(proof === null ? null : { ...proof, verdict: proof.verdict ?? "" });
22155
+ // Needs you: the action that resolves it comes first; Request changes stays beside it, never alone.
22156
+ // On the result itself, "Review result" would link here: accepting it is what resolves it.
22157
+ const need = needActionOf(current, taskStatus, o.csrf, o.returnTo, {
22158
+ accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`,
22159
+ // What Store.finalResultReason refuses: a verified, attested, accepted or published result.
22160
+ rebuildable: proof?.accepted == null && proof?.verdict !== "verified" && proof?.verdict !== "attested" && !(detail.publication !== null && ["pushed", "opened"].includes(detail.publication.state))
22161
+ });
22083
22162
  const directAssessment = proof?.matrix.some(row => row.assessment !== undefined) === true;
22084
22163
  const awaitingGoalReview = directAssessment && proof?.reasons.length === 1 && proof.reasons[0] === GOAL_ASSESSMENT_PENDING;
22085
22164
  const assessmentReasons = directAssessment ? new Set(proof.matrix.flatMap(row => row.review ? [`${row.review.author} ${row.review.judgement === "contradicts" ? "contradicts" : "needs more evidence for"} criterion "${row.id}": ${row.review.note}`] : [])) : new Set();
@@ -22115,6 +22194,16 @@ function resultPanelParts(detail, o) {
22115
22194
  if (current == null)
22116
22195
  attention.push(...receipt.caveats.filter(caveat => !historicalAssessmentReason(caveat) && !attention.some(problem => problem.includes(caveat))));
22117
22196
  attention.push(...(handoff?.followUps ?? []).map(one => `Follow-up: ${one}`));
22197
+ // The automatic review: a HIGH that did not send the work back, or a review
22198
+ // that could not finish, is the person's to see. (Said once when the
22199
+ // current assignment already carries it.)
22200
+ const automatic = detail.automaticReview ?? null;
22201
+ if (automatic !== null && automatic.sentBackAs === null) {
22202
+ const reviewed = [...(automatic.state === "not-reviewed" ? [`Not reviewed: ${automatic.reason ?? "the automatic review did not finish"}.`] : []), ...automatic.high.map(one => `Review: ${findingWords(one)}`)];
22203
+ for (const line of reviewed)
22204
+ if (!attention.includes(line))
22205
+ attention.push(line);
22206
+ }
22118
22207
  // Publication risks stay in the open (repair 2026-09-14); the routine
22119
22208
  // publication fact lives with the build details below.
22120
22209
  // With the shared status, the pull request is its own quiet row (its reason under Details).
@@ -22133,7 +22222,7 @@ function resultPanelParts(detail, o) {
22133
22222
  const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
22134
22223
  const youCheck = personChecks.length === 0 ? null : {
22135
22224
  lines: [...new Set(personChecks)],
22136
- accept: acceptable ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22225
+ accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
22137
22226
  };
22138
22227
  const youCheckHtml = youCheck === null ? "" :
22139
22228
  `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
@@ -22147,7 +22236,8 @@ function resultPanelParts(detail, o) {
22147
22236
  // account waits behind a disclosure in Summary so nothing is said twice.
22148
22237
  const conclusion = plainConclusionOf(receipt.summary ?? (run.outcome === "no-change" ? "The agent found that no repository change was needed." : "The build finished without a concise handoff."));
22149
22238
  const outcome = conciseOutcomeOf(conclusion);
22150
- const action = o.action === false ? "" : resultPrimaryAction(detail, o, prUrl);
22239
+ // A failing pull request keeps its repair draft beside the need's action.
22240
+ const action = o.action === false ? "" : need !== null ? resultNeedAction(need, o, detail.canAnnotate) + (detail.ciFailing && o.csrf !== "" ? resultPrimaryAction(detail, o, prUrl) : "") : resultPrimaryAction(detail, o, prUrl);
22151
22241
  // ---- summary: the deliverable first ----------------------------------
22152
22242
  const summaryParts = [];
22153
22243
  const report = receipt.report;
@@ -22189,6 +22279,10 @@ function resultPanelParts(detail, o) {
22189
22279
  ];
22190
22280
  if (agentAccount.length > 0)
22191
22281
  summaryParts.push(`<details class="result-notes" data-result-notes-agent><summary>What the agent reported</summary>${agentAccount.join("")}</details>`);
22282
+ // MEDIUM and LOW never block: suggested follow-ups, on request.
22283
+ if (automatic !== null && automatic.followUps.length > 0) {
22284
+ summaryParts.push(`<details class="result-notes" data-review-followups="${automatic.followUps.length}"><summary>Suggested follow-ups · ${automatic.followUps.length}</summary><ul class="result-changes">${automatic.followUps.map(one => `<li><span class="badge">${one.severity === "MEDIUM" ? "Medium" : "Low"}</span> <span class="mono">${escape(`${one.file}:${one.line}`)}</span> ${escape(one.scenario)}</li>`).join("")}</ul></details>`);
22285
+ }
22192
22286
  const evidenceSources = [
22193
22287
  ...(report !== null ? [report.ok ? `verified report${report.truncated ? " (shortened)" : ""}` : "report (unverifiable)"] : []),
22194
22288
  ...(patch !== null ? [patchOk ? `sealed diff${patch.truncated ? " (shortened)" : ""}` : "diff (unverifiable)"] : []),
@@ -22431,10 +22525,56 @@ function resultPanelParts(detail, o) {
22431
22525
  learning: detail.learning ?? "",
22432
22526
  request: detail.skillTest ? null : requestParts.join("\n"),
22433
22527
  canRequest: detail.canAnnotate && o.csrf !== "",
22528
+ need,
22434
22529
  requestQuiet: detail.canAnnotate && o.csrf !== "" && detail.comments.length === 0 && (detail.pastComments?.length ?? 0) === 0 && detail.revisions.length === 0,
22435
22530
  };
22436
22531
  return { html, panel };
22437
22532
  }
22533
+ /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
22534
+ * stopped behind the password. Null under every other headline. */
22535
+ export function needActionOf(assignment, status, csrf, returnTo, result) {
22536
+ if (assignment === null || status === null || status.headline !== "Needs you" || status.need == null)
22537
+ return null;
22538
+ const action = assignment.primaryAction;
22539
+ const rebuild = { href: null, confirm: null, rebuild: { action: `${taskHref(assignment.rootId)}/requeue` } };
22540
+ // Built to an earlier plan: Build again, in place (the task page's requeue).
22541
+ if (action?.code === "retry-task" && status.need.key === "rebuild" && csrf !== "")
22542
+ return { label: status.need.action.label, ...rebuild };
22543
+ if (result !== undefined) {
22544
+ if (status.need.key === "review-result" && !result.accepted && csrf !== "")
22545
+ return { label: result.humanReview ? "Accept result" : "Accept with exception", href: null, confirm: null,
22546
+ accept: { action: result.action, run: result.run, returnTo, note: result.humanReview ? null : "Why is this safe to accept?" } };
22547
+ // The task page sends the person here: never back to it. What this page can do leads — Accept
22548
+ // a check only a person can make, or Build again a result that may run again.
22549
+ const here = (action?.code === "open-result" || action?.code === "inspect-run") && action.target.runId === result.run;
22550
+ if (here && csrf !== "" && !result.accepted && result.humanReview)
22551
+ return { label: "Accept result", href: null, confirm: null, accept: { action: result.action, run: result.run, returnTo, note: null } };
22552
+ if (here && csrf !== "" && result.rebuildable === true)
22553
+ return { label: NEEDS.rebuild.action.label, ...rebuild };
22554
+ if (status.need.key === "review-result" || here)
22555
+ return { label: "Open the task", href: taskHref(assignment.rootId), confirm: null };
22556
+ }
22557
+ const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
22558
+ ? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
22559
+ return { label: status.need.action.label, href: assignmentActionHref(assignment) ?? taskHref(assignment.rootId), confirm };
22560
+ }
22561
+ /** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
22562
+ const needsCheck = (assignment) => assignment.need != null && "key" in assignment.need && assignment.need.key === "check-stopped";
22563
+ /** The server page's form of the same: the need's action first, Request changes as the quiet second. */
22564
+ function resultNeedAction(need, o, canRequest) {
22565
+ const control = need.accept != null
22566
+ ? `<form method="post" action="${escape(need.accept.action)}" class="accept-result"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${need.accept.run}"><input type="hidden" name="return" value="${escape(need.accept.returnTo)}">` +
22567
+ (need.accept.note === null ? "" : `<input type="text" name="note" maxlength="500" required placeholder="${escape(need.accept.note)}" aria-label="${escape(need.accept.note)}">`) +
22568
+ `<button type="submit" data-primary-action data-accept-result style="min-height:44px">${escape(need.label)}</button></form>`
22569
+ : need.rebuild != null
22570
+ ? `<form method="post" action="${escape(need.rebuild.action)}" class="rebuild"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><button type="submit" data-primary-action data-rebuild style="min-height:44px">${escape(need.label)}</button></form>`
22571
+ : need.confirm !== null
22572
+ ? `<form method="post" action="${escape(need.confirm.action)}" id="confirm-stopped" class="confirm-stopped"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${need.confirm.run}"><input type="hidden" name="return" value="${escape(need.confirm.returnTo)}">` +
22573
+ (need.confirm.checked === true ? `<label><input type="checkbox" name="checked" value="yes" required> Nothing from build #${need.confirm.run} is running</label>` : "") +
22574
+ `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><button type="submit" style="min-height:44px">${escape(need.label)}</button></form>`
22575
+ : `<a class="button-link" href="${escape(need.href ?? "#")}" data-primary-action>${escape(need.label)}</a>`;
22576
+ return `<div class="result-action" data-result-action="need">${control}${canRequest && o.csrf !== "" ? `<a class="result-feedback-link" href="#request-changes">Request changes</a>` : ""}</div>`;
22577
+ }
22438
22578
  /** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
22439
22579
  * first sentence, cut at a word when it runs past 180 characters. The
22440
22580
  * full conclusion stays available behind the Summary's disclosure. */
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
15
15
  } | null;
16
16
  /** Browser installation may write only this known file inside its project. */
17
17
  export declare function previewProjectInstructions(repo: string): {
18
+ content?: never;
18
19
  ok: false;
19
20
  message: string;
20
21
  plan?: never;
21
- content?: never;
22
22
  fingerprint?: never;
23
23
  installed?: never;
24
24
  } | {
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
30
30
  installed: boolean;
31
31
  };
32
32
  export declare function addProjectInstructions(repo: string, fingerprint: string): {
33
+ content?: never;
33
34
  ok: false;
34
35
  message: string;
35
36
  plan?: never;
36
- content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
@@ -0,0 +1,94 @@
1
+ export declare const DEPS_FOLDER = "deps";
2
+ /** A copy linked or made this recently is never removed by a clean-up: a lease may be about to link it. */
3
+ export declare const SHARED_GRACE_MS: number;
4
+ export type SharedCopy = {
5
+ key: string;
6
+ /** The copy's folder, holding node_modules and ready.json. */
7
+ dir: string;
8
+ repo: string;
9
+ /** sha256 of the lockfile and package.json it was installed from. */
10
+ lock: string;
11
+ node: string;
12
+ createdAt: string;
13
+ /** Last linked (ready.json's modification time). */
14
+ usedAt: string;
15
+ /** Its installed tree changed after it was made: never linked again. */
16
+ retired: boolean;
17
+ };
18
+ export declare function sharedDepsRoot(databaseFile: string): string;
19
+ /** A setup whose whole effect is node_modules: `npm ci` or `npm install`, with plain flags only. */
20
+ export declare function installsOnly(command: string): boolean;
21
+ /**
22
+ * The checkout's dependency identity: its npm lockfile and package.json, hashed together. null when there is no
23
+ * lockfile, or the lockfile links local folders (workspaces, `file:`), whose links would point into the shared
24
+ * folder instead of the checkout.
25
+ */
26
+ export declare function lockDigest(checkout: string): string | null;
27
+ /** The node a setup on this PATH would install with: version, platform and architecture (native modules differ). */
28
+ export declare function nodeIdentity(path?: string): string | null;
29
+ export declare function depsKey(repo: string, setupDigest: string, lock: string, node: string): string;
30
+ /** What a checkout would share: its key and the facts behind it, or null when it can't share. */
31
+ export declare function sharingFor(args: {
32
+ repo: string;
33
+ worktree: string;
34
+ setup: {
35
+ command: string;
36
+ digest: string;
37
+ };
38
+ }): {
39
+ key: string;
40
+ lock: string;
41
+ node: string;
42
+ } | null;
43
+ /** The shared node_modules for a key, when one is ready and unchanged since it was made. A changed one is retired. */
44
+ export declare function readyCopy(root: string, key: string): string | null;
45
+ /**
46
+ * The shared copy a checkout's node_modules links, or null (none, or its own install). A linked node_modules is a
47
+ * small real folder: one link per top-level entry of the shared copy and a note naming it. Tools that write a cache
48
+ * there (`node_modules/.vite`, `.cache`) write into the checkout's own folder, never the shared copy.
49
+ */
50
+ export declare function linkedKey(worktree: string, root: string): string | null;
51
+ /** Remove a checkout's linked node_modules (its links and caches, never what they point to). True when there was one. */
52
+ export declare function detachShared(worktree: string, root: string): boolean;
53
+ /**
54
+ * Put a shared copy into a checkout that has no node_modules: links (a junction per folder on Windows), else a
55
+ * copy-on-write clone the build owns. null: neither worked, and the caller runs the plain setup.
56
+ */
57
+ export declare function linkInto(worktree: string, shared: string): "link" | "clone" | null;
58
+ /**
59
+ * After the approved setup succeeded in a checkout with no copy for its key: move its node_modules into
60
+ * `deps/<key>` (one rename), make it read-only and link it back. Returns how the checkout now has it, or null when it
61
+ * kept its own install (a different volume, a setup that rewrote the lockfile, a race lost cleanly).
62
+ */
63
+ export declare function promoteInstall(args: {
64
+ root: string;
65
+ repo: string;
66
+ worktree: string;
67
+ key: string;
68
+ lock: string;
69
+ node: string;
70
+ setupDigest: string;
71
+ now: Date;
72
+ }): "link" | "clone" | null;
73
+ /** Every shared copy on disk, newest first. */
74
+ export declare function sharedCopies(root: string): SharedCopy[];
75
+ export type SharedUse = SharedCopy & {
76
+ checkouts: string[];
77
+ };
78
+ /** Which checkouts use each copy: those linking it, and those whose lockfile it was installed from. */
79
+ export declare function sharedUse(root: string, checkouts: readonly {
80
+ path: string;
81
+ repo: string;
82
+ }[]): SharedUse[];
83
+ /** Half-made copies under deps/ (a crash mid-promotion), old enough that nothing is still making them. */
84
+ export declare function sharedDebris(root: string, now: Date): string[];
85
+ /** What `storage clean` removes: copies no checkout uses and none linked within the grace period, plus debris. */
86
+ export declare function unusedShared(root: string, checkouts: readonly {
87
+ path: string;
88
+ repo: string;
89
+ }[], now: Date): {
90
+ copies: SharedUse[];
91
+ debris: string[];
92
+ };
93
+ /** Remove one copy or debris folder (read-only on disk, so made writable first). */
94
+ export declare function removeShared(path: string): boolean;