toolroll 0.9.67 → 0.9.68

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 (236) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/acp-client.js +1 -3
  3. package/dist/agent-board.d.ts +0 -1
  4. package/dist/agent-board.js +0 -1
  5. package/dist/agent-fence.js +3 -0
  6. package/dist/browser/workspace.css +1 -1
  7. package/dist/browser/workspace.js +31 -29
  8. package/dist/builder.js +3 -26
  9. package/dist/canonical-json.d.ts +14 -0
  10. package/dist/canonical-json.js +36 -0
  11. package/dist/chat-actions.js +1 -2
  12. package/dist/chat-admission.d.ts +19 -0
  13. package/dist/chat-admission.js +22 -0
  14. package/dist/chat-channel.js +1 -1
  15. package/dist/chat-delivery.js +0 -1
  16. package/dist/chat-follow.d.ts +17 -0
  17. package/dist/chat-follow.js +32 -0
  18. package/dist/chat-messages.js +0 -1
  19. package/dist/chat-settings-page.d.ts +23 -0
  20. package/dist/chat-settings-page.js +41 -0
  21. package/dist/claim.js +3 -6
  22. package/dist/claude-print.d.ts +12 -0
  23. package/dist/claude-print.js +34 -0
  24. package/dist/cli-http.d.ts +0 -7
  25. package/dist/cli-http.js +0 -21
  26. package/dist/cli.d.ts +3 -10
  27. package/dist/cli.js +15 -24
  28. package/dist/coding-provider.js +2 -1
  29. package/dist/coding-workspace.js +1 -1
  30. package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
  31. package/dist/connect-cli.js +331 -0
  32. package/dist/containment.d.ts +0 -2
  33. package/dist/containment.js +2 -6
  34. package/dist/contracts/chat-actions.d.ts +0 -1
  35. package/dist/contracts/chat-content.d.ts +0 -4
  36. package/dist/contracts/chat-content.js +0 -2
  37. package/dist/contracts/chat-tables.d.ts +0 -1
  38. package/dist/contracts/cli.d.ts +0 -62
  39. package/dist/contracts/cli.js +0 -4
  40. package/dist/contracts/coding-workspace.js +1 -1
  41. package/dist/contracts/contract.js +1 -1
  42. package/dist/contracts/flow-card.d.ts +0 -1
  43. package/dist/contracts/flow-propose.d.ts +0 -1
  44. package/dist/contracts/flow-send.js +1 -1
  45. package/dist/contracts/gateway-tools.d.ts +0 -1
  46. package/dist/contracts/integration-metadata.js +1 -1
  47. package/dist/contracts/lead-tools.d.ts +0 -1
  48. package/dist/contracts/memory-pass.d.ts +0 -1
  49. package/dist/contracts/memory-pass.js +1 -1
  50. package/dist/contracts/plan.js +1 -1
  51. package/dist/contracts/project-knowledge.js +1 -1
  52. package/dist/contracts/project-skills.d.ts +0 -1
  53. package/dist/contracts/proof.js +1 -1
  54. package/dist/contracts/recipes.js +1 -1
  55. package/dist/contracts/scope.js +1 -1
  56. package/dist/contracts/scout-report.js +1 -1
  57. package/dist/contracts/teams-callback.d.ts +0 -1
  58. package/dist/converse.d.ts +0 -6
  59. package/dist/converse.js +0 -19
  60. package/dist/daemon.js +3 -2
  61. package/dist/decision.d.ts +1 -3
  62. package/dist/deprecations.d.ts +12 -4
  63. package/dist/deprecations.js +27 -4
  64. package/dist/desktop-update-recovery.js +1 -1
  65. package/dist/desktop-update.js +3 -2
  66. package/dist/discord-chat.js +4 -15
  67. package/dist/discord-settings.d.ts +2 -6
  68. package/dist/discord-settings.js +10 -38
  69. package/dist/discord.js +12 -23
  70. package/dist/evidence.d.ts +0 -1
  71. package/dist/evidence.js +0 -3
  72. package/dist/exec.d.ts +0 -9
  73. package/dist/exec.js +3 -81
  74. package/dist/flow-actions.js +0 -3
  75. package/dist/flow-draft.d.ts +1 -3
  76. package/dist/flow-draft.js +15 -28
  77. package/dist/flow-scripts.d.ts +1 -8
  78. package/dist/flow-scripts.js +0 -13
  79. package/dist/flow-send.js +1 -1
  80. package/dist/flow-share.d.ts +0 -2
  81. package/dist/flow-share.js +1 -6
  82. package/dist/flow-steps.js +1 -1
  83. package/dist/flow-triggers.d.ts +0 -1
  84. package/dist/flow-triggers.js +1 -2
  85. package/dist/flow-unattended.js +1 -12
  86. package/dist/flows.js +1 -1
  87. package/dist/health-cli.d.ts +2 -2
  88. package/dist/health-cli.js +1 -1
  89. package/dist/invoke.d.ts +0 -3
  90. package/dist/invoke.js +5 -3
  91. package/dist/is-record.d.ts +2 -0
  92. package/dist/is-record.js +2 -0
  93. package/dist/lead-doors.d.ts +1 -5
  94. package/dist/lead-doors.js +0 -4
  95. package/dist/lead-tools.js +3 -6
  96. package/dist/live-detail.d.ts +0 -1
  97. package/dist/live-detail.js +0 -6
  98. package/dist/live-terminal.js +2 -1
  99. package/dist/live.d.ts +0 -1
  100. package/dist/live.js +1 -2
  101. package/dist/mcp-core.js +0 -2
  102. package/dist/memory-pass.js +1 -1
  103. package/dist/model-catalog.js +1 -1
  104. package/dist/modes.js +1 -12
  105. package/dist/operate.js +383 -750
  106. package/dist/password-bearer.d.ts +0 -2
  107. package/dist/password-bearer.js +0 -4
  108. package/dist/peek-cli.js +2 -1
  109. package/dist/peek.d.ts +2 -26
  110. package/dist/peek.js +2 -168
  111. package/dist/phase-routing.d.ts +0 -4
  112. package/dist/phase-routing.js +2 -21
  113. package/dist/planner-source.d.ts +2 -1
  114. package/dist/planner-source.js +2 -12
  115. package/dist/planner.js +2 -14
  116. package/dist/process-tree.d.ts +0 -2
  117. package/dist/process-tree.js +0 -10
  118. package/dist/project-delete.d.ts +0 -2
  119. package/dist/project-delete.js +1 -3
  120. package/dist/project-learning.d.ts +0 -14
  121. package/dist/project-skills.d.ts +0 -1
  122. package/dist/project-skills.js +0 -14
  123. package/dist/project-tools.js +2 -10
  124. package/dist/provider.d.ts +0 -17
  125. package/dist/provider.js +0 -26
  126. package/dist/pulls.d.ts +0 -2
  127. package/dist/pulls.js +0 -9
  128. package/dist/release-adapters.d.ts +3 -0
  129. package/dist/release-adapters.js +15 -5
  130. package/dist/remote-exec.d.ts +3 -2
  131. package/dist/remote-exec.js +3 -2
  132. package/dist/repos.d.ts +0 -5
  133. package/dist/repos.js +2 -10
  134. package/dist/repository-context.d.ts +0 -2
  135. package/dist/repository-context.js +0 -4
  136. package/dist/restart-certification.d.ts +0 -2
  137. package/dist/restart-certification.js +1 -5
  138. package/dist/result-follow-ups.d.ts +1 -4
  139. package/dist/result-follow-ups.js +1 -6
  140. package/dist/result-review.d.ts +0 -2
  141. package/dist/result-review.js +0 -2
  142. package/dist/retention.d.ts +0 -1
  143. package/dist/retention.js +0 -1
  144. package/dist/review-context.d.ts +0 -1
  145. package/dist/review-context.js +1 -12
  146. package/dist/reviewer.d.ts +0 -16
  147. package/dist/reviewer.js +0 -18
  148. package/dist/run-closeout.js +2 -1
  149. package/dist/scope.js +1 -13
  150. package/dist/scout-browser.js +2 -1
  151. package/dist/serve.js +2 -2
  152. package/dist/server/console.css +5 -133
  153. package/dist/server/flows.js +2 -2
  154. package/dist/server/http.d.ts +2 -0
  155. package/dist/server/http.js +5 -0
  156. package/dist/server/pages.js +2 -2
  157. package/dist/server/people-tokens.js +3 -4
  158. package/dist/server/remote.js +1 -6
  159. package/dist/server/render-chat.d.ts +9 -2
  160. package/dist/server/render-chat.js +53 -7
  161. package/dist/server/render-results.d.ts +0 -6
  162. package/dist/server/render-results.js +3 -9
  163. package/dist/server/render-tasks.js +4 -5
  164. package/dist/server/route-table.d.ts +0 -4
  165. package/dist/server/route-table.js +0 -6
  166. package/dist/server/settings.js +23 -23
  167. package/dist/server/task-views.js +1 -1
  168. package/dist/server/tasks.js +3 -3
  169. package/dist/setup-guide.d.ts +2 -2
  170. package/dist/skills.d.ts +0 -6
  171. package/dist/skills.js +0 -8
  172. package/dist/slack-chat.d.ts +1 -7
  173. package/dist/slack-chat.js +5 -18
  174. package/dist/slack-settings.d.ts +2 -6
  175. package/dist/slack-settings.js +10 -38
  176. package/dist/slack.js +12 -23
  177. package/dist/sleep.d.ts +2 -0
  178. package/dist/sleep.js +2 -0
  179. package/dist/store.d.ts +0 -73
  180. package/dist/store.js +3 -177
  181. package/dist/structured-output.d.ts +0 -1
  182. package/dist/structured-output.js +0 -1
  183. package/dist/subagent-work.d.ts +0 -2
  184. package/dist/subagent-work.js +0 -2
  185. package/dist/subagents.d.ts +1 -3
  186. package/dist/subagents.js +20 -33
  187. package/dist/subscription-chat.d.ts +1 -3
  188. package/dist/subscription-chat.js +2 -3
  189. package/dist/task-sizing.d.ts +1 -4
  190. package/dist/task-sizing.js +3 -10
  191. package/dist/teams-chat.js +4 -11
  192. package/dist/teams.js +12 -19
  193. package/dist/telegram-lead.js +2 -2
  194. package/dist/telegram.js +1 -1
  195. package/dist/token-notices.d.ts +0 -11
  196. package/dist/token-notices.js +1 -5
  197. package/dist/tool-secret-store.d.ts +0 -3
  198. package/dist/tool-secret-store.js +2 -11
  199. package/dist/toolroll-update.d.ts +0 -2
  200. package/dist/toolroll-update.js +2 -2
  201. package/dist/verification-evidence.d.ts +0 -1
  202. package/dist/verification-evidence.js +0 -1
  203. package/dist/webhooks.d.ts +0 -1
  204. package/dist/webhooks.js +0 -1
  205. package/dist/workspace-ui.d.ts +0 -8
  206. package/dist/workspace-ui.js +1 -6
  207. package/package.json +1 -1
  208. package/dist/contracts/native-census.d.ts +0 -42
  209. package/dist/contracts/native-census.js +0 -36
  210. package/dist/process-recovery-anchor.d.ts +0 -75
  211. package/dist/process-recovery-anchor.js +0 -188
  212. package/dist/process-recovery-coalition.d.ts +0 -25
  213. package/dist/process-recovery-coalition.js +0 -105
  214. package/dist/process-recovery-eligibility.d.ts +0 -179
  215. package/dist/process-recovery-eligibility.js +0 -174
  216. package/dist/process-recovery-native.d.ts +0 -161
  217. package/dist/process-recovery-native.js +0 -595
  218. package/dist/process-recovery-provenance.d.ts +0 -163
  219. package/dist/process-recovery-provenance.js +0 -292
  220. package/dist/process-recovery-services.d.ts +0 -56
  221. package/dist/process-recovery-services.js +0 -191
  222. package/dist/process-recovery-settlement.d.ts +0 -32
  223. package/dist/process-recovery-settlement.js +0 -133
  224. package/dist/process-recovery.d.ts +0 -41
  225. package/dist/process-recovery.js +0 -91
  226. package/dist/session-brief.d.ts +0 -8
  227. package/dist/session-brief.js +0 -24
  228. package/dist/session-cli.d.ts +0 -42
  229. package/dist/session-cli.js +0 -271
  230. package/dist/session-contract.d.ts +0 -183
  231. package/dist/session-contract.js +0 -122
  232. package/dist/session-remote.d.ts +0 -19
  233. package/dist/session-remote.js +0 -86
  234. package/dist/session-service.d.ts +0 -22
  235. package/dist/session-service.js +0 -157
  236. package/dist/team-cli.js +0 -692
@@ -95,9 +95,6 @@ export declare function withRefusal(href: string, code: keyof typeof RESULT_REFU
95
95
  /** The queue reads at most this many completed tasks — the store's own
96
96
  * page ceiling, admission bound before it. */
97
97
  export declare const REVIEW_QUEUE_CAP = 100;
98
- /** The one return road the comment endpoint honors besides its own run
99
- * page: the cockpit's selected-result link, exactly. */
100
- export declare const REVIEW_RETURN: RegExp;
101
98
  /** Review priority: a deterministic, LABELED presentation aid over facts
102
99
  * the done list already carries. Band 0 goes first; the words name why.
103
100
  * It is never a verdict — the stored proof verdict stays the authority,
@@ -287,9 +284,6 @@ export declare function reviewCockpitPage(chrome: Chrome, data: {
287
284
  user: string;
288
285
  now: Date;
289
286
  }): Screen;
290
- /** The selected result: intent → proof → changes → publication → acts,
291
- * one scan path, every fact labeled by its source. */
292
- export declare function reviewCockpitDetail(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview?: boolean, tab?: ResultTab, user?: string): Html;
293
287
  /** The selected result's HTML and, for the rebuilt page, the same result
294
288
  * as data: every form and panel body is the one the HTML carries. */
295
289
  /** Where a result is shown: its own page (/review), or the Result tab of its task's page (docs/plans/one-page-task-and-agent.md),
@@ -238,9 +238,6 @@ export function withRefusal(href, code) {
238
238
  /** The queue reads at most this many completed tasks — the store's own
239
239
  * page ceiling, admission bound before it. */
240
240
  export const REVIEW_QUEUE_CAP = 100;
241
- /** The one return road the comment endpoint honors besides its own run
242
- * page: the cockpit's selected-result link, exactly. */
243
- export const REVIEW_RETURN = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}$/;
244
241
  export const PRIORITY_LABELS = { 0: "needs action", 1: "review", 2: "no flags" };
245
242
  export function reviewPriorityOf(row) {
246
243
  if (row.assignment != null && (row.assignment.receipt?.runId ?? null) === row.runId) {
@@ -568,11 +565,6 @@ export function reviewCockpitPage(chrome, data) {
568
565
  functional: { script: reviewEvidenceScript() + (selected !== null && selected.detail !== null ? RESULT_REVIEW_SCRIPT : ""), fetches: false },
569
566
  });
570
567
  }
571
- /** The selected result: intent → proof → changes → publication → acts,
572
- * one scan path, every fact labeled by its source. */
573
- export function reviewCockpitDetail(view, csrf, noted, canRetryReview = false, tab = "summary", user = "") {
574
- return reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user).html;
575
- }
576
568
  export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user, host = { kind: "review" }) {
577
569
  const parts = [];
578
570
  const run = view.run;
@@ -1268,7 +1260,9 @@ export function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Ma
1268
1260
  ? html `<span class="diff-annotate-space"></span>`
1269
1261
  : html `<button type="button" class="diff-annotate pick-line" data-path="${one.path}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${one.path}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
1270
1262
  const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
1271
- return html `<div class="diff-line diff-${line.kind}">${annotate}<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span><code><b aria-hidden="true">${marker}</b>${line.text}</code></div>`;
1263
+ // Each real line names its file and line, so a comment (or a test) can find the exact row.
1264
+ const named = lineNumber === null || line.kind === "meta" ? "" : html ` data-diff-file="${one.path}" data-diff-line="${lineNumber}"`;
1265
+ return html `<div class="diff-line diff-${line.kind}"${named}>${annotate}<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span><code><b aria-hidden="true">${marker}</b>${line.text}</code></div>`;
1272
1266
  })}</div></section>`;
1273
1267
  });
1274
1268
  const anchor = anchors.get(one.path);
@@ -22,17 +22,16 @@ import { starterForWork } from "../flow-starters.js";
22
22
  import {} from "../gaps.js";
23
23
  import { readAuthModeStrict } from "../keys.js";
24
24
  import { failedAttemptSentence, retryNoteOf } from "../needs-you.js";
25
- import { agentsSummary, chosenWords, makesNoPlan, postureWords, projectRoute, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
25
+ import { chosenWords, makesNoPlan, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
26
26
  import { parseExecutionPlanDocument } from "../plan.js";
27
27
  import {} from "../planner-source.js";
28
28
  import { projectName } from "../project.js";
29
29
  import { dispatchStatusToken, manualReviewOnly, plainReasonWords, reviewConflict } from "../proof.js";
30
30
  import { providerName } from "../provider-auth.js";
31
- import { PROVIDER_IDS } from "../provider.js";
31
+ import {} from "../provider.js";
32
32
  import { qualityModeTitle } from "../quality.js";
33
- import {} from "../result-evidence-readers.js";
34
33
  import { evidenceShortenedWords } from "../result-review.js";
35
- import { acceptanceToLines, approvalOf, chainFromJson, scopeAuthorityOf } from "../scope.js";
34
+ import { acceptanceToLines, approvalOf, scopeAuthorityOf } from "../scope.js";
36
35
  import {} from "../store.js";
37
36
  import { runCostWords } from "../summary.js";
38
37
  import {} from "../task-control.js";
@@ -44,7 +43,7 @@ import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SEN
44
43
  import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
45
44
  import { reviewHref, taskChatHref, taskHref } from "./http.js";
46
45
  import { coordinatorProposalsSection } from "./render-chat.js";
47
- import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, terminalDiffCard, verificationExplanation, verificationRecovered } from "./render-results.js";
46
+ import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
48
47
  import {} from "./session.js";
49
48
  import { linkWords, originNote } from "../task-source.js";
50
49
  export function contractChangeHtml(change) {
@@ -91,9 +91,5 @@ export interface RouteDeclaration {
91
91
  export declare const ROUTES: readonly RouteDeclaration[];
92
92
  /** The first row that declares this method and path, or null: an undeclared request never reaches a handler. */
93
93
  export declare function matchRoute(method: string, pathname: string, stage?: Stage): RouteDeclaration | null;
94
- /** True when some row declares this path for another method (a wrong-method request). */
95
- export declare function declaredForAnotherMethod(method: string, pathname: string, stage?: Stage): boolean;
96
94
  /** Refuses a malformed table at startup: duplicate ids, duplicate method+pattern rows, or a row shadowed by an earlier one for its own sample. */
97
95
  export declare function assertRouteTable(routes?: readonly RouteDeclaration[]): void;
98
- /** The effective policy of every row, as plain data — what the snapshot test compares. */
99
- export declare const routePolicySnapshot: () => Array<Omit<RouteDeclaration, "sample">>;
@@ -431,10 +431,6 @@ export function matchRoute(method, pathname, stage) {
431
431
  }
432
432
  return null;
433
433
  }
434
- /** True when some row declares this path for another method (a wrong-method request). */
435
- export function declaredForAnotherMethod(method, pathname, stage) {
436
- return compiled.some(({ route, test }) => (stage === undefined || route.stage === stage) && !methodMatches(route.method, method) && test.test(pathname));
437
- }
438
434
  /** Refuses a malformed table at startup: duplicate ids, duplicate method+pattern rows, or a row shadowed by an earlier one for its own sample. */
439
435
  export function assertRouteTable(routes = ROUTES) {
440
436
  const ids = new Set(), keys = new Set();
@@ -466,5 +462,3 @@ export function assertRouteTable(routes = ROUTES) {
466
462
  }
467
463
  }
468
464
  assertRouteTable();
469
- /** The effective policy of every row, as plain data — what the snapshot test compares. */
470
- export const routePolicySnapshot = () => ROUTES.map(({ sample: _sample, ...rest }) => ({ ...rest }));
@@ -32,7 +32,7 @@ import { budgetLabel, monthOf } from "../spend.js";
32
32
  import { PACKAGE_VERSION } from "../version.js";
33
33
  import { handlersOf } from './handler-registry.js';
34
34
  import { refuse, screen } from "./chrome.js";
35
- import { HANDOFF_STYLE, redirect, respond, safeReturn, signInSpent, startedHere } from "./http.js";
35
+ import { HANDOFF_STYLE, redirect, respond, safeReturn, sayTo, signInSpent, startedHere } from "./http.js";
36
36
  import { html, htmlString, joinHtml, postForm, reauth } from "../html.js";
37
37
  import { browserCaller } from "./session.js";
38
38
  /** settings handlers, moved without changing their route bodies. */
@@ -127,7 +127,7 @@ import { personChip, ROLE_TITLES } from "./chrome.js";
127
127
  import { chatReturnWithSaid, goOutside } from "./http.js";
128
128
  import { latestReleaseFor, leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
129
129
  export function createSettingsHandlers(runtime) {
130
- const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, recordSignIn, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
130
+ const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
131
131
  // v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
132
132
  async function spendPage(ctx) {
133
133
  const { url, who, response, now } = ctx;
@@ -451,7 +451,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
451
451
  // v105: Settings → Data, where an instance operator downloads everything. The download is a POST with the password.
452
452
  async function dataPage(ctx) {
453
453
  const { url, response, project } = ctx;
454
- const who = browserCaller(ctx.who);
454
+ browserCaller(ctx.who);
455
455
  return sendScreen(response, 200, screen("Data", html `<p><a href="/settings">Settings</a></p><h1>Data</h1>${(dataExportHtml({ problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
456
456
  }
457
457
  // v100: Settings → Sign-in, the identity provider people sign in with. An instance operator's page.
@@ -486,7 +486,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
486
486
  const { response, now, posted } = ctx;
487
487
  const who = browserCaller(ctx.who);
488
488
  const body = readForm(posted, CONSOLE_FORMS.passwordless);
489
- const back = (key, words) => redirect(response, `/settings/passwordless?${key}=${encodeURIComponent(words)}`);
489
+ const back = sayTo(response, "/settings/passwordless");
490
490
  const action = body.get("action");
491
491
  if (action === "off") {
492
492
  setPasswordless(store, false, who.name, now);
@@ -812,7 +812,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
812
812
  async function spendBudget(ctx) {
813
813
  const { who, response, now, posted } = ctx;
814
814
  const body = readForm(posted, CONSOLE_FORMS.spendBudget);
815
- const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
815
+ const back = sayTo(response, "/spend");
816
816
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
817
817
  return back("problem", "Enter your Toolroll password to change a budget.");
818
818
  const target = /^(installation|project|person|subagent):(.+)$/.exec(body.get("target") ?? "");
@@ -848,7 +848,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
848
848
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
849
849
  if (who.via !== "cookie" || who.role !== "approver" || !store.accountCanAccess(who.name, repo))
850
850
  return refuse(response, who, 403, "An approver for this project sets how many tasks build at once.", "/settings/project");
851
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
851
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`);
852
852
  const n = parseProjectConcurrency(body.get("concurrency") ?? "");
853
853
  if (n === null)
854
854
  return back("problem", "Builds at once is a whole number from 1 to 64. Nothing changed.");
@@ -870,7 +870,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
870
870
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
871
871
  if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
872
872
  return refuse(response, who, 403, "An approver for this project changes the live view.", "/settings/project");
873
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
873
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`);
874
874
  const act = body.get("act");
875
875
  if (act === "off")
876
876
  return back("said", setLiveCommands(store, { repo, commands: false, by: who.name, via: "the console" }, now).said);
@@ -893,7 +893,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
893
893
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
894
894
  if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
895
895
  return refuse(response, who, 403, "An approver for this project changes what its builds may reach.", "/settings/project");
896
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#network`);
896
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#network");
897
897
  const hosts = (body.get("hosts") ?? "").split(/[\s,]+/).filter(Boolean).map(normalizeHost);
898
898
  if (hosts.some(one => one === null))
899
899
  return back("problem", "A host is a name like api.stripe.com or *.stripe.com: no address, port or path. Nothing changed.");
@@ -942,7 +942,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
942
942
  async function policySave(ctx) {
943
943
  const { who, response, now, posted } = ctx;
944
944
  const body = readForm(posted, CONSOLE_FORMS.policy);
945
- const back = (key, words) => redirect(response, `/settings/policy?${key}=${encodeURIComponent(words)}`);
945
+ const back = sayTo(response, "/settings/policy");
946
946
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
947
947
  return back("problem", "That password didn't match. Nothing changed.");
948
948
  const chosen = body.getAll("provider");
@@ -962,7 +962,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
962
962
  const known = consoleProjects();
963
963
  if (!known.includes(repo))
964
964
  return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
965
- const back = (key, words) => redirect(response, `/settings/approval?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
965
+ const back = sayTo(response, `/settings/approval?repo=${encodeURIComponent(repo)}`);
966
966
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
967
967
  return back("problem", "That password didn't match. Nothing changed.");
968
968
  const protect = body.get("protect") ?? "none";
@@ -980,7 +980,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
980
980
  async function requestLimitsSave(ctx) {
981
981
  const { who, response, now, posted } = ctx;
982
982
  const body = readForm(posted, CONSOLE_FORMS.requestLimits);
983
- const back = (key, words) => redirect(response, `/settings/sessions?${key}=${encodeURIComponent(words)}`);
983
+ const back = sayTo(response, "/settings/sessions");
984
984
  const target = body.get("target") ?? "";
985
985
  const token = target === "*" ? null : store.apiTokens(null).find(one => one.id === target && one.revokedAt === null) ?? null;
986
986
  if (target !== "*" && token === null)
@@ -1004,7 +1004,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1004
1004
  const body = readForm(posted, CONSOLE_FORMS.signIn);
1005
1005
  if (!options.configDir)
1006
1006
  return refuse(response, who, 403, "An instance operator sets up sign-in.", "/settings");
1007
- const back = (key, words) => redirect(response, `/settings/sign-in?${key}=${encodeURIComponent(words)}`);
1007
+ const back = sayTo(response, "/settings/sign-in");
1008
1008
  const action = body.get("action") ?? "";
1009
1009
  const before = ssoSettings();
1010
1010
  if (action === "test") {
@@ -1046,7 +1046,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1046
1046
  if (databaseFile === null)
1047
1047
  return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
1048
1048
  const stateDir = dirname(databaseFile);
1049
- const back = (key, words) => redirect(response, `/settings/updates?${key}=${encodeURIComponent(words)}`);
1049
+ const back = sayTo(response, "/settings/updates");
1050
1050
  if (op === "seen") {
1051
1051
  markWhatsNewSeen(stateDir);
1052
1052
  return redirect(response, "/settings/updates");
@@ -1090,7 +1090,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1090
1090
  async function retentionSave(ctx) {
1091
1091
  const { who, response, now, posted } = ctx;
1092
1092
  const body = readForm(posted, CONSOLE_FORMS.retention);
1093
- const back = (key, words) => redirect(response, `/settings/retention?${key}=${encodeURIComponent(words)}`);
1093
+ const back = sayTo(response, "/settings/retention");
1094
1094
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1095
1095
  return back("problem", "Enter your Toolroll password to change retention.");
1096
1096
  const before = store.retentionPeriods();
@@ -1112,7 +1112,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1112
1112
  const databaseFile = store.databaseFile();
1113
1113
  if (databaseFile === null)
1114
1114
  return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
1115
- const back = (key, words) => redirect(response, `/settings/storage?${key}=${encodeURIComponent(words)}`);
1115
+ const back = sayTo(response, "/settings/storage");
1116
1116
  const what = op === "clean" ? "clean up" : op === "discard" ? "discard a checkout" : "change checkout cleanup";
1117
1117
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1118
1118
  return back("problem", `Enter your Toolroll password to ${what}.`);
@@ -1152,7 +1152,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1152
1152
  return refuse(response, who, 404, "No such project.", "/projects");
1153
1153
  if (who.role !== "approver" || store.isDemo())
1154
1154
  return refuse(response, who, 403, "An approver sets up pull requests.", "/projects");
1155
- const back = (key, words) => redirect(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
1155
+ const back = sayTo(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}`);
1156
1156
  const act = body.get("act");
1157
1157
  if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
1158
1158
  return back("problem", "Enter your Toolroll password to change pull requests.");
@@ -1193,7 +1193,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1193
1193
  return refuse(response, who, 404, "No such project.", "/projects");
1194
1194
  if (who.role !== "approver" || store.isDemo())
1195
1195
  return refuse(response, who, 403, "An approver sets a project's checks.", "/projects");
1196
- const back = (key, words) => redirect(response, `/settings/checks?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
1196
+ const back = sayTo(response, `/settings/checks?repo=${encodeURIComponent(repo)}`);
1197
1197
  if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
1198
1198
  return back("problem", "Enter your Toolroll password to change checks.");
1199
1199
  const act = body.get("act");
@@ -1256,7 +1256,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1256
1256
  const databaseFile = store.databaseFile();
1257
1257
  if (databaseFile === null)
1258
1258
  return refuse(response, who, 403, "An instance operator looks after backups.", "/settings");
1259
- const back = (key, words) => redirect(response, `/settings/backups?${key}=${encodeURIComponent(words)}`);
1259
+ const back = sayTo(response, "/settings/backups");
1260
1260
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1261
1261
  return back("problem", "Enter your Toolroll password to change backups.");
1262
1262
  const every = body.get("every") ?? "";
@@ -1296,7 +1296,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1296
1296
  const { who, response, posted } = ctx;
1297
1297
  const body = readForm(posted, CONSOLE_FORMS.flowsOn);
1298
1298
  const repo = body.get("repo") ?? "";
1299
- const back = (key, words) => redirect(response, `/settings/flows?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
1299
+ const back = sayTo(response, `/settings/flows?repo=${encodeURIComponent(repo)}`);
1300
1300
  const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
1301
1301
  if (store.isDemo())
1302
1302
  return refuse(response, who, 403, "An approver switches starter flows on.", "/settings/flows");
@@ -1315,7 +1315,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1315
1315
  if (!options.configDir)
1316
1316
  return refuse(response, who, 403, "An installation approver tests integrations.", "/settings");
1317
1317
  const key = body.get("key") ?? "";
1318
- const back = (name, words) => redirect(response, `/settings/integrations?${name}=${encodeURIComponent(words)}`);
1318
+ const back = sayTo(response, "/settings/integrations");
1319
1319
  const checked = (await integrations.check([key])).find(one => one.key === key);
1320
1320
  if (checked === undefined)
1321
1321
  return back("problem", "That integration isn't here any more.");
@@ -1328,7 +1328,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1328
1328
  const body = readForm(posted, CONSOLE_FORMS.monitoring);
1329
1329
  if (!options.configDir)
1330
1330
  return refuse(response, who, 403, "An instance operator sets up monitoring.", "/settings");
1331
- const back = (key, words) => redirect(response, `/settings/monitoring?${key}=${encodeURIComponent(words)}`);
1331
+ const back = sayTo(response, "/settings/monitoring");
1332
1332
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1333
1333
  return back("problem", "Enter your Toolroll password to change monitoring.");
1334
1334
  const before = readMonitoring(options.configDir);
@@ -1429,7 +1429,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1429
1429
  const repo = body.get("repo") ?? "";
1430
1430
  if (!visible(repo) || !consoleProjects().includes(repo))
1431
1431
  return refuse(response, who, 403, "That project is outside your access.", "/projects");
1432
- const back = (key, words, anchor = "") => redirect(response, `/settings/tools?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}${anchor}`);
1432
+ const back = sayTo(response, `/settings/tools?repo=${encodeURIComponent(repo)}`);
1433
1433
  // As for Connect: a post from a page that showed another project is refused, never applied here.
1434
1434
  const shown = body.get("shown") ?? "";
1435
1435
  if (shown !== repo) {
@@ -2578,7 +2578,7 @@ ${modeWords(terms)
2578
2578
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
2579
2579
  if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
2580
2580
  return refuse(response, who, 403, "An approver for this project changes where its builds may write.", "/settings/project");
2581
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#writes`);
2581
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#writes");
2582
2582
  const wanted = body.get("writes");
2583
2583
  if (!isWritesMode(wanted))
2584
2584
  return back("problem", "Choose where builds may write. Nothing changed.");
@@ -3,7 +3,7 @@ import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
3
3
  import { originOf } from "../task-source.js";
4
4
  import { taskBaseOf } from "../task-base.js";
5
5
  import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
6
- import { html, htmlString, joinHtml } from "../html.js";
6
+ import { html, joinHtml } from "../html.js";
7
7
  import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
8
8
  import { assignmentOf } from '../assignment.js';
9
9
  import { knowledgeContextHtml } from "../knowledge-ui.js";
@@ -11,7 +11,7 @@ import { handlersOf } from './handler-registry.js';
11
11
  import { closedQuestionWords } from "../decision.js";
12
12
  import { DEPRECATED_PAGE } from "../deprecations.js";
13
13
  /** tasks handlers, moved without changing their route bodies. */
14
- import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
14
+ import { html, htmlString, joinHtml, textHtml } from "../html.js";
15
15
  import { createHash, randomBytes, randomUUID } from "node:crypto";
16
16
  import { lstatSync, realpathSync } from "node:fs";
17
17
  import {} from "node:http";
@@ -40,7 +40,7 @@ import { isTaskSize, PHASES as ROUTE_PHASES } from "../phase-routing.js";
40
40
  import { applyModeToNewFiling, authorizePlanUnderMode } from "../plan-auto.js";
41
41
  import { milestonesOf, parseExecutionPlanDocument } from "../plan.js";
42
42
  import { levelOfProfile } from "../policy.js";
43
- import { authorizedProject, canonicalProject, projectName, rowVisible } from "../project.js";
43
+ import { authorizedProject, canonicalProject, projectName, } from "../project.js";
44
44
  import { fileTaskProposal } from "../proposal.js";
45
45
  import { isProviderId, PROVIDER_IDS, validateSpec } from "../provider.js";
46
46
  import { completeAndOpenPullRequest, mergeAsPerson } from '../pull-request-flow.js';
@@ -69,7 +69,7 @@ import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReview
69
69
  import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
70
70
  import {} from "./session.js";
71
71
  export function createTasksHandlers(runtime) {
72
- const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, taskRooms, identify, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
72
+ const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
73
73
  /** A row admitted the address but its path parameter does not parse: the console's unknown page. */
74
74
  function noPage(who, response) {
75
75
  return refuse(response, who, 404, "There's no page at this address.", "/chat");
@@ -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;
19
18
  ok: false;
20
19
  message: string;
21
20
  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;
34
33
  ok: false;
35
34
  message: string;
36
35
  plan?: never;
36
+ content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
package/dist/skills.d.ts CHANGED
@@ -33,12 +33,6 @@ export declare const CONTEXT_END = "<!-- standing-orders:end -->";
33
33
  export declare function skillContent(): string;
34
34
  /** The AGENTS.md block, marker-fenced so reinstalls replace only themselves. */
35
35
  export declare function contextBlock(): string;
36
- /**
37
- * Whether a repository has the Toolroll skill: a SKILL.md in the current
38
- * folder, or in the one installs before the rename used. The setup guide
39
- * and the console's first-run checklist both ask this, so they agree.
40
- */
41
- export declare function projectSkillInstalled(repo: string): boolean;
42
36
  export type InstallPlan = {
43
37
  skillPath: string;
44
38
  skillAction: "create" | "replace" | "refuse-foreign";
package/dist/skills.js CHANGED
@@ -72,14 +72,6 @@ in the plane: read \`skills get knowledge\`, search it with
72
72
  merge anything yourself.
73
73
  ${CONTEXT_END}`;
74
74
  }
75
- /**
76
- * Whether a repository has the Toolroll skill: a SKILL.md in the current
77
- * folder, or in the one installs before the rename used. The setup guide
78
- * and the console's first-run checklist both ask this, so they agree.
79
- */
80
- export function projectSkillInstalled(repo) {
81
- return [SKILL_DIR, LEGACY_SKILL_DIR].some(dir => existsSync(join(repo, dir, SKILL_FILE)));
82
- }
83
75
  /** What install would do, computed without writing anything. */
84
76
  export function planInstall(repo, writeContext) {
85
77
  const skillPath = join(repo, SKILL_DIR, SKILL_FILE);
@@ -1,4 +1,4 @@
1
- import { ChatState, type ChatBinding, type ChatContent, type ChatEvent } from "./chat-delivery-state.js";
1
+ import { ChatState } from "./chat-delivery-state.js";
2
2
  import { uploadSlackBytes, type SlackApi, type SlackIdentity } from "./slack-api.js";
3
3
  import { type Store } from "./store.js";
4
4
  import type { SubscriptionLeadRunner } from "./subscription-chat.js";
@@ -19,12 +19,6 @@ export type SlackChatOptions = {
19
19
  /** Synchronous receipt before ACK. Never stores response_url, raw tokens, or a pairing code. */
20
20
  export declare function receiveSlack(state: ChatState, identity: SlackIdentity, type: string, raw: unknown, now: Date): boolean;
21
21
  export declare const processSlackEvent: (options: SlackChatOptions) => Promise<boolean>;
22
- export declare const applySlackAction: (options: SlackChatOptions, event: ChatEvent, binding: ChatBinding, repos: readonly string[]) => {
23
- merge: import("./chat-decide.js").ChatMerge;
24
- part: number;
25
- shown: string;
26
- content: ChatContent;
27
- } | null;
28
22
  export declare const planSlackNotifications: (options: SlackChatOptions) => Promise<void>;
29
23
  export declare const planSlackRooms: (options: SlackChatOptions) => Promise<void>;
30
24
  export declare function slackBlocks(text: string, buttons?: Record<string, unknown>[]): Record<string, unknown>[];
@@ -2,9 +2,8 @@ import { chatFlowButtons } from "./chat-flow.js";
2
2
  import { refuseResultShots, resultShotsPruned, shotWaitsUntil } from "./result-shots.js";
3
3
  import { chatQuestionButtons } from "./subagent-question.js";
4
4
  import { chatAskButtons } from "./chat-ask.js";
5
- import { channelInbox } from "./chat-inbox.js";
6
- import { roomCommand } from "./chat-rooms.js";
7
- import { processChatEvent, applyChatAction, planChatNotifications, channelAccess, planRoomMessages, heldForWatch, } from "./chat-delivery.js";
5
+ import { admitChatEvent } from "./chat-admission.js";
6
+ import { processChatEvent, planChatNotifications, channelAccess, planRoomMessages, heldForWatch, } from "./chat-delivery.js";
8
7
  /** Slack is a transport for the same saved assistant and confirmation doors. */
9
8
  import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
10
9
  import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
@@ -100,21 +99,10 @@ export function receiveSlack(state, identity, type, raw, now) {
100
99
  // A channel or private group is a room: accepted only while it follows a
101
100
  // conversation, or for the `/team` words that make it follow one.
102
101
  const isRoom = !slackId(channel, "D");
103
- const words = "text" in payload ? payload.text ?? "" : "";
104
- // v89: a channel that feeds a flow takes anyone's message (as a card, with no say over anything); "flow 12" connects one.
105
- const inbox = isRoom && kind === "message" ? channelInbox(state.store, "slack", identity.installation, String(channel), words) : { watched: false, command: false };
106
- const roomish = isRoom && (state.room(identity.installation, String(channel)) !== null || (kind === "message" && roomCommand(words) !== null) || inbox.watched || inbox.command);
107
- if (isRoom && !roomish)
108
- return false;
109
- const binding = state.bindingFor(identity.installation, member);
110
- const open = inbox.watched && !inbox.command;
111
- if (kind !== "pair" && !open &&
112
- (!binding ||
113
- !state.live(binding) ||
114
- (binding.channel !== channel && !roomish)))
115
- return false;
116
- if (kind === "pair" && (binding || isRoom))
102
+ const admitted = admitChatEvent(state, "slack", identity.installation, { kind, member, channel, isRoom, payload });
103
+ if (admitted === null)
117
104
  return false;
105
+ const { binding } = admitted;
118
106
  return state.enqueue({
119
107
  id,
120
108
  installation: identity.installation,
@@ -139,7 +127,6 @@ const delivery = (options) => ({
139
127
  }),
140
128
  });
141
129
  export const processSlackEvent = (options) => processChatEvent(delivery(options));
142
- export const applySlackAction = (options, event, binding, repos) => applyChatAction(delivery(options), event, binding, repos);
143
130
  export const planSlackNotifications = (options) => planChatNotifications(delivery(options));
144
131
  export const planSlackRooms = (options) => planRoomMessages(delivery(options));
145
132
  const access = (options, binding, ceiling) => channelAccess(delivery(options), binding, ceiling);
@@ -1,8 +1,4 @@
1
1
  import type { Store } from "./store.js";
2
2
  import { type Html } from "./html.js";
3
- export declare function slackSettingsHtml(store: Store, dir: string, options?: {
4
- code?: string;
5
- problem?: string;
6
- now?: Date;
7
- who?: string;
8
- }): Html;
3
+ import { type ChatSettingsOptions } from "./chat-settings-page.js";
4
+ export declare function slackSettingsHtml(store: Store, dir: string, options?: ChatSettingsOptions): Html;
@@ -1,41 +1,13 @@
1
- import { loadPrimary } from "./webhooks.js";
2
- import { html, postForm } from "./html.js";
1
+ import { html } from "./html.js";
3
2
  import { loadSlackCredentials } from "./slack-api.js";
4
- import { ChatState } from "./chat-delivery-state.js";
3
+ import { chatSettingsHtml } from "./chat-settings-page.js";
5
4
  export function slackSettingsHtml(store, dir, options = {}) {
6
- const credentials = loadSlackCredentials(dir), state = new ChatState(store, "slack"), bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [], binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null, others = bindings.length - (binding === null ? 0 : 1);
7
- const runtime = credentials ? state.runtime(credentials.installation) : null;
8
- const now = options.now ?? new Date(), live = runtime &&
9
- runtime.leaseUntil !== null &&
10
- runtime.leaseUntil > now.toISOString() &&
11
- runtime.connected;
12
- const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
13
- const post = (action, content) => postForm(`/settings/slack/${action}`, content, { attrs: { class: "card" } });
14
- const parts = [];
15
- if (!credentials) {
16
- parts.push(html `<p>Manage projects and review results in a private Slack conversation.</p>`, html `<details open><summary>Create your Slack app</summary><ol><li>Download the <a href="/settings/slack/manifest">app manifest</a>, then <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code>connections:write</code>.</li><li>Under OAuth &amp; Permissions, install the app and copy its bot token.</li></ol></details>`, post("connect", html `<label>App token<input style="min-height:44px" type="password" name="app-token" autocomplete="off" placeholder="xapp-…" required></label><label>Bot token<input style="min-height:44px" type="password" name="bot-token" autocomplete="off" placeholder="xoxb-…" required></label>${password}<p class="meta">Tokens stay on this installation. Next, pair your own Slack account.</p><button type="submit">Connect Slack</button>`));
17
- }
18
- else {
19
- parts.push(html `<p><strong>${credentials.workspace}</strong> · ${live ? "Connected" : "Waiting for connection"}</p>`);
20
- if (runtime?.problem)
21
- parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
22
- if (!live && !runtime?.problem)
23
- parts.push(html `<p>Keep the Toolroll worker running to receive Slack messages.</p>`);
24
- if (options.code) {
25
- parts.push(html `<h2>Pair your account</h2><p>Send this in a direct message to your Toolroll Slack app. It expires in 10 minutes.</p>`, html `<label>Pairing message<input type="text" readonly value="pair ${options.code}" autocomplete="off" style="width:100%;max-width:100%;font-size:14px;min-height:44px"></label>`, html `<a class="button-link" style="min-height:44px;white-space:nowrap" href="/settings/slack">Check connection</a>`);
26
- }
27
- else if (binding && state.live(binding)) {
28
- const { pending, dropped: failed } = state.partCounts(binding.id);
29
- parts.push(html `<p>Your Slack account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}${pending ? ` ${pending} replies waiting to send.` : ""}${failed ? ` ${failed} replies could not be delivered. Open the saved chat to recover them.` : ""}</p>`, html `<p>Try “What needs my attention?” or “Show the evidence for the latest result.”</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === "slack"
30
- ? html `<p>Task updates are sent here.</p>`
31
- : post("alerts", html `<button type="submit">Send task updates here</button>`));
32
- }
33
- else {
34
- parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired Slack account can read your connected projects and confirm proposed changes. Password approvals still open in Toolroll.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} already paired.` : ""}</p>${password}<button type="submit">Create pairing code</button>`));
35
- }
36
- if (binding && state.live(binding) && !options.code)
37
- parts.push(post("unpair", html `${password}<p class="meta">Unpairing ends every open button in your chat. Teammates are unaffected.</p><button type="submit">Unpair my account</button>`));
38
- parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect Slack</button>`)}</details>`);
39
- }
40
- return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>Slack</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}<details><summary>Access and setup</summary><p>Private messages and threads use your Toolroll permissions. Shared channels are not supported. Secure review links use this installation’s configured HTTPS console address.</p><p>Socket Mode connects out to Slack. It does not need a public webhook or a Tailscale account.</p></details></section>`;
5
+ return chatSettingsHtml(store, dir, options, {
6
+ provider: "slack",
7
+ label: "Slack",
8
+ credentials: loadSlackCredentials(dir),
9
+ setup: html `<details open><summary>Create your Slack app</summary><ol><li>Download the <a href="/settings/slack/manifest">app manifest</a>, then <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code>connections:write</code>.</li><li>Under OAuth &amp; Permissions, install the app and copy its bot token.</li></ol></details>`,
10
+ connect: password => html `<label>App token<input style="min-height:44px" type="password" name="app-token" autocomplete="off" placeholder="xapp-…" required></label><label>Bot token<input style="min-height:44px" type="password" name="bot-token" autocomplete="off" placeholder="xoxb-…" required></label>${password}<p class="meta">Tokens stay on this installation. Next, pair your own Slack account.</p><button type="submit">Connect Slack</button>`,
11
+ access: html `<p>Private messages and threads use your Toolroll permissions. Shared channels are not supported. Secure review links use this installation’s configured HTTPS console address.</p><p>Socket Mode connects out to Slack. It does not need a public webhook or a Tailscale account.</p>`,
12
+ });
41
13
  }
package/dist/slack.js CHANGED
@@ -1,19 +1,12 @@
1
1
  /** One leased Socket Mode connection, hosted by the ordinary worker. */
2
2
  import { randomBytes } from "node:crypto";
3
- import { setTimeout as sleep } from "node:timers/promises";
3
+ import { abortablePause, workChatQueue } from "./chat-follow.js";
4
4
  import { loadSlackCredentials, slackApi } from "./slack-api.js";
5
5
  import { ChatState } from "./chat-delivery-state.js";
6
6
  import { receiveSlack, processSlackEvent, deliverSlackPart, planSlackNotifications, planSlackRooms, } from "./slack-chat.js";
7
7
  export async function followSlack(options) {
8
8
  const owner = randomBytes(16).toString("hex"), state = new ChatState(options.store, "slack");
9
- const pause = async (ms) => {
10
- try {
11
- await sleep(ms, undefined, { signal: options.signal });
12
- }
13
- catch {
14
- /* shutdown */
15
- }
16
- };
9
+ const pause = abortablePause(options.signal);
17
10
  while (!options.signal.aborted) {
18
11
  const credentials = loadSlackCredentials(options.dir);
19
12
  if (!credentials ||
@@ -104,20 +97,16 @@ export async function followSlack(options) {
104
97
  };
105
98
  try {
106
99
  await client.start();
107
- while (same()) {
108
- if (!connected || state.retryAt(credentials.installation) > new Date().toISOString()) {
109
- await pause(1000);
110
- continue;
111
- }
112
- await processSlackEvent(chat);
113
- if (same())
114
- await planSlackRooms(chat);
115
- if (same())
116
- await deliverSlackPart(chat);
117
- if (same() && options.notifications())
118
- await planSlackNotifications(chat);
119
- await pause(1000);
120
- }
100
+ await workChatQueue({
101
+ same,
102
+ ready: () => connected && !(state.retryAt(credentials.installation) > new Date().toISOString()),
103
+ pause,
104
+ notifications: () => options.notifications(),
105
+ process: () => processSlackEvent(chat),
106
+ rooms: () => planSlackRooms(chat),
107
+ deliver: () => deliverSlackPart(chat),
108
+ notify: () => planSlackNotifications(chat),
109
+ });
121
110
  }
122
111
  catch {
123
112
  if (alive)