toolroll 0.9.94 → 0.9.95

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 (191) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/agent-fence.d.ts +6 -0
  3. package/dist/agent-fence.js +2 -2
  4. package/dist/assignment-status.d.ts +1 -1
  5. package/dist/assignment-ui.js +2 -0
  6. package/dist/attest.d.ts +3 -0
  7. package/dist/attest.js +10 -0
  8. package/dist/browser/terminal.js +1 -1
  9. package/dist/browser/workspace.css +1 -1
  10. package/dist/browser/workspace.js +24 -24
  11. package/dist/browser-crew.js +2 -0
  12. package/dist/browser-workspace.d.ts +5 -0
  13. package/dist/build-review.js +2 -0
  14. package/dist/builder.d.ts +7 -2
  15. package/dist/builder.js +183 -79
  16. package/dist/card-catalog.d.ts +209 -0
  17. package/dist/card-catalog.js +133 -0
  18. package/dist/card-cli.d.ts +43 -0
  19. package/dist/card-cli.js +44 -0
  20. package/dist/card-render.d.ts +26 -0
  21. package/dist/card-render.js +223 -0
  22. package/dist/card-sources.d.ts +68 -0
  23. package/dist/card-sources.js +234 -0
  24. package/dist/chat-actions.d.ts +95 -8
  25. package/dist/chat-actions.js +449 -45
  26. package/dist/chat-approval.d.ts +13 -0
  27. package/dist/chat-approval.js +39 -9
  28. package/dist/chat-card-part.d.ts +11 -0
  29. package/dist/chat-card-part.js +18 -0
  30. package/dist/chat-card.d.ts +46 -0
  31. package/dist/chat-card.js +99 -0
  32. package/dist/chat-channel.d.ts +2 -0
  33. package/dist/chat-channel.js +26 -14
  34. package/dist/chat-controls.d.ts +4 -0
  35. package/dist/chat-controls.js +4 -0
  36. package/dist/chat-decide.d.ts +3 -14
  37. package/dist/chat-decide.js +43 -54
  38. package/dist/chat-delivery.js +13 -4
  39. package/dist/chat-flow.d.ts +0 -27
  40. package/dist/chat-flow.js +31 -2
  41. package/dist/chat-quiet.js +1 -1
  42. package/dist/chat-task-actions.d.ts +3 -2
  43. package/dist/chat-task-actions.js +19 -5
  44. package/dist/chat-terms.d.ts +14 -2
  45. package/dist/chat-terms.js +35 -0
  46. package/dist/check-failure.d.ts +2 -0
  47. package/dist/check-failure.js +34 -2
  48. package/dist/check-repair.d.ts +47 -0
  49. package/dist/check-repair.js +240 -0
  50. package/dist/claim.d.ts +6 -0
  51. package/dist/claim.js +13 -2
  52. package/dist/cli-help.d.ts +1 -1
  53. package/dist/cli-help.js +2 -0
  54. package/dist/cli.js +4 -0
  55. package/dist/coding-provider.d.ts +1 -1
  56. package/dist/coding-provider.js +9 -5
  57. package/dist/coding-workspace.d.ts +1 -1
  58. package/dist/coding-workspace.js +4 -4
  59. package/dist/contracts/chat-actions.d.ts +83 -0
  60. package/dist/contracts/chat-actions.js +18 -0
  61. package/dist/contracts/chat-card.d.ts +91 -0
  62. package/dist/contracts/chat-card.js +54 -0
  63. package/dist/contracts/chat-content.d.ts +191 -0
  64. package/dist/contracts/chat-content.js +4 -0
  65. package/dist/contracts/console-api.d.ts +2 -0
  66. package/dist/contracts/console-api.js +2 -0
  67. package/dist/contracts/flow-propose.d.ts +8 -1
  68. package/dist/contracts/flow-propose.js +3 -1
  69. package/dist/contracts/flow-send.d.ts +126 -0
  70. package/dist/contracts/flow-send.js +3 -0
  71. package/dist/contracts/lead-tools.d.ts +173 -0
  72. package/dist/contracts/lead-tools.js +15 -0
  73. package/dist/contracts/store-json.d.ts +16 -0
  74. package/dist/discord-chat.js +14 -0
  75. package/dist/dispatch.d.ts +1 -1
  76. package/dist/dispatch.js +7 -1
  77. package/dist/dispose.d.ts +19 -0
  78. package/dist/dispose.js +47 -7
  79. package/dist/eval-oracle.js +4 -1
  80. package/dist/evidence-pair.d.ts +53 -0
  81. package/dist/evidence-pair.js +128 -0
  82. package/dist/exhaustion.d.ts +17 -4
  83. package/dist/exhaustion.js +44 -4
  84. package/dist/export.js +1 -1
  85. package/dist/failing-first.d.ts +31 -0
  86. package/dist/failing-first.js +44 -0
  87. package/dist/fallback-schema.d.ts +8 -0
  88. package/dist/fallback-schema.js +40 -0
  89. package/dist/fallback.d.ts +138 -0
  90. package/dist/fallback.js +605 -0
  91. package/dist/findings.d.ts +224 -0
  92. package/dist/findings.js +449 -0
  93. package/dist/fleet-schema.d.ts +17 -0
  94. package/dist/fleet-schema.js +135 -0
  95. package/dist/flow-browser.js +2 -2
  96. package/dist/flow-code.d.ts +2 -2
  97. package/dist/flow-code.js +9 -5
  98. package/dist/flow-result-card.d.ts +22 -0
  99. package/dist/flow-result-card.js +55 -0
  100. package/dist/flow-send.js +12 -9
  101. package/dist/flow-steps.js +1 -1
  102. package/dist/flow-triggers.js +1 -1
  103. package/dist/flow-unattended.d.ts +26 -0
  104. package/dist/flow-unattended.js +12 -3
  105. package/dist/flows-ui.js +10 -2
  106. package/dist/follow-through-brief.d.ts +12 -0
  107. package/dist/follow-through-brief.js +31 -0
  108. package/dist/follow-through-cli.d.ts +23 -0
  109. package/dist/follow-through-cli.js +153 -0
  110. package/dist/git-incident.d.ts +16 -0
  111. package/dist/git-incident.js +69 -6
  112. package/dist/handover-note.d.ts +33 -0
  113. package/dist/handover-note.js +106 -0
  114. package/dist/invoke.d.ts +20 -1
  115. package/dist/invoke.js +108 -44
  116. package/dist/lead-cli.js +26 -24
  117. package/dist/lead-contract.js +1 -0
  118. package/dist/lead-doors.d.ts +4 -0
  119. package/dist/lead-doors.js +2 -2
  120. package/dist/lead-progress.js +2 -0
  121. package/dist/lead-tools.js +111 -32
  122. package/dist/lead.js +1 -1
  123. package/dist/local-owner.js +5 -0
  124. package/dist/modes.d.ts +6 -6
  125. package/dist/modes.js +3 -1
  126. package/dist/needs-you.d.ts +5 -2
  127. package/dist/needs-you.js +14 -2
  128. package/dist/operate-remote-arguments.js +1 -0
  129. package/dist/operate-remote.js +1 -1
  130. package/dist/operate.d.ts +1 -1
  131. package/dist/operate.js +165 -14
  132. package/dist/plan-login.d.ts +14 -0
  133. package/dist/plan-login.js +58 -0
  134. package/dist/planner.d.ts +1 -1
  135. package/dist/planner.js +2 -1
  136. package/dist/project-delete-ui.d.ts +12 -0
  137. package/dist/project-delete-ui.js +18 -1
  138. package/dist/project-delete.js +6 -0
  139. package/dist/project-knowledge.d.ts +6 -6
  140. package/dist/project-tools.d.ts +4 -4
  141. package/dist/project-tools.js +23 -7
  142. package/dist/protected-storage-look.d.ts +1 -0
  143. package/dist/protected-storage-look.js +13 -0
  144. package/dist/protected-storage.d.ts +70 -39
  145. package/dist/protected-storage.js +196 -147
  146. package/dist/provider-capture.d.ts +34 -0
  147. package/dist/provider-capture.js +53 -0
  148. package/dist/release-carry.js +10 -1
  149. package/dist/release.js +9 -1
  150. package/dist/scope.d.ts +22 -3
  151. package/dist/scope.js +71 -20
  152. package/dist/scout.d.ts +1 -1
  153. package/dist/scout.js +2 -1
  154. package/dist/serve.js +2 -0
  155. package/dist/server/chat.js +2 -2
  156. package/dist/server/console.css +50 -0
  157. package/dist/server/render-chat.js +14 -1
  158. package/dist/server/render-follow-through.d.ts +11 -0
  159. package/dist/server/render-follow-through.js +68 -0
  160. package/dist/server/render-results.js +1 -1
  161. package/dist/server/render-tasks.d.ts +3 -0
  162. package/dist/server/render-tasks.js +24 -5
  163. package/dist/server/route-table.js +3 -0
  164. package/dist/server/settings.js +37 -12
  165. package/dist/server/task-views.js +5 -0
  166. package/dist/server/tasks.js +76 -0
  167. package/dist/settings-groups-ui.d.ts +1 -0
  168. package/dist/settings-groups-ui.js +3 -1
  169. package/dist/settings-kit.js +3 -2
  170. package/dist/settings-proofs.js +3 -1
  171. package/dist/setup-guide.d.ts +2 -2
  172. package/dist/slack-chat.js +14 -1
  173. package/dist/steer-mailbox.d.ts +8 -1
  174. package/dist/steer-mailbox.js +164 -6
  175. package/dist/store.d.ts +11 -4
  176. package/dist/store.js +93 -30
  177. package/dist/surface.js +24 -4
  178. package/dist/task-control.d.ts +2 -1
  179. package/dist/task-status.js +5 -0
  180. package/dist/teardown-git.d.ts +2 -0
  181. package/dist/teardown-git.js +42 -16
  182. package/dist/telegram-lead.js +23 -6
  183. package/dist/telegram.js +12 -1
  184. package/dist/toolroll-update.js +2 -0
  185. package/dist/work-index.js +22 -7
  186. package/dist/work-summary.d.ts +1 -1
  187. package/dist/work-summary.js +5 -2
  188. package/dist/workspace-ui.d.ts +3 -1
  189. package/dist/workspace-ui.js +10 -1
  190. package/dist/worktree.js +23 -1
  191. package/package.json +2 -1
@@ -3,7 +3,8 @@ import { archivedWords } from "../tidy.js";
3
3
  import { btn } from "./kit.js";
4
4
  import { whenUtc } from "../when-html.js";
5
5
  import { networkWords } from "../egress.js";
6
- import { releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
6
+ import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
7
+ import { SUBSCRIPTION_CAPABLE } from "../keys.js";
7
8
  import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
8
9
  import { writesWords } from "../writes-mode.js";
9
10
  import { checkAccessWords } from "../check-access.js";
@@ -28,7 +29,7 @@ import { taskRoad } from "../first-run.js";
28
29
  import { starterForWork } from "../flow-starters.js";
29
30
  import {} from "../gaps.js";
30
31
  import { readAuthModeStrict } from "../keys.js";
31
- import { failedAttemptSentence, retryNoteOf, START_BUILDER_COMMAND, startBuilderHref } from "../needs-you.js";
32
+ import { failedAttemptSentence, retryNoteOf, START_BUILDER_COMMAND, startBuilderHref, STORAGE_HOLD_PREFIX } from "../needs-you.js";
32
33
  import { planVersionWords } from "../plain-words.js";
33
34
  import { chosenWords, makesNoPlan, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
34
35
  import { parseExecutionPlanDocument } from "../plan.js";
@@ -315,6 +316,14 @@ export function approvalSheetParts(input) {
315
316
  if (allowed !== null)
316
317
  allowing.push(allowed);
317
318
  allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
319
+ // Fallback agents are terms: who takes over when an agent runs out, with any that spend money said before the yes.
320
+ const chain = chainTermsLines(scope);
321
+ const fallbacks = liveChainFromJson(scope.proposedChainJson ?? null)?.slice(1) ?? [];
322
+ if (fallbacks.length > 0) {
323
+ const paid = fallbacks.filter(one => one.authMode === "api-key" || SUBSCRIPTION_CAPABLE[one.profile.provider] !== true);
324
+ // Each agent named once, the paid one said where it stands in the order.
325
+ allowing.push(`if it runs out of its plan, the work moves to ${fallbacks.map(one => `${agentNameWords(one.profile.provider, one.profile.model)}${paid.includes(one) ? " on an API key, which spends money" : ""}`).join(", then ")}`);
326
+ }
318
327
  const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
319
328
  if (earlier !== null)
320
329
  allowing.push(earlier);
@@ -441,6 +450,7 @@ export function approvalSheetParts(input) {
441
450
  row("Changes", changes),
442
451
  row("Won’t touch", html `<p>${scope.outOfScope === null ? "Nothing is ruled out." : scope.outOfScope}</p>`),
443
452
  row("Done when", doneWhen),
453
+ chain.length === 0 ? "" : row("If it runs out", html `<p>${chain[0]}</p><ol class="approval-steps" data-approval-fallback>${chain.slice(1).map(one => html `<li>${one.replace(/^\d+\. /, "")}</li>`)}</ol>`),
444
454
  html `</dl>`,
445
455
  amendment,
446
456
  // The size, said once and plainly beside who builds: "Small change: fast model, no plan".
@@ -1044,6 +1054,7 @@ export function statusActionHref(status, taskId, runId, prUrl, repo) {
1044
1054
  case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId);
1045
1055
  case "open-run": return runId === null ? taskHref(taskId) : `/r/${runId}`;
1046
1056
  case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId));
1057
+ case "open-findings": return `${taskHref(taskId)}#findings`;
1047
1058
  case "open-task":
1048
1059
  default: return taskHref(taskId);
1049
1060
  }
@@ -1364,7 +1375,7 @@ export function taskBodyParts(data) {
1364
1375
  case "select-agent": return html ` <a href="#scope">Choose an available provider and model</a>.`;
1365
1376
  case "approve-scope": return html ` <a href="#approve">Review the plan and approve it</a>.`;
1366
1377
  case "answer-decision": return html ` <a href="#decisions">Answer the waiting question</a>.`;
1367
- case "unhold": return html ` Use <strong>Remove hold</strong> when it can continue.`;
1378
+ case "unhold": return diagnosis.code === "storage-blocked" ? html ` Use <strong>Try again</strong> once it's allowed.` : html ` Use <strong>Remove hold</strong> when it can continue.`;
1368
1379
  // The reason is the detail itself; Retry is the status card's own act, said once.
1369
1380
  case "retry-task": return "";
1370
1381
  case "repair-capability": return html ` <a href="/caps">Repair the requirement</a>.`;
@@ -1536,7 +1547,9 @@ export function taskBodyParts(data) {
1536
1547
  ? ""
1537
1548
  : joinHtml([
1538
1549
  html `<h2>Holds</h2>`,
1539
- joinHtml(data.holds.map(hold => html `<p class="row">${holdOwnerWords(hold.ownerKind)} — ${hold.reason}${hold.until === null ? "" : html ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`), "\n"),
1550
+ joinHtml(data.holds.map(
1551
+ // The storage check's hold (dispose.ts holdForStorage) says what it waits for, in the check's own words.
1552
+ hold => html `<p class="row">${hold.ownerKind === "backoff" && hold.reason.startsWith(STORAGE_HOLD_PREFIX) ? `waiting for macOS permission — ${hold.reason.slice(STORAGE_HOLD_PREFIX.length)}` : `${holdOwnerWords(hold.ownerKind)} — ${hold.reason}`}${hold.until === null ? "" : html ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`), "\n"),
1540
1553
  html `<p class="meta">Only your hold can be lifted here. Waits for a question, repeated failures or a retry clear on their own.</p>`,
1541
1554
  ]);
1542
1555
  const approval = approvalOf(scope);
@@ -2012,7 +2025,9 @@ export function taskBodyParts(data) {
2012
2025
  // to "plan first" adds a second, unnecessary decision at the exact
2013
2026
  // moment the page should have one obvious action.
2014
2027
  stopControlsActive || canPlan || !canHold ? "" : holdAct,
2015
- data.holds.some(hold => hold.ownerKind === "operator") ? act("unhold", "Remove hold") : "",
2028
+ data.holds.some(hold => hold.ownerKind === "operator") ? act("unhold", "Remove hold")
2029
+ // The storage check couldn't finish (dispose.ts holdForStorage): one tap tries again now.
2030
+ : data.holds.some(hold => hold.ownerKind === "backoff" && hold.ownerId === `storage:${hold.taskRef}`) ? act("unhold", "Try again") : "",
2016
2031
  html `</div>`,
2017
2032
  primaryAct === null ? "" : html `<p class="meta acts-why acts-why-${primaryAct.whyClass}">${primaryAct.why}</p>`,
2018
2033
  data.claimed && !stopControlsActive
@@ -2159,6 +2174,10 @@ export function taskBodyParts(data) {
2159
2174
  data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", data.runs.length === 0 ? "" : runRows, false, data.runs.length, "attempts"),
2160
2175
  section("usage", spendCard, false),
2161
2176
  section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
2177
+ data.followThrough == null ? "" : section("findings", data.followThrough.findings, data.followThrough.openFindings > 0, data.followThrough.findingCount),
2178
+ data.followThrough == null ? "" : section("failed checks", data.followThrough.repairs, true),
2179
+ data.followThrough == null ? "" : section("before and after", data.followThrough.evidence, false),
2180
+ data.followThrough == null ? "" : section("handover note", data.followThrough.handover, false),
2162
2181
  section("plan",
2163
2182
  // The recipe road rides with the scope it reuses (UI polish
2164
2183
  // 2026-09-13), off the title-to-action path.
@@ -461,6 +461,7 @@ post("settings", "spend.budget", "/spend/budget", { scope: "step-up", ...operato
461
461
  post("settings", "settings.project-concurrency", "/settings/project/concurrency");
462
462
  post("settings", "settings.project-live-view", "/settings/project/live-view", { scope: "step-up", callers: ["cookie"] });
463
463
  post("settings", "settings.project-network", "/settings/project/network", { scope: "step-up", callers: ["cookie"] });
464
+ post("settings", "settings.project-fallback", "/settings/project/fallback", { scope: "step-up", callers: ["cookie"] });
464
465
  post("settings", "settings.project-writes", "/settings/project/writes", { scope: "step-up", callers: ["cookie"] });
465
466
  post("settings", "settings.project-check-access", "/settings/project/check-access", { scope: "step-up", callers: ["cookie"] });
466
467
  post("settings", "settings.project-sandbox", "/settings/project/sandbox", { scope: "step-up", callers: ["cookie"] });
@@ -572,6 +573,8 @@ post("tasks", "decision.answer", `^\\/d\\/${ID}\\/answer$`, { project: "decision
572
573
  // A held automatic release goes on: a person, with their password typed now (release-auto.ts allowAutoPublish).
573
574
  post("tasks", "task.release.go-on", `^\\/t\\/${TASK}\\/release\\/go-on$`, { project: "task", limited: "resource", scope: "step-up", sample: "/t/one/release/go-on", ...approver("An approver lets a release go on.", "/tasks") });
574
575
  post("tasks", "task.act.keep-grown", `^\\/t\\/${TASK}\\/keep-grown$`, { project: "task", limited: "resource", sample: "/t/one/keep-grown", callers: ["cookie"], ...approver("An approver keeps a change.", "/tasks") });
576
+ // Findings, a failed check's repair, before-and-after evidence: a person's acts from a browser, none of them an approval.
577
+ post("tasks", "task.act.follow-through", `^\\/t\\/${TASK}\\/follow-through$`, { project: "task", limited: "resource", sample: "/t/one/follow-through", callers: ["cookie"], ...approver("An approver acts on a finding.", "/tasks") });
575
578
  post("tasks", "task.act.hold", `^\\/t\\/${TASK}\\/hold$`, { project: "task", limited: "resource", sample: "/t/one/hold" });
576
579
  post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task", limited: "resource", sample: "/t/one/unhold" });
577
580
  post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
@@ -2,6 +2,7 @@ import { PASSWORDLESS_TRADE, passwordlessSetting, setPasswordless } from "../pas
2
2
  import { ACP_AGENTS, isKeyedProvider } from "../acp-agents.js";
3
3
  import { LIVE_VIEW_CONSENT, liveViewConsentDigest, setLiveCommands } from "../live-detail.js";
4
4
  import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
5
+ import { clearFallbackChain, parseEntry, setFallbackChain, setPaidGrant } from "../fallback.js";
5
6
  import { galleryTemplateOf } from "../flow-gallery.js";
6
7
  import { finishGoogleConsent, GOOGLE_CALLBACK } from "../google-mail.js";
7
8
  import { kitInstalled, kitOf } from "../kits.js";
@@ -84,7 +85,7 @@ import { acceptSuggestion, dismissSuggestion, grantWords, keepRule, modelName, r
84
85
  import { ALL_SLOTS, applyModelChoices, browserWords, checkScope, DEFAULT_PRESET, defaultAgentOf, fillDefaultRouting, isPresetName, newerModels, PRESETS, presetChange, presetInUse, previewModelChange, recentRuns, rowAt, slotKey, SLOTS, slotWords, stateDigest, switchChange } from "../model-choices.js";
85
86
  import { HEADROOM_FIVE_HOUR_LIMIT, HEADROOM_WEEKLY_LIMIT, exactModelId } from "../phase-routing.js";
86
87
  import { RISKY_MEANS, SIZE_MEANS, SIZING_CLAUDE_MODEL } from "../task-sizing.js";
87
- import { CHAT_APPROVE_ALL, MODE_MAX_DAYS, modeDigestOf, modeTermsJson, modeWords, presetTerms } from "../modes.js";
88
+ import { CHAT_APPROVE_ALL, CHECK_REPAIR_LIMIT, MODE_MAX_DAYS, modeDigestOf, modeTermsJson, modeWords, presetTerms } from "../modes.js";
88
89
  import { monitoringChange, readMonitoring, saveMonitoring } from "../monitoring-settings.js";
89
90
  import { monitoringHtml, signingSecretHtml } from "../monitoring-ui.js";
90
91
  import { openRouterPickerScript } from "../openrouter-models.js";
@@ -2461,19 +2462,17 @@ ${postForm("/settings/chat-approval/save", html `\
2461
2462
  planAuto: body.get("plan-auto") === "1",
2462
2463
  reviewAuto: false,
2463
2464
  reviewRetryAuto: false,
2464
- // The repair-auto grant is the SAME rule (v40): unchecked stays
2465
- // false on every preset — only the explicit box grants it, and the
2466
- // attempt cap it carries is meaningless without it.
2467
- repairAuto: false,
2468
- repairMaxAttempts: 0,
2465
+ // Repairing failed checks: only the explicit choice grants it, with the limit it signs (check-repair.ts).
2466
+ repairAuto: body.get("repair-auto") === "1",
2467
+ repairMaxAttempts: body.get("repair-auto") === "1" ? CHECK_REPAIR_LIMIT : 0,
2469
2468
  // Approving from the paired chat: only the explicit choice grants it, naming every chat app it covers (a grant
2470
2469
  // signed before it named them stays Telegram only).
2471
2470
  chatApprove: body.get("chat-approve") === "1",
2472
2471
  ...(body.get("chat-approve") === "1" ? { chatApproveChats: CHAT_APPROVE_ALL } : {}),
2473
2472
  publication: body.get("publication") === "automerge" ? "automerge" : "notify",
2474
2473
  };
2475
- if (["review-auto", "review-retry-auto", "repair-auto"].some(field => body.get(field) === "1")) {
2476
- return refuse(response, who, 400, "Review scheduling and automatic revisions are no longer available. Reload the mode form.", "/mode");
2474
+ if (["review-auto", "review-retry-auto"].some(field => body.get(field) === "1")) {
2475
+ return refuse(response, who, 400, "Review scheduling is no longer available. Reload the mode form.", "/mode");
2477
2476
  }
2478
2477
  if (terms.planAuto && !terms.autoApproveFiling) {
2479
2478
  return refuse(response, who, 400, "Automatic planner approval requires automatic filing approval.", "/mode");
@@ -2486,9 +2485,9 @@ ${postForm("/settings/chat-approval/save", html `\
2486
2485
  // THE CEREMONY (M1): every resolved term in words, and the password
2487
2486
  // signs exactly this digest — a drifted form refuses at /mode/sign.
2488
2487
  const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
2489
- const signed = Object.fromEntries(["name", "days", "publication", "auto-approve", "plan-auto", "chat-approve"]
2488
+ const signed = Object.fromEntries(["name", "days", "publication", "auto-approve", "plan-auto", "chat-approve", "repair-auto"]
2490
2489
  .map(field => [field, body.get(field) ?? ""]));
2491
- const bodyHtml = modeCeremonyHtml({ name, project: projectName(project), words: modeWords(terms), chatApprove: signed["chat-approve"] === "1", hidden: { nonce, digest, ...signed, expiry } });
2490
+ const bodyHtml = modeCeremonyHtml({ name, project: projectName(project), words: modeWords(terms), chatApprove: signed["chat-approve"] === "1", repairAuto: signed["repair-auto"] === "1", hidden: { nonce, digest, ...signed, expiry } });
2492
2491
  return sendScreen(response, 200, screen("Sign the mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
2493
2492
  }
2494
2493
  // /mode/sign: the fixed expiry rides the form; the digest is
@@ -2504,8 +2503,8 @@ ${postForm("/settings/chat-approval/save", html `\
2504
2503
  return refuse(response, who, 409, "the terms moved while you were reading — read them again", "/mode");
2505
2504
  }
2506
2505
  const token = body.get("token") ?? "";
2507
- // A mode that lets a chat app approve opens a way to act: a credential act. Any other mode is an ordinary one.
2508
- if (!(body.get("chat-approve") === "1" ? authenticateCredential(who, token) : authenticateApprover(who, token)).ok) {
2506
+ // A mode that lets a chat app approve, or repairs start by themselves, opens a way to act: a credential act. Any other mode is an ordinary one.
2507
+ if (!(body.get("chat-approve") === "1" || body.get("repair-auto") === "1" ? authenticateCredential(who, token) : authenticateApprover(who, token)).ok) {
2509
2508
  return refuse(response, who, 403, "signing a mode takes your password, typed again", "/mode");
2510
2509
  }
2511
2510
  store.signMode({
@@ -2814,6 +2813,7 @@ ${postForm("/settings/chat-approval/save", html `\
2814
2813
  "settings.project-concurrency": projectConcurrency,
2815
2814
  "settings.project-live-view": projectLiveView,
2816
2815
  "settings.project-network": projectNetwork,
2816
+ "settings.project-fallback": projectFallback,
2817
2817
  "settings.project-writes": projectWrites,
2818
2818
  "settings.project-check-access": projectCheckAccess,
2819
2819
  "settings.project-sandbox": projectSandbox,
@@ -3198,5 +3198,30 @@ ${postForm("/settings/chat-approval/save", html `\
3198
3198
  : kind === "suggest-on" ? "Suggestions are on for this project." : "Suggestions are off for this project.");
3199
3199
  };
3200
3200
  }
3201
+ // Settings → Project → Fallback agents (fallback.ts): who takes over when an agent runs out of its plan. An approver for
3202
+ // the project, with their password typed now; the ledger keeps every change. New tasks seal it; approved ones keep theirs.
3203
+ async function projectFallback(ctx) {
3204
+ const { who, response, now, posted } = ctx;
3205
+ const body = readForm(posted, CONSOLE_FORMS.projectFallback);
3206
+ const repo = body.get("repo") ?? "";
3207
+ if (!consoleProjects().includes(repo) || !visible(repo))
3208
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
3209
+ if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
3210
+ return refuse(response, who, 403, "An approver for this project changes its fallback agents.", "/settings/project");
3211
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#fallback");
3212
+ const parsed = (body.get("agents") ?? "").split(/[\n,]+/).map(one => one.trim()).filter(Boolean).map(parseEntry);
3213
+ const bad = parsed.find((one) => typeof one === "string");
3214
+ if (bad !== undefined)
3215
+ return back("problem", `Nothing changed: ${bad}.`);
3216
+ if (!authenticateCredential(who, body.get("password") ?? "").ok)
3217
+ return back("problem", "Enter your Toolroll password to change fallback agents.");
3218
+ const done = store.transact(() => {
3219
+ const set = parsed.length === 0 ? clearFallbackChain(store, repo, who.name, now) : setFallbackChain(store, repo, parsed, who.name, now);
3220
+ if (!set.ok || parsed.length === 0)
3221
+ return set;
3222
+ return setPaidGrant(store, repo, body.get("paid") === "on", who.name, now);
3223
+ });
3224
+ return done.ok ? back("said", "Saved. Tasks scoped from now on use it; tasks already approved keep the chain they were approved with.") : back("problem", `Nothing changed: ${done.problem}`);
3225
+ }
3201
3226
  return { registrations, modelsView };
3202
3227
  }
@@ -1,3 +1,5 @@
1
+ import { heldFindingCount } from "../findings.js";
2
+ import { familyInWebReach } from "../findings.js";
1
3
  import { existsSync } from "node:fs";
2
4
  import { resultRecordWordsOf } from "../route-results.js";
3
5
  import { selectionEvidenceWords } from "../route-rules.js";
@@ -28,6 +30,7 @@ import { readSkillsSnapshot, skillTestResult } from "../project-skills.js";
28
30
  import { newestPullRequestOf, publishingOf, pullRequestBlocker } from '../pull-request-flow.js';
29
31
  import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
30
32
  import { resultCheckLevel } from '../result-follow-ups.js';
33
+ import { followThroughView } from "./render-follow-through.js";
31
34
  import { skillsSnapshotHtml, skillTestFeedbackHtml } from "../skills-ui.js";
32
35
  import { archiveOf } from "../store.js";
33
36
  import { runActivityOf } from "../task-activity.js";
@@ -143,6 +146,7 @@ export function createTaskViews(runtime) {
143
146
  const live = runs.find(one => runIsLive(one)) ?? null;
144
147
  const facts = {
145
148
  id: task.id,
149
+ heldFindings: heldFindingCount(runtime.store, task.id),
146
150
  title: task.title,
147
151
  repo: task.repo,
148
152
  state: task.state,
@@ -541,6 +545,7 @@ export function createTaskViews(runtime) {
541
545
  : field.field === "local-refused" ? { eligible: false, message: field.message, href: field.href, action: field.action } : null };
542
546
  })(),
543
547
  routeChange: ref === null ? null : routeChangeOf(runtime.store, ref.externalId),
548
+ followThrough: who === undefined || familyInWebReach(runtime.store, who, taskId, runtime.visible) === null ? null : followThroughView(runtime.store, runtime.evidenceRoot, taskId, taskHref(taskId), who.role === "approver" && who.via === "cookie" && !runtime.store.isDemo(), runtime.clock()),
544
549
  fastGrown: (() => {
545
550
  if (ref === null || runtime.store.getTask(ref.externalId)?.state !== "done" || !fastStarted(runtime.store, ref.externalId))
546
551
  return null;
@@ -36,6 +36,10 @@ import { firstTaskSuggestions, withBuilderStep } from "../first-run.js";
36
36
  import { fastStartPage, fastTermsRows, importPage, newTaskPage } from "./render-new-task.js";
37
37
  import { fileGithubItem, readGithubItem } from "../github-import.js";
38
38
  import { allowAutoPublish, autoPublishHeld } from "../release-auto.js";
39
+ import { startCheckRepair } from "../check-repair.js";
40
+ import { askFailingFirst, dropFailingFirst, rerunEvidence, saveEvidencePair } from "../evidence-pair.js";
41
+ import { closeFinding, familyInWebReach, familyRootOf, getFinding, NOT_ON_TASK, resendFinding, runOnTask, sendFinding, sendReviewFinding } from "../findings.js";
42
+ import { labelProviderCapture, providerCapture } from "../provider-capture.js";
39
43
  import { fastStartBuild, keepGrownChange, prepareFastStart } from "../fast-start.js";
40
44
  import { evidencePack } from "../evidence-pack.js";
41
45
  import { decisionPage, evidenceRecordPage, runPage } from "./render-history.js";
@@ -2962,6 +2966,7 @@ export function createTasksHandlers(runtime) {
2962
2966
  "tasks.add": addTask,
2963
2967
  "tasks.fast": buildNow,
2964
2968
  "task.act.keep-grown": keepGrown,
2969
+ "task.act.follow-through": followThroughAct,
2965
2970
  "queue.move": queueMove,
2966
2971
  "queue.note": queueNote,
2967
2972
  "decision.answer": answerDecision,
@@ -3062,6 +3067,77 @@ export function createTasksHandlers(runtime) {
3062
3067
  return taskScreen(response, who, act.taskId, kept.message, 409);
3063
3068
  return redirect(response, taskHref(act.taskId));
3064
3069
  }
3070
+ /** Findings, a failed check's repair and evidence (render-follow-through.ts). Nothing here is an approval: a repair is a
3071
+ * draft, and its task page asks for the plan's approval as any other. */
3072
+ async function followThroughAct(ctx) {
3073
+ const { url, who, response, now, posted } = ctx;
3074
+ const body = readForm(posted, CONSOLE_FORMS.followThrough);
3075
+ const act = matchTaskPath(url.pathname, "/follow-through$");
3076
+ if (act === null)
3077
+ return noPage(who, response);
3078
+ const { taskId } = act;
3079
+ // Chat's gate, here too (findings.ts familyInReach): every project in this task's family, not only this page's.
3080
+ if (familyInWebReach(store, who, taskId, visible) === null)
3081
+ return refuse(response, who, 403, "That task is outside your access.", "/tasks");
3082
+ const number = (name) => Number(body.get(name) ?? "");
3083
+ const back = () => redirect(response, `${taskHref(taskId)}#findings`);
3084
+ const refused = (message) => taskScreen(response, who, taskId, message, 409);
3085
+ /** A finding or a run named by a form must belong to this task's family: this page's project was the one admitted. */
3086
+ const ownFinding = () => { const found = getFinding(store, number("finding")); return found !== null && found.familyRoot === familyRootOf(store, taskId) ? found : null; };
3087
+ const ownRun = () => runOnTask(store, number("run"), taskId);
3088
+ switch (body.get("act")) {
3089
+ case "send": {
3090
+ const line = body.get("line") ?? "";
3091
+ const pointer = body.get("pointer") ?? "";
3092
+ const sent = pointer !== "" ? sendReviewFinding(store, taskId, pointer, who.name, now)
3093
+ : sendFinding(store, { taskId, source: "person", by: who.name, summary: body.get("note") ?? "", file: body.get("file") || null, line: line === "" ? null : Number(line) }, now);
3094
+ return sent.ok ? back() : refused(sent.message);
3095
+ }
3096
+ case "resend": {
3097
+ if (ownFinding() === null)
3098
+ return refused("That finding isn't on this task.");
3099
+ const sent = resendFinding(store, number("finding"), now);
3100
+ return sent.ok ? back() : refused(sent.message);
3101
+ }
3102
+ case "close": {
3103
+ if (ownFinding() === null)
3104
+ return refused("That finding isn't on this task.");
3105
+ const closed = closeFinding(store, number("finding"), who.name, body.get("note") ?? "", now);
3106
+ return closed.ok ? back() : refused(closed.message);
3107
+ }
3108
+ case "repair": {
3109
+ if (!ownRun())
3110
+ return refused(NOT_ON_TASK);
3111
+ const started = startCheckRepair(store, evidenceRoot, number("run"), now);
3112
+ return started.ok ? redirect(response, taskHref(started.draft)) : refused(started.message);
3113
+ }
3114
+ case "save-evidence": {
3115
+ if (!ownRun())
3116
+ return refused(NOT_ON_TASK);
3117
+ const saved = saveEvidencePair(store, evidenceRoot, { taskId, runId: number("run"), by: who.name }, now);
3118
+ return saved.ok ? back() : refused(saved.message);
3119
+ }
3120
+ case "rerun-evidence": {
3121
+ const asked = rerunEvidence(store, taskId, who.name, now);
3122
+ return asked.ok ? back() : refused(asked.message);
3123
+ }
3124
+ case "failing-first-on":
3125
+ askFailingFirst(store, taskId, who.name, now);
3126
+ return back();
3127
+ case "failing-first-off":
3128
+ dropFailingFirst(store, taskId);
3129
+ return back();
3130
+ case "mark": {
3131
+ const capture = providerCapture(store, number("capture"));
3132
+ const label = body.get("label");
3133
+ if (capture === null || !runOnTask(store, capture.run, taskId) || (label !== "exhausted" && label !== "not-exhausted"))
3134
+ return refused("That capture isn't on this task.");
3135
+ labelProviderCapture(store, capture.id, label, who.name);
3136
+ return back();
3137
+ }
3138
+ default: return refused("Reload the page and try again.");
3139
+ }
3140
+ }
3065
3141
  return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
3066
3142
  }
3067
3143
  /** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
@@ -213,6 +213,7 @@ export declare function modeCeremonyHtml(view: {
213
213
  project: string;
214
214
  words: string[];
215
215
  chatApprove: boolean;
216
+ repairAuto: boolean;
216
217
  hidden: Readonly<Record<string, string>>;
217
218
  }): Html;
218
219
  export type PersonView = {
@@ -527,6 +527,8 @@ export function modePageHtml(view) {
527
527
  items: [["0", "Wait for a person"], ["1", "Approve plans that keep what I filed"]] })),
528
528
  formRow("Approving from a chat app", "Plans that widen permissions, go past the attempt limit or touch protected paths still open Toolroll.", select({ name: "chat-approve", label: "Approving from a chat app", current: "",
529
529
  items: [["", "Here, with my password"], ["1", "In my paired chat app, two taps"]] })),
530
+ formRow("Failed checks", "A repair stays inside the plan you approved. When it can't, or the tries run out, it comes to you.", select({ name: "repair-auto", label: "Failed checks", current: "",
531
+ items: [["", "Wait for me to start a repair"], ["1", "Repair them, up to 2 tries"]] })),
530
532
  html `<div class="k-acts">${btn("Review the terms", { variant: "primary", type: "submit" })}</div>`,
531
533
  ]))
532
534
  });
@@ -538,7 +540,7 @@ export function modeCeremonyHtml(view) {
538
540
  return settingsSubPage("approval", `Sign the ${view.name} mode`, joinHtml([
539
541
  settingsSection({ id: "mode-terms", title: `What you sign for ${view.project}`,
540
542
  body: html `<ul class="k-ssec-lead">${view.words.map(words => html `<li>${words}</li>`)}</ul>` }),
541
- kitSectionForm({ proof: view.chatApprove ? "mode-sign-chat-approve" : "mode-sign", action: "/mode/sign", id: "mode-sign-form", native: true, hidden: view.hidden,
543
+ kitSectionForm({ proof: view.repairAuto ? "mode-sign-repair" : view.chatApprove ? "mode-sign-chat-approve" : "mode-sign", action: "/mode/sign", id: "mode-sign-form", native: true, hidden: view.hidden,
542
544
  save: { label: "Sign it" }, confirm: `Sign the ${view.name} mode for ${view.project}`, body: html `` }),
543
545
  note(html `<a class="k-link" href="/mode">Back, and sign nothing</a>`),
544
546
  ]));
@@ -151,11 +151,12 @@ function rowForForm(proof, action, hidden) {
151
151
  for (const name of Object.keys(hidden))
152
152
  if (row.field === name || row.secrets.includes(name))
153
153
  throw new Error(`${row.id}: ${name} is never a hidden value`);
154
- // The exact variant: its own selector must be in the hidden terms, and no other variant's may be.
154
+ // The exact variant: its own selector must be in the hidden terms, and no other variant's may be, unless both are
155
+ // credential acts: two of those on one form ask the strongest proof there is, so nothing gets weaker.
155
156
  if (row.select !== undefined && String(hidden[row.select.field] ?? "") !== row.select.value)
156
157
  throw new Error(`${row.id} posts ${row.select.field}=${row.select.value}`);
157
158
  for (const other of variantsOf(row))
158
- if (other.select !== undefined && String(hidden[other.select.field] ?? "") === other.select.value)
159
+ if (other.select !== undefined && !(other.proof === "credential" && row.proof === "credential") && String(hidden[other.select.field] ?? "") === other.select.value)
159
160
  throw new Error(`${action} with ${other.select.field}=${other.select.value} is ${other.id}, not ${row.id}`);
160
161
  return row;
161
162
  }
@@ -20,7 +20,7 @@ const CREDENTIAL = { passwordless: "refused", sso: "accepted" };
20
20
  const TYPED = { passwordless: "refused", sso: "refused" };
21
21
  const NONE = { passwordless: "n/a", sso: "n/a" };
22
22
  /** /mode/sign re-derives the digest from the posted terms, so every one rides back. */
23
- const MODE_TERMS = ["nonce", "digest", "expiry", "name", "days", "publication", "auto-approve", "plan-auto", "chat-approve"];
23
+ const MODE_TERMS = ["nonce", "digest", "expiry", "name", "days", "publication", "auto-approve", "plan-auto", "chat-approve", "repair-auto"];
24
24
  const CHAT_APPS = [["slack", "Slack", "slack", ["app-token", "bot-token"]], ["teams", "Teams", "teams", ["secret"]], ["discord", "Discord", "discord", ["bot-token"]]];
25
25
  export const PROOF_INVENTORY = [
26
26
  // CSRF only.
@@ -72,11 +72,13 @@ export const PROOF_INVENTORY = [
72
72
  { id: "local-mode-off", action: "Turn off local mode", routes: ["/settings/local-mode/off"], form: "localModeOff", field: "password", proof: "step-up", ...ACCEPT, role: "operator", handlerRole: "a full session (never the local owner)", secrets: [], terms: [] },
73
73
  { id: "mode-sign", action: "Sign an operating mode", routes: ["/mode/sign"], form: "mode", field: "token", proof: "step-up", ...ACCEPT, role: "any", handlerRole: "an approver of the project", secrets: [], terms: MODE_TERMS },
74
74
  { id: "mode-sign-chat-approve", action: "Sign an operating mode that lets your chat app approve", routes: ["/mode/sign"], select: { field: "chat-approve", value: "1" }, form: "mode", field: "token", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: MODE_TERMS },
75
+ { id: "mode-sign-repair", action: "Sign an operating mode that repairs failed checks by itself", routes: ["/mode/sign"], select: { field: "repair-auto", value: "1" }, form: "mode", field: "token", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: MODE_TERMS },
75
76
  // Credential acts: a password typed now or a one-use re-check; never Passwordless.
76
77
  { id: "passwordless-on", action: "Turn Passwordless on", routes: ["/settings/passwordless"], form: "passwordless", field: "password", proof: "credential", when: "only to turn it on (off needs nothing)", ...CREDENTIAL, role: "operator", secrets: [], terms: [] },
77
78
  { id: "sign-in", action: "Change sign-in (SSO)", routes: ["/settings/sign-in"], form: "signIn", field: "password", proof: "credential", when: "every action except Test", ...CREDENTIAL, role: "operator", secrets: ["client-secret"], terms: [] },
78
79
  { id: "release-approval", action: "Approve releases automatically", routes: ["/settings/release-approval"], form: "releaseApproval", field: "password", proof: "credential", when: "only to approve automatically (Ask me needs nothing)", ...CREDENTIAL, role: "approver", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
79
80
  { id: "project-network", action: "Change what builds may reach", routes: ["/settings/project/network"], form: "projectNetwork", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
81
+ { id: "project-fallback", action: "Change a project's fallback agents", routes: ["/settings/project/fallback"], form: "projectFallback", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: [] },
80
82
  { id: "project-writes", action: "Change where builds may write", routes: ["/settings/project/writes"], form: "projectWrites", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
81
83
  { id: "project-sandbox", action: "Change a project's sandbox", routes: ["/settings/project/sandbox"], form: "projectSandbox", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"] },
82
84
  { id: "project-check-access", action: "Narrow how far checks reach", routes: ["/settings/project/check-access"], form: "projectCheckAccess", field: "password", proof: "credential", ...CREDENTIAL, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo"], note: "for a form that can only narrow; one that offers a wider level uses project-check-access-widen" },
@@ -18,9 +18,9 @@ export declare function previewProjectInstructions(repo: string): {
18
18
  ok: false;
19
19
  message: string;
20
20
  plan?: never;
21
+ content?: never;
21
22
  fingerprint?: never;
22
23
  installed?: never;
23
- content?: never;
24
24
  } | {
25
25
  message?: never;
26
26
  ok: true;
@@ -33,7 +33,7 @@ export declare function addProjectInstructions(repo: string, fingerprint: string
33
33
  ok: false;
34
34
  message: string;
35
35
  plan?: never;
36
+ content?: never;
36
37
  fingerprint?: never;
37
38
  installed?: never;
38
- content?: never;
39
39
  } | import("./skills.js").InstallResult;
@@ -1,3 +1,6 @@
1
+ import { slackCardBlocks } from "./card-render.js";
2
+ import { withoutCard } from "./chat-card-part.js";
3
+ import { cardWithin } from "./chat-card.js";
1
4
  import { chatFlowButtons } from "./chat-flow.js";
2
5
  import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
3
6
  import { chatQuestionButtons } from "./subagent-question.js";
@@ -14,6 +17,7 @@ import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./con
14
17
  import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
15
18
  import { resultImageFileName, safeResultImageCaption, verifyResultImage, } from "./chat-evidence.js";
16
19
  import {} from "./store.js";
20
+ const SLACK_REFUSED_BLOCKS = new Set(["invalid_blocks", "invalid_blocks_format", "invalid_arguments", "msg_too_long", "msg_blocks_too_long"]);
17
21
  const nowOf = (options) => options.clock?.() ?? new Date();
18
22
  const split = (text, size = 2800) => {
19
23
  const parts = [];
@@ -214,6 +218,10 @@ export async function deliverSlackPart(options) {
214
218
  if (content.task &&
215
219
  !repos.includes(store.lookupRef(content.task)?.repo ?? ""))
216
220
  throw new SlackError("Connected projects changed");
221
+ if (content.rich && !cardWithin(content.rich, repos)) {
222
+ state.dropPart(row.id, "Connected projects changed");
223
+ return true;
224
+ }
217
225
  // A screenshot sent with a result goes in that result's thread once its message is placed, and waits until then.
218
226
  const result = content.shot?.follows == null ? null : state.part(content.shot.follows);
219
227
  if (result !== null && !slackTs(result.message) && result.state === "pending") {
@@ -385,7 +393,7 @@ export async function deliverSlackPart(options) {
385
393
  .replaceAll("&", "&amp;")
386
394
  .replaceAll("<", "&lt;")
387
395
  .replaceAll(">", "&gt;"),
388
- blocks: voiced === null ? slackBlocks(text, buttons) : slackVoiceBlocks(voiced, buttons),
396
+ blocks: content.rich ? slackCardBlocks(content.rich, buttons) : voiced === null ? slackBlocks(text, buttons) : slackVoiceBlocks(voiced, buttons),
389
397
  mrkdwn: voiced !== null,
390
398
  parse: "none",
391
399
  unfurl_links: false,
@@ -415,6 +423,11 @@ export async function deliverSlackPart(options) {
415
423
  }
416
424
  // No permission to upload files here: one plain line instead of the result's screenshots.
417
425
  const saved = readChatPart(row.payload), content = saved.ok ? saved.value : null;
426
+ // Slack refused the card's blocks: the part's own plain text goes instead, so the message is never lost.
427
+ if (content?.rich && error instanceof SlackError && SLACK_REFUSED_BLOCKS.has(error.code)) {
428
+ state.repaint(row.id, withoutCard(content));
429
+ return true;
430
+ }
418
431
  if (content !== null && content.image && content.shot && error instanceof SlackError && ["missing_scope", "no_permission"].includes(error.code)) {
419
432
  refuseResultShots(state, store, row, content, "Slack", nowOf(options));
420
433
  return true;
@@ -1,5 +1,10 @@
1
1
  import type { Store } from "./store.js";
2
2
  export declare const STEER_HANDLED = "handled";
3
+ /** Findings have their own folder: a finding is not operator steering, and must never read as it (findings.ts). */
4
+ export declare const STEER_FINDINGS = "findings";
5
+ /** What the agent writes back: one file per finding it answers. */
6
+ export declare const STEER_ANSWERS = "answers";
7
+ export declare const findingNoteName: (id: number) => string;
3
8
  export type SteerMailbox = {
4
9
  runId: number;
5
10
  taskRef: number;
@@ -14,11 +19,13 @@ export type SteerMailbox = {
14
19
  last: number;
15
20
  /** Mailed and not yet moved to handled/: number → note id. */
16
21
  waiting: Map<number, number>;
22
+ /** Findings mailed to this run and not yet moved to handled/. */
23
+ findingsWaiting: Set<number>;
17
24
  };
18
25
  export declare const steerNoteName: (number: number) => string;
19
26
  /** Make the folder and its handled/ inside the worktree; null when it cannot be made. */
20
27
  export declare function openSteerMailbox(worktree: string, folder: string, runId: number, taskRef: number, key: string): SteerMailbox | null;
21
- /** One beat: record what the agent read, then mail what is new while the agent runs. */
28
+ /** One beat: record what the agent read and answered, then mail what is new while the agent runs. */
22
29
  export declare function pulseSteerMailbox(store: Store, box: SteerMailbox, now: Date, agentRunning: boolean): void;
23
30
  /** The attempt is over: one last read, then the folder goes. Unread notes stay pending for the next attempt. */
24
31
  export declare function closeSteerMailbox(store: Store, box: SteerMailbox, now: Date): void;