toolroll 0.9.8 → 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 (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/assignment-brief.d.ts +2 -0
  3. package/dist/assignment-brief.js +2 -1
  4. package/dist/assignment-presentation.js +3 -1
  5. package/dist/assignment-status.d.ts +1 -1
  6. package/dist/assignment-ui.js +5 -0
  7. package/dist/assignment.d.ts +9 -0
  8. package/dist/assignment.js +39 -7
  9. package/dist/browser/workspace.css +1 -1
  10. package/dist/browser/workspace.js +18 -18
  11. package/dist/browser-crew.js +4 -0
  12. package/dist/browser-workspace.d.ts +40 -0
  13. package/dist/builder.d.ts +2 -0
  14. package/dist/builder.js +114 -8
  15. package/dist/chat-controls.d.ts +1 -1
  16. package/dist/chat-controls.js +1 -1
  17. package/dist/check-progress.js +4 -3
  18. package/dist/checkout-cleanup.d.ts +40 -2
  19. package/dist/checkout-cleanup.js +129 -9
  20. package/dist/desktop-update.d.ts +52 -0
  21. package/dist/desktop-update.js +147 -5
  22. package/dist/evidence.d.ts +6 -0
  23. package/dist/evidence.js +8 -0
  24. package/dist/first-run.d.ts +1 -1
  25. package/dist/first-run.js +7 -5
  26. package/dist/lead-context.d.ts +1 -1
  27. package/dist/lead-context.js +4 -2
  28. package/dist/lead-status.d.ts +10 -1
  29. package/dist/lead-status.js +21 -24
  30. package/dist/needs-you.d.ts +55 -0
  31. package/dist/needs-you.js +83 -0
  32. package/dist/operate.d.ts +2 -0
  33. package/dist/operate.js +112 -14
  34. package/dist/plane-review.js +4 -0
  35. package/dist/process-recovery-eligibility.d.ts +1 -0
  36. package/dist/project-learning.d.ts +3 -3
  37. package/dist/project-learning.js +28 -17
  38. package/dist/proof.d.ts +3 -1
  39. package/dist/proof.js +4 -2
  40. package/dist/result-completion.d.ts +4 -0
  41. package/dist/result-completion.js +11 -0
  42. package/dist/review-context.d.ts +3 -0
  43. package/dist/review-context.js +45 -5
  44. package/dist/run-closeout.d.ts +89 -0
  45. package/dist/run-closeout.js +302 -0
  46. package/dist/serve.d.ts +15 -1
  47. package/dist/serve.js +129 -14
  48. package/dist/setup-guide.d.ts +2 -2
  49. package/dist/shared-deps.d.ts +94 -0
  50. package/dist/shared-deps.js +365 -0
  51. package/dist/storage.js +2 -0
  52. package/dist/store.d.ts +36 -0
  53. package/dist/store.js +125 -29
  54. package/dist/task-status.d.ts +18 -4
  55. package/dist/task-status.js +35 -18
  56. package/dist/telegram-progress.js +2 -1
  57. package/dist/telegram-status.js +3 -3
  58. package/dist/test-temp.d.ts +19 -0
  59. package/dist/test-temp.js +98 -0
  60. package/dist/toolroll-update-ui.js +7 -2
  61. package/dist/toolroll-update.d.ts +21 -1
  62. package/dist/toolroll-update.js +135 -5
  63. package/dist/work-index.d.ts +11 -0
  64. package/dist/work-index.js +103 -16
  65. package/dist/work-summary.d.ts +3 -1
  66. package/dist/workspace-ui.js +1 -1
  67. package/dist/worktree.d.ts +16 -0
  68. package/dist/worktree.js +47 -0
  69. 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,7 +228,7 @@ 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
233
  import { RETENTION_KINDS, lastSweepAt, parsePeriod, periodChoices, retentionPlan } from "./retention.js";
233
234
  import { EXPORT_CSS, dataExportHtml } from "./export-ui.js";
@@ -1696,7 +1697,7 @@ export function createDecisionServer(options) {
1696
1697
  }
1697
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"]);
1698
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"]);
1699
- 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)$");
1700
1701
  const resource = request.method === "GET"
1701
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)
1702
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);
@@ -3885,6 +3886,11 @@ export function createDecisionServer(options) {
3885
3886
  const databaseFile = store.databaseFile();
3886
3887
  if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
3887
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 */ }
3888
3894
  const status = runtimeUpdateStatus(dirname(databaseFile));
3889
3895
  if (url.searchParams.get("fragment") === "steps")
3890
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>`);
@@ -4889,6 +4895,8 @@ export function createDecisionServer(options) {
4889
4895
  notices.unshift(DEMO_BANNER);
4890
4896
  if (s.chrome.modeBanner)
4891
4897
  notices.push(s.chrome.modeBanner.words);
4898
+ if (s.chrome.updateWaiting)
4899
+ notices.push(s.chrome.updateWaiting.words);
4892
4900
  let crew = { crew: [], crewTruncated: false };
4893
4901
  try {
4894
4902
  const project = s.chrome.active === 'chat' ? null : s.chrome.project;
@@ -5018,14 +5026,20 @@ export function createDecisionServer(options) {
5018
5026
  const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
5019
5027
  const counts = [
5020
5028
  { key: "working", label: "Working now", value: agents.length, href: "/work?view=running" },
5021
- { key: "waiting", label: "Waiting on you", value: needs.items.filter(one => one.assignmentState === "needs-decision").length, href: "/work?view=needs-you" },
5022
- { 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" },
5023
5032
  { key: "done", label: "Done this week", value: done.items.filter(one => (one.completion?.at ?? one.updatedAt) >= weekAgo).length, href: "/work?view=completed" },
5024
5033
  ];
5025
- const tabOf = (state) => state === "needs-decision" ? "needs-you" : state === "ready-to-check" ? "ready" : state === "working" || state === "checking" ? "running" : "finished";
5026
- const catchUp = all.items.filter(one => tabOf(one.assignmentState) !== "finished" || one.updatedAt >= weekAgo).slice(0, 40).map(one => ({
5027
- 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),
5028
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
+ })(),
5029
5043
  }));
5030
5044
  const windows = who.via === "cookie" && store.isInstanceOperator(who.name) ? limitsView(store.providerLimits(), [], { project: projectName, teammate: id => `Teammate ${id}` }, now) : null;
5031
5045
  return {
@@ -5105,6 +5119,14 @@ export function createDecisionServer(options) {
5105
5119
  return {};
5106
5120
  return { update: { version: update.release.version, security: update.release.security, href: "/settings#updates", dismissHref: "/settings/updates/dismiss" } };
5107
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
+ })(),
5108
5130
  ...(liveMode === null || liveModeTerms === null
5109
5131
  ? {}
5110
5132
  : {
@@ -8782,7 +8804,38 @@ export function createDecisionServer(options) {
8782
8804
  if (attendAct !== null) {
8783
8805
  return attendMutation(response, who, attendAct.taskId, attendAct.verb, body, now);
8784
8806
  }
8785
- 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
+ }
8786
8839
  if (act !== null && act.verb === "merge") {
8787
8840
  // Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
8788
8841
  // result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
@@ -11945,6 +11998,7 @@ function criterionMatrixHtml(matrix, options = {}) {
11945
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>`) +
11946
11999
  reviewDetails +
11947
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>`) +
11948
12002
  `<p class="meta">Requirement ID: <code>${escape(row.id)}</code></p></div></details></li>`;
11949
12003
  }).join("");
11950
12004
  return `<div class="result-section criterion-matrix"><strong>Requirements · ${matrix.length}</strong><ol class="requirement-list">${rows}</ol></div>`;
@@ -15504,6 +15558,9 @@ function shell(title, body, options = {}) {
15504
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>`) +
15505
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)}`}` +
15506
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>`) +
15507
15564
  (chrome.update === undefined
15508
15565
  ? ""
15509
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>` +
@@ -16517,7 +16574,7 @@ function taskChatApproval(focus, csrf) {
16517
16574
  `<p class="chat-plan-outcome">${escape(oneLineOf(scope.goal, 200))}</p>` +
16518
16575
  `<p class="chat-plan-facts">${facts.map(escape).join(" · ")}</p>` +
16519
16576
  `<details class="chat-approval">` +
16520
- `<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>` +
16521
16578
  `<form method="post" action="${taskHref(focus.executionId)}/approve" class="chat-approval-form approve-form">` +
16522
16579
  `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
16523
16580
  `<input type="hidden" name="nonce" value="${escape(approval.nonce)}">` +
@@ -16548,7 +16605,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
16548
16605
  const approvalContent = (inert && focus.approval !== null && focus.plan !== "requested"
16549
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>`
16550
16607
  : fragment && focus.approval !== null && focus.plan !== "requested"
16551
- ? `<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>`
16552
16609
  : taskChatApproval(focus, csrf));
16553
16610
  const approvalCard = focus.approval !== null && focus.dispatch?.action !== "approve-scope"
16554
16611
  ? `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approvalContent}</details>` : approvalContent;
@@ -20450,7 +20507,7 @@ function taskBodyParts(data) {
20450
20507
  `<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
20451
20508
  : "";
20452
20509
  const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : data.dispatch?.action === "approve-scope"
20453
- ? `<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>`
20454
20511
  : `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
20455
20512
  const optionsHtml = `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
20456
20513
  const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
@@ -20651,6 +20708,10 @@ function taskBodyParts(data) {
20651
20708
  status: data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null,
20652
20709
  statusHtml,
20653
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,
20654
20715
  // The plan, progress and plan changes are thread entries now; the rest still needs a person here.
20655
20716
  lead: [
20656
20717
  { key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
@@ -22091,6 +22152,13 @@ function resultPanelParts(detail, o) {
22091
22152
  const status = presentation?.status ?? resultHeadlineOf(receiptStatusOf(receipt));
22092
22153
  const stored = receiptStatusOf(receipt, null);
22093
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
+ });
22094
22162
  const directAssessment = proof?.matrix.some(row => row.assessment !== undefined) === true;
22095
22163
  const awaitingGoalReview = directAssessment && proof?.reasons.length === 1 && proof.reasons[0] === GOAL_ASSESSMENT_PENDING;
22096
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();
@@ -22154,7 +22222,7 @@ function resultPanelParts(detail, o) {
22154
22222
  const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
22155
22223
  const youCheck = personChecks.length === 0 ? null : {
22156
22224
  lines: [...new Set(personChecks)],
22157
- 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,
22158
22226
  };
22159
22227
  const youCheckHtml = youCheck === null ? "" :
22160
22228
  `<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
@@ -22168,7 +22236,8 @@ function resultPanelParts(detail, o) {
22168
22236
  // account waits behind a disclosure in Summary so nothing is said twice.
22169
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."));
22170
22238
  const outcome = conciseOutcomeOf(conclusion);
22171
- 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);
22172
22241
  // ---- summary: the deliverable first ----------------------------------
22173
22242
  const summaryParts = [];
22174
22243
  const report = receipt.report;
@@ -22456,10 +22525,56 @@ function resultPanelParts(detail, o) {
22456
22525
  learning: detail.learning ?? "",
22457
22526
  request: detail.skillTest ? null : requestParts.join("\n"),
22458
22527
  canRequest: detail.canAnnotate && o.csrf !== "",
22528
+ need,
22459
22529
  requestQuiet: detail.canAnnotate && o.csrf !== "" && detail.comments.length === 0 && (detail.pastComments?.length ?? 0) === 0 && detail.revisions.length === 0,
22460
22530
  };
22461
22531
  return { html, panel };
22462
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
+ }
22463
22578
  /** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
22464
22579
  * first sentence, cut at a word when it runs past 180 characters. The
22465
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;