toolroll 0.9.85 → 0.9.87

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 (220) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/assignment.js +4 -3
  10. package/dist/backup-ui.d.ts +5 -4
  11. package/dist/backup-ui.js +65 -32
  12. package/dist/browser/terminal.css +1 -1
  13. package/dist/browser/terminal.js +57 -54
  14. package/dist/browser/workspace.css +2 -2
  15. package/dist/browser/workspace.js +35 -68
  16. package/dist/browser-crew.js +1 -1
  17. package/dist/browser-workspace.d.ts +363 -177
  18. package/dist/browser-workspace.js +10 -9
  19. package/dist/chat-actions.d.ts +11 -1
  20. package/dist/chat-actions.js +53 -1
  21. package/dist/chat-channel.js +1 -0
  22. package/dist/chat-controls.d.ts +1 -1
  23. package/dist/chat-controls.js +1 -1
  24. package/dist/chat-quiet.js +3 -2
  25. package/dist/chat-settings-page.d.ts +29 -9
  26. package/dist/chat-settings-page.js +59 -28
  27. package/dist/check-levels-ui.d.ts +2 -1
  28. package/dist/check-levels-ui.js +67 -58
  29. package/dist/claim.d.ts +3 -0
  30. package/dist/claim.js +4 -1
  31. package/dist/contracts/chat-actions.d.ts +9 -0
  32. package/dist/contracts/chat-actions.js +3 -0
  33. package/dist/contracts/cli.d.ts +43 -0
  34. package/dist/contracts/cli.js +4 -0
  35. package/dist/contracts/console-api.d.ts +4 -1
  36. package/dist/contracts/console-api.js +4 -1
  37. package/dist/contracts/flow-propose.d.ts +4 -1
  38. package/dist/contracts/flow-propose.js +1 -1
  39. package/dist/contracts/flow.d.ts +41 -0
  40. package/dist/contracts/flow.js +4 -2
  41. package/dist/contracts/lead-tools.d.ts +21 -0
  42. package/dist/contracts/lead-tools.js +2 -0
  43. package/dist/contracts/store-json.d.ts +2 -0
  44. package/dist/contracts/trigger-payloads.d.ts +20 -0
  45. package/dist/contracts/trigger-payloads.js +4 -0
  46. package/dist/coordinator.js +1 -0
  47. package/dist/credentials-ui.d.ts +5 -3
  48. package/dist/credentials-ui.js +58 -31
  49. package/dist/deploy-gate.js +4 -0
  50. package/dist/design/kit.css +685 -0
  51. package/dist/design/kit.d.ts +208 -0
  52. package/dist/design/kit.js +177 -0
  53. package/dist/design/tokens.css +19 -1
  54. package/dist/discord-settings.js +8 -4
  55. package/dist/dispatch.d.ts +3 -0
  56. package/dist/dispatch.js +4 -1
  57. package/dist/export-ui.d.ts +6 -4
  58. package/dist/export-ui.js +18 -8
  59. package/dist/flow-browser.d.ts +103 -0
  60. package/dist/flow-browser.js +204 -0
  61. package/dist/flow-changed.d.ts +14 -0
  62. package/dist/flow-changed.js +53 -0
  63. package/dist/flow-code.d.ts +5 -1
  64. package/dist/flow-code.js +4 -1
  65. package/dist/flow-engine.js +16 -0
  66. package/dist/flow-gallery-ui.d.ts +41 -4
  67. package/dist/flow-gallery-ui.js +95 -63
  68. package/dist/flow-live.js +9 -1
  69. package/dist/flow-pull-request.js +15 -7
  70. package/dist/flow-share.js +3 -1
  71. package/dist/flow-starters-ui.d.ts +3 -9
  72. package/dist/flow-starters-ui.js +12 -23
  73. package/dist/flow-steps.d.ts +9 -1
  74. package/dist/flow-steps.js +55 -5
  75. package/dist/flow-triggers.js +55 -0
  76. package/dist/flow-unattended-words.d.ts +20 -0
  77. package/dist/flow-unattended-words.js +24 -0
  78. package/dist/flow-unattended.d.ts +9 -5
  79. package/dist/flow-unattended.js +45 -23
  80. package/dist/flows-cli.d.ts +19 -0
  81. package/dist/flows-cli.js +54 -5
  82. package/dist/flows-ui.d.ts +12 -6
  83. package/dist/flows-ui.js +152 -32
  84. package/dist/flows.js +5 -2
  85. package/dist/integrations-ui.d.ts +6 -4
  86. package/dist/integrations-ui.js +34 -40
  87. package/dist/integrations.d.ts +2 -2
  88. package/dist/integrations.js +4 -4
  89. package/dist/kits-ui.d.ts +8 -10
  90. package/dist/kits-ui.js +29 -33
  91. package/dist/kits.js +1 -1
  92. package/dist/knowledge-ui.js +76 -19
  93. package/dist/lead-tools.js +35 -6
  94. package/dist/lead-voice.js +16 -5
  95. package/dist/ledger-view.d.ts +2 -0
  96. package/dist/ledger-view.js +41 -38
  97. package/dist/models-ui.d.ts +8 -3
  98. package/dist/models-ui.js +82 -59
  99. package/dist/monitoring-ui.d.ts +4 -2
  100. package/dist/monitoring-ui.js +44 -13
  101. package/dist/needs-you.d.ts +1 -1
  102. package/dist/needs-you.js +8 -0
  103. package/dist/operate-remote-arguments.js +1 -0
  104. package/dist/operate-remote.js +1 -1
  105. package/dist/operate.d.ts +1 -1
  106. package/dist/operate.js +249 -6
  107. package/dist/passwordless.d.ts +1 -1
  108. package/dist/passwordless.js +1 -1
  109. package/dist/people-audit-ui.d.ts +2 -1
  110. package/dist/people-audit-ui.js +25 -23
  111. package/dist/policy-ui.d.ts +3 -2
  112. package/dist/policy-ui.js +27 -21
  113. package/dist/policy.d.ts +3 -0
  114. package/dist/policy.js +14 -1
  115. package/dist/project-delete-ui.d.ts +4 -2
  116. package/dist/project-delete-ui.js +135 -126
  117. package/dist/project-delete.js +7 -3
  118. package/dist/project-knowledge.d.ts +10 -10
  119. package/dist/publish.d.ts +11 -0
  120. package/dist/publish.js +16 -3
  121. package/dist/pull-request-flow.js +35 -4
  122. package/dist/pull-request-ui.d.ts +2 -1
  123. package/dist/pull-request-ui.js +39 -28
  124. package/dist/recipe-ui.d.ts +4 -2
  125. package/dist/recipe-ui.js +14 -4
  126. package/dist/release-adapters.d.ts +3 -0
  127. package/dist/release-adapters.js +22 -2
  128. package/dist/release-cli.js +81 -9
  129. package/dist/release.d.ts +21 -0
  130. package/dist/release.js +40 -4
  131. package/dist/releases.js +1 -1
  132. package/dist/request-budget-ui.d.ts +4 -2
  133. package/dist/request-budget-ui.js +30 -19
  134. package/dist/result-follow-ups.js +4 -0
  135. package/dist/retention-ui.d.ts +4 -3
  136. package/dist/retention-ui.js +15 -12
  137. package/dist/scope.d.ts +8 -0
  138. package/dist/scope.js +13 -0
  139. package/dist/serve.d.ts +0 -1
  140. package/dist/serve.js +51 -7
  141. package/dist/server/agents-computers.d.ts +9 -0
  142. package/dist/server/agents-computers.js +67 -0
  143. package/dist/server/chat.js +20 -18
  144. package/dist/server/chrome.d.ts +1 -1
  145. package/dist/server/chrome.js +19 -9
  146. package/dist/server/console.css +31 -2
  147. package/dist/server/flows.js +79 -36
  148. package/dist/server/home.d.ts +35 -0
  149. package/dist/server/home.js +104 -0
  150. package/dist/server/http.d.ts +3 -0
  151. package/dist/server/http.js +5 -0
  152. package/dist/server/kit-standalone.d.ts +22 -0
  153. package/dist/server/kit-standalone.js +69 -0
  154. package/dist/server/kit.d.ts +405 -0
  155. package/dist/server/kit.js +361 -0
  156. package/dist/server/pages.d.ts +2 -1
  157. package/dist/server/pages.js +213 -174
  158. package/dist/server/people-tokens.js +26 -85
  159. package/dist/server/render-history.d.ts +51 -0
  160. package/dist/server/render-history.js +228 -0
  161. package/dist/server/render-new-task.d.ts +29 -0
  162. package/dist/server/render-new-task.js +96 -0
  163. package/dist/server/render-next.d.ts +63 -0
  164. package/dist/server/render-next.js +109 -0
  165. package/dist/server/render-pages.d.ts +35 -190
  166. package/dist/server/render-pages.js +154 -563
  167. package/dist/server/render-people.d.ts +24 -3
  168. package/dist/server/render-people.js +93 -72
  169. package/dist/server/render-projects.d.ts +103 -0
  170. package/dist/server/render-projects.js +247 -0
  171. package/dist/server/render-results.d.ts +5 -24
  172. package/dist/server/render-results.js +48 -185
  173. package/dist/server/render-settings.d.ts +5 -74
  174. package/dist/server/render-settings.js +102 -493
  175. package/dist/server/render-tasks.d.ts +43 -5
  176. package/dist/server/render-tasks.js +131 -91
  177. package/dist/server/route-table.js +31 -5
  178. package/dist/server/settings.js +276 -255
  179. package/dist/server/task-views.d.ts +8 -0
  180. package/dist/server/task-views.js +90 -2
  181. package/dist/server/tasks.d.ts +18 -0
  182. package/dist/server/tasks.js +148 -41
  183. package/dist/server/terminals.js +1 -1
  184. package/dist/server/welcome.d.ts +22 -0
  185. package/dist/server/welcome.js +69 -0
  186. package/dist/settings-groups-ui.d.ts +279 -0
  187. package/dist/settings-groups-ui.js +615 -0
  188. package/dist/settings-kit.d.ts +42 -6
  189. package/dist/settings-kit.js +69 -7
  190. package/dist/settings-proofs.js +1 -0
  191. package/dist/setup-guide.d.ts +2 -2
  192. package/dist/skills-ui.js +59 -7
  193. package/dist/slack-settings.js +12 -5
  194. package/dist/spend-ui.d.ts +45 -26
  195. package/dist/spend-ui.js +124 -56
  196. package/dist/sso-ui.d.ts +5 -2
  197. package/dist/sso-ui.js +63 -31
  198. package/dist/storage-ui.d.ts +3 -2
  199. package/dist/storage-ui.js +56 -50
  200. package/dist/store.d.ts +182 -5
  201. package/dist/store.js +512 -38
  202. package/dist/surface.js +23 -3
  203. package/dist/task-archive.d.ts +10 -0
  204. package/dist/task-archive.js +16 -0
  205. package/dist/task-control.d.ts +1 -1
  206. package/dist/teams-settings.d.ts +2 -1
  207. package/dist/teams-settings.js +24 -41
  208. package/dist/telegram-settings.d.ts +0 -4
  209. package/dist/telegram-settings.js +52 -25
  210. package/dist/tidy.d.ts +130 -0
  211. package/dist/tidy.js +503 -0
  212. package/dist/toolroll-update-ui.d.ts +8 -4
  213. package/dist/toolroll-update-ui.js +55 -45
  214. package/dist/tools-ui.d.ts +5 -3
  215. package/dist/tools-ui.js +107 -42
  216. package/dist/work-index.d.ts +24 -0
  217. package/dist/work-index.js +56 -30
  218. package/dist/workspace-ui.d.ts +1 -1
  219. package/dist/workspace-ui.js +8 -4
  220. package/package.json +1 -1
package/dist/surface.js CHANGED
@@ -165,13 +165,17 @@ const GUIDE_ROWS = [
165
165
  positionals: [{ name: "task", required: true, meaning: "any version of the task" }],
166
166
  flags: [jsonFlag, dbFlag, { name: "level", takesValue: true, meaning: "quick (default) or full" }],
167
167
  notableReasons: ["no-result", "no-check", "checks-failed"] },
168
+ { invocation: "task ship", synopsis: "the task's pull request checks as last observed for its head: each check's state and how long it took",
169
+ audience: "agent", agentMayInvoke: true, mutation: "none",
170
+ positionals: [{ name: "task", required: true, meaning: "any version of the task" }], flags: [jsonFlag, dbFlag],
171
+ notableReasons: ["unknown-task", "no-pull-request"] },
168
172
  { invocation: "task add-tests", synopsis: "file a small task to write tests for the task's latest change; it waits for approval like any task",
169
173
  audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
170
174
  positionals: [{ name: "task", required: true, meaning: "any version of the task" }], flags: [jsonFlag, dbFlag],
171
175
  notableReasons: ["no-result"] },
172
176
  { invocation: "task list", synopsis: "a page of saved task status", audience: "agent", agentMayInvoke: true, mutation: "none",
173
177
  flags: [jsonFlag, dbFlag, { name: "state", takesValue: true, meaning: "queued|running|done|failed|cancelled" },
174
- { name: "view", takesValue: true, meaning: "all|needs-you|running|completed" },
178
+ { name: "view", takesValue: true, meaning: "all|needs-you|running|completed|archived" },
175
179
  { name: "limit", takesValue: true, meaning: "page size, 1–100; default 40" },
176
180
  { name: "cursor", takesValue: true, meaning: "nextCursor from the previous response" },
177
181
  { name: "repo", takesValue: true, meaning: "filter to one project" }] },
@@ -299,6 +303,18 @@ const GUIDE_ROWS = [
299
303
  operator("task route", "which configured agents plan, build and repair this task; phase and size changes require renewed approval"),
300
304
  operator("task reopen", "resume external work its tracker closed and has been SEEN open again"),
301
305
  operator("task stop", "stop ONE exact live attempt (--run <id>): durable before any process is signalled, answers 'stopping' until its own processes are established gone; work, branch, and evidence preserved; no strike, no retry"),
306
+ { ...operator("task archive", "put tasks away (a person's act): out of every list, count and nudge, history and approvals kept; nothing claims, approves or dispatches them until restored"),
307
+ positionals: [{ name: "id", required: true, meaning: "one or more tasks (any version: the whole family goes)" }],
308
+ flags: [jsonFlag, dbFlag, { name: "reason", takesValue: true, meaning: "why, shown on the task page" }], notableReasons: ["unknown-task", "not-an-approver"] },
309
+ { ...operator("task restore", "bring put-away tasks back exactly as they were (a person's act)"),
310
+ positionals: [{ name: "id", required: true, meaning: "one or more put-away tasks" }], flags: [jsonFlag, dbFlag], notableReasons: ["unknown-task", "not-an-approver"] },
311
+ { ...operator("task tidy", "Put away old tasks: --preview lists what would go and why (read-only), --apply --digest <d> puts away exactly that list (a person's act)"),
312
+ flags: [jsonFlag, dbFlag, { name: "preview", takesValue: false, meaning: "show the list and the digest; changes nothing" },
313
+ { name: "apply", takesValue: false, meaning: "put away exactly the previewed list (needs --digest)" },
314
+ { name: "digest", takesValue: true, meaning: "the digest --preview printed" },
315
+ { name: "days", takesValue: true, meaning: "also finished tasks untouched this many days (1–365, default 14)" },
316
+ { name: "except", takesValue: true, meaning: "comma-separated tasks from the list to leave out" }],
317
+ notableReasons: ["usage", "not-an-approver", "empty"] },
302
318
  operator("run settle", "last resort for a finished run whose process witness can't be proven either way: records your --why in the ledger; refuses while any of the run's processes is alive"),
303
319
  operator("task resume", "resume the exact stopped attempt (--run <id>): refuses until it is quiescent, lifts only that stop's hold, approves nothing — the next pass re-proves the scope and inherits the draft with fresh proof"),
304
320
  // ---- leases (runner surface) ----
@@ -443,11 +459,15 @@ const REMOTE_NO = [
443
459
  "enroll", "revoke",
444
460
  // Flow steps that fetch addresses or run checks and scripts here.
445
461
  "flows import", "flows trigger check", "flows script save",
462
+ // Lets a flow script on this machine start Chromium in its sandbox: a person here, with their own password.
463
+ "flows browser",
446
464
  // Reads this machine owner's own Claude and Codex session files (~/.claude, ~/.codex).
447
465
  "memory propose",
448
466
  ];
449
467
  const REMOTE_STEP_UP = [
450
- "proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle", "knowledge apply", "memory apply",
468
+ "proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle",
469
+ // Putting tasks away and restoring them: a person's act, in the console or on the server's machine.
470
+ "task archive", "task restore", "task tidy", "knowledge apply", "memory apply",
451
471
  "flows card approve", "flows card send-back", "flows script approve", "flows unattended",
452
472
  "approver list", "approver add", "people list", "people invite", "people projects", "people revoke",
453
473
  // A person's own API tokens: a password step-up on the server's machine, or the console. Never with a token.
@@ -457,7 +477,7 @@ const REMOTE_STEP_UP = [
457
477
  ];
458
478
  const REMOTE_YES = [
459
479
  "status", "integrations", "ready", "gaps", "grants", "sync", "audit",
460
- "task add", "task ask", "task checks", "task add-tests", "task list", "task show", "check-progress", "task wait", "task complete", "task revise",
480
+ "task add", "task ask", "task checks", "task ship", "task add-tests", "task list", "task show", "check-progress", "task wait", "task complete", "task revise",
461
481
  "task state", "task block", "task unblock", "task next", "task steer", "task pause", "task continue", "task assign", "task scope", "task plan", "task hold", "task unhold", "task require",
462
482
  "task requeue", "task review", "task repair", "task route", "task reopen", "task stop", "task resume",
463
483
  "runner list", "coordinator list", "cap list", "cap add", "outbox list", "incident list", "incident resolve",
@@ -0,0 +1,10 @@
1
+ /** Put-away checks (v145, docs/plans/task-tidy.md), in a leaf module so every door can ask without loading the store. */
2
+ import type { Store } from "./store.js";
3
+ /** Whether this task version is put away. Every door that dispatches, claims or approves asks this (fail closed: a
4
+ * row means no). */
5
+ export declare function refArchived(store: Store, taskRef: number): boolean;
6
+ /** Whether the built-in task with this id is put away. */
7
+ export declare function taskArchived(store: Store, taskId: string): boolean;
8
+ /** Whether a flow card's work is put away: its current task or its first build task (a later stage keeps the first
9
+ * and may clear the current one). Read from the card's own row now, so a put away that committed first holds it. */
10
+ export declare function cardTaskArchived(store: Store, cardId: number): boolean;
@@ -0,0 +1,16 @@
1
+ /** Whether this task version is put away. Every door that dispatches, claims or approves asks this (fail closed: a
2
+ * row means no). */
3
+ export function refArchived(store, taskRef) {
4
+ return store.handle.prepare("SELECT 1 FROM task_archive WHERE task_ref = ?").get(taskRef) !== undefined;
5
+ }
6
+ /** Whether the built-in task with this id is put away. */
7
+ export function taskArchived(store, taskId) {
8
+ return store.handle.prepare(`SELECT 1 FROM task_archive a JOIN task_ref r ON r.id = a.task_ref
9
+ WHERE r.backend = 'built-in' AND r.external_id = ?`).get(taskId) !== undefined;
10
+ }
11
+ /** Whether a flow card's work is put away: its current task or its first build task (a later stage keeps the first
12
+ * and may clear the current one). Read from the card's own row now, so a put away that committed first holds it. */
13
+ export function cardTaskArchived(store, cardId) {
14
+ return store.handle.prepare(`SELECT 1 FROM flow_card c JOIN task_ref r ON r.backend = 'built-in' AND r.external_id IN (c.task, c.primary_task)
15
+ JOIN task_archive a ON a.task_ref = r.id WHERE c.id = ?`).get(cardId) !== undefined;
16
+ }
@@ -102,7 +102,7 @@ export type ResumeOutcome = {
102
102
  gate: DispatchDiagnosis | null;
103
103
  } | {
104
104
  ok: false;
105
- reason: "no-task" | "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review" | "occupied";
105
+ reason: "no-task" | "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review" | "occupied" | "archived";
106
106
  detail: string;
107
107
  };
108
108
  /**
@@ -1,7 +1,8 @@
1
1
  /** Settings → Teams: connect the Entra app once (operator), then each
2
2
  * teammate pairs their own Teams account. The messaging endpoint the bot
3
3
  * registration must point at is shown here; it has to be reachable from the
4
- * internet, which a tailnet-only address is not. */
4
+ * internet, which a tailnet-only address is not. One page with Slack and
5
+ * Discord (chat-settings-page.ts). */
5
6
  import type { Store } from "./store.js";
6
7
  import { type Html } from "./html.js";
7
8
  export declare function teamsSettingsHtml(store: Store, dir: string, options?: {
@@ -1,46 +1,29 @@
1
- import { html, joinHtml, postForm } from "./html.js";
2
- import { ChatState } from "./chat-delivery-state.js";
1
+ import { html, joinHtml } from "./html.js";
3
2
  import { loadTeamsCredentials } from "./teams-api.js";
4
- import { loadPrimary } from "./webhooks.js";
5
3
  import { TEAMS_MESSAGES_PATH } from "./teams.js";
4
+ import { formRow, input } from "./server/kit.js";
5
+ import { chatSecretRow, chatSettingsHtml } from "./chat-settings-page.js";
6
6
  export function teamsSettingsHtml(store, dir, options = {}) {
7
- const credentials = loadTeamsCredentials(dir), state = new ChatState(store, "teams");
8
- const bindings = credentials ? state.bindings(credentials.installation).filter(one => state.live(one)) : [];
9
- const binding = options.who === undefined ? null : bindings.find(one => one.approver === options.who) ?? null;
10
- const others = bindings.length - (binding === null ? 0 : 1);
11
- const runtime = credentials ? state.runtime(credentials.installation) : null;
12
- const now = options.now ?? new Date();
13
- const live = runtime && runtime.leaseUntil !== null && runtime.leaseUntil > now.toISOString() && runtime.connected;
14
- const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
15
- const post = (action, content) => postForm(`/settings/teams/${action}`, content, { attrs: { class: "card" } });
16
7
  const endpoint = options.publicUrl ? `${options.publicUrl.replace(/\/$/, "")}${TEAMS_MESSAGES_PATH}` : null;
17
- const parts = [];
18
- if (!credentials) {
19
- parts.push(html `<p>Manage projects and review results in a Teams chat, and let a channel follow a team conversation.</p>`, html `<details open><summary>Register the bot</summary><ol><li>In <a href="https://portal.azure.com" target="_blank" rel="noopener noreferrer">Azure</a>, create an <em>Azure Bot</em> with a new single-tenant app registration; note the app (client) id and the directory (tenant) id, and create a client secret.</li>`, html `<li>Set its messaging endpoint to <code>${endpoint === null ? "https://your-public-address" + TEAMS_MESSAGES_PATH : endpoint}</code>. Microsoft must reach it from the internet: a tailnet-only address needs Tailscale Funnel (or another public tunnel) on that path.</li>`, html `<li>Enable the Microsoft Teams channel on the bot, then add the app to Teams (personal scope; team scope for channels).</li></ol></details>`, post("connect", joinHtml([
20
- html `<label>App (client) id<input style="min-height:44px" type="text" name="app-id" autocomplete="off" required></label>`,
21
- html `<label>Directory (tenant) id<input style="min-height:44px" type="text" name="tenant" autocomplete="off" required></label>`,
22
- html `<label>Client secret<input style="min-height:44px" type="password" name="secret" autocomplete="off" required></label>`,
23
- html `${password}<p class="meta">The secret stays on this installation. Next, pair your own Teams account.</p><button type="submit">Connect Teams</button>`
24
- ])));
25
- }
26
- else {
27
- parts.push(html `<p><strong>Microsoft Teams</strong> · ${live ? "Signed in" : "Waiting for sign-in"}</p>`);
28
- if (endpoint !== null)
29
- parts.push(html `<p class="meta">Messaging endpoint: <code>${endpoint}</code> — must be reachable from the internet.</p>`);
30
- if (runtime?.problem)
31
- parts.push(html `<p role="status">${String(runtime.problem)}</p>`);
32
- if (!live && !runtime?.problem)
33
- parts.push(html `<p>Keep the Toolroll worker running to answer Teams messages.</p>`);
34
- if (options.code) {
35
- parts.push(html `<h2>Pair your account</h2><p>Send this to the Toolroll bot in a personal Teams chat. 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/teams">Check connection</a>`);
36
- }
37
- else if (binding && state.live(binding)) {
38
- parts.push(html `<p>Your Teams account is paired.${others ? ` ${others} teammate${others === 1 ? " is" : "s are"} paired too.` : ""}</p>`, html `<p>Try “What needs my attention?” or send <code>status</code>. In a channel, mention the bot with <code>team</code> to follow a conversation.</p>`, html `<p><a href="/chat">Open saved chat</a></p>`, loadPrimary(process.env, dir) === "teams" ? html `<p>Task updates are sent here.</p>` : post("alerts", html `<button type="submit">Send task updates here</button>`), 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>`));
39
- }
40
- else {
41
- parts.push(post("pair", html `<h2>Pair your account</h2><p>Your paired Teams 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>`));
42
- }
43
- parts.push(html `<details><summary>Connection settings</summary>${post("disconnect", html `${password}<button type="submit">Disconnect Teams</button>`)}</details>`);
44
- }
45
- return html `<section style="max-width:42rem;overflow-wrap:anywhere"><p><a href="/settings">Settings</a></p><h1>Teams</h1>${options.problem ? html `<p class="problem" role="alert">${options.problem}</p>` : ""}${parts}</section>`;
8
+ const text = (id, name, label) => formRow(label, null, input({ id, name, attrs: { autocomplete: "off", spellcheck: "false", required: true, "aria-label": label } }));
9
+ const { code, problem, now, who } = options;
10
+ return chatSettingsHtml(store, dir, { ...(code === undefined ? {} : { code }), ...(problem === undefined ? {} : { problem }), ...(now === undefined ? {} : { now }), ...(who === undefined ? {} : { who }) }, {
11
+ provider: "teams",
12
+ label: "Teams",
13
+ credentials: loadTeamsCredentials(dir),
14
+ intro: "Manage projects and review results in a Teams chat, and let a channel follow a team conversation.",
15
+ steps: html `<ol class="k-note"><li>In <a class="k-link" href="https://portal.azure.com" target="_blank" rel="noopener noreferrer">Azure</a>, create an <em>Azure Bot</em> with a new single-tenant app registration; note the app (client) id and the directory (tenant) id, and create a client secret.</li>\
16
+ <li>Set its messaging endpoint to <code class="k-mono">${endpoint === null ? "https://your-public-address" + TEAMS_MESSAGES_PATH : endpoint}</code>. Microsoft must reach it from the internet: a tailnet-only address needs Tailscale Funnel (or another public tunnel) on that path.</li>\
17
+ <li>Enable the Microsoft Teams channel on the bot, then add the app to Teams (personal scope; team scope for channels).</li></ol>`,
18
+ fields: joinHtml([
19
+ text("teams-app-id", "app-id", "App (client) id"),
20
+ text("teams-tenant", "tenant", "Directory (tenant) id"),
21
+ chatSecretRow({ id: "teams-secret", name: "secret", label: "Client secret" }),
22
+ ]),
23
+ fieldsNote: "The secret stays on this installation. Next, pair your own Teams account.",
24
+ pairWhere: "to the Toolroll bot in a personal Teams chat",
25
+ tryLine: html `Try “What needs my attention?” or send <code class="k-mono">status</code>. In a channel, mention the bot with <code class="k-mono">team</code> to follow a conversation.`,
26
+ liveWords: ["Signed in", "Waiting for sign-in"],
27
+ ...(endpoint === null ? {} : { connection: html `<p class="k-note">Messaging endpoint, reachable from the internet: <code class="k-mono">${endpoint}</code></p>` }),
28
+ });
46
29
  }
@@ -1,7 +1,3 @@
1
- /** The person's own Telegram pairing, from the console: one private chat per
2
- * teammate with the installation's bot (v72). The bot token itself stays on
3
- * the settings page; this card only mints or revokes the signed-in person's
4
- * pairing, never anyone else's. */
5
1
  import type { Store } from "./store.js";
6
2
  import { type Html } from "./html.js";
7
3
  /** How long a written reply may wait to be sent before it shows here with a Retry. */
@@ -1,36 +1,63 @@
1
+ /** The person's own Telegram pairing, from the console: one private chat per
2
+ * teammate with the installation's bot (v72). The bot token itself stays on
3
+ * Chat apps and alerts; this page only mints or revokes the signed-in person's
4
+ * pairing, never anyone else's. Laid out as three steps: token, pair, check. */
5
+ import { settingsSubPage } from "./settings-groups-ui.js";
1
6
  import { html, joinHtml, postForm } from "./html.js";
7
+ import { acts, btn, codeLine, note, notice, settingsRecord, settingsSection } from "./server/kit.js";
8
+ import { kitSectionForm } from "./settings-kit.js";
2
9
  /** How long a written reply may wait to be sent before it shows here with a Retry. */
3
10
  export const UNSENT_REPLY_MS = 2 * 60_000;
4
11
  export function telegramSettingsHtml(store, botId, who, options = {}) {
5
- const password = html `<label>Your Toolroll password<input style="min-height:44px" type="password" name="password" autocomplete="current-password" required></label>`;
6
- const post = (action, content) => postForm(`/settings/telegram/${action}`, content, { attrs: { class: "card" } });
7
- let content;
12
+ const problem = options.problem === undefined ? "" : notice(options.problem);
13
+ const group = note(html `Each teammate pairs their own private chat. To connect a group, a conversation manager sends <code class="k-mono">/team</code> there and chooses a conversation. Everyone in the group can read its replies.`);
8
14
  if (botId === null) {
9
- content = html `<p>No Telegram bot is connected yet. An installation approver saves the bot token on the <a href="/settings">settings page</a> first.</p>`;
15
+ return settingsSubPage("alerts", "Telegram", joinHtml([
16
+ problem,
17
+ settingsSection({ id: "telegram-bot", title: "1. Bot token", state: " · Not saved",
18
+ lead: "No Telegram bot is connected yet. An approver saves the bot token first.",
19
+ body: acts(btn("Open Chat apps and alerts", { sm: true, href: "/settings/alerts#telegram-token" })) }),
20
+ settingsSection({ id: "telegram-pair", title: "2. Pair your phone", body: note("Once the bot token is saved, pair your phone here.") }),
21
+ group,
22
+ ]));
23
+ }
24
+ const bindings = store.liveTelegramBindings(botId);
25
+ const mine = bindings.filter(binding => binding.approver === who);
26
+ const others = bindings.length - mine.length;
27
+ const paired = mine.length > 0;
28
+ const teammates = others === 0 ? "No teammates are paired yet." : `${others} teammate${others === 1 ? " is" : "s are"} paired.`;
29
+ const bot = settingsSection({ id: "telegram-bot", title: "1. Bot token", state: " · Saved",
30
+ body: note(html `Saved on <a class="k-link" href="/settings/alerts#telegram-token">Chat apps and alerts</a>.`) });
31
+ let pair;
32
+ let check;
33
+ if (options.code !== undefined) {
34
+ // The code shows once, on this page only: selectable text, never in a link or an address.
35
+ pair = settingsSection({ id: "telegram-pair", title: "2. Pair your phone", state: " · Code ready",
36
+ lead: "Send this to the bot in a private Telegram chat within 10 minutes:",
37
+ body: joinHtml([codeLine(`/pair ${options.code}`), note("The code works once. That chat then answers as you, so treat it like your sign-in.")]) });
38
+ check = settingsSection({ id: "telegram-check", title: "3. Check it works",
39
+ lead: "Once you've sent it, this page says your phone is paired.",
40
+ body: acts(btn("Check connection", { sm: true, href: "/settings/telegram" })) });
10
41
  }
11
42
  else {
12
- const bindings = store.liveTelegramBindings(botId);
13
- const mine = bindings.filter(binding => binding.approver === who);
14
- const others = bindings.length - mine.length;
15
- const parts = [html `<p><strong>${mine.length > 0 ? "Your phone is paired." : "Your phone is not paired."}</strong> ${others === 0 ? "No teammates are paired yet." : `${others} teammate${others === 1 ? " is" : "s are"} paired.`}</p>`];
43
+ pair = settingsSection({ id: "telegram-pair", title: "2. Pair your phone", state: paired ? " · Paired" : " · Not paired",
44
+ lead: `${paired ? "Your phone is paired." : "Your phone is not paired."} ${teammates}`,
45
+ body: paired
46
+ ? kitSectionForm({ proof: "telegram-unpair", action: "/settings/telegram/unpair", id: "telegram-unpair-form", save: { label: "Unpair my phone", variant: "danger" },
47
+ confirm: "Unpair your phone from Telegram", body: note("This phone will lose access. Its existing Telegram buttons will stop working.") })
48
+ : kitSectionForm({ proof: "telegram-pair", action: "/settings/telegram/pair", id: "telegram-pair-form", native: true, save: { label: "Pair my phone" },
49
+ confirm: "Pair your phone with Telegram", body: note("You get a one-time code to send to the bot.") }) });
16
50
  // A reply the assistant wrote that has not reached this person's phone for two minutes.
17
51
  const unsent = store.unsentTelegramReplies(botId, who, new Date((options.now ?? new Date()).getTime() - UNSENT_REPLY_MS));
18
- if (unsent.length > 0) {
19
- const oldest = unsent[0];
20
- parts.push(html `<div class="card" role="status" data-unsent-replies><p><strong>${unsent.length === 1 ? html `A reply hasn't reached your phone` : html `${unsent.length} replies haven't reached your phone`}</strong></p><p class="meta">Trying since ${oldest.since.slice(11, 16)} UTC${oldest.error === null ? "" : `: ${oldest.error}`}</p>${postForm("/settings/telegram/retry", html `<button type="submit">Retry</button>`)}</div>`);
21
- }
22
- if (options.code !== undefined) {
23
- parts.push(html `<h2>Pair your phone</h2><p>Send this to the bot in a private Telegram chat within 10 minutes:</p><pre class="pairing-code">/pair ${options.code}</pre><p class="meta">The code works once. That chat then answers as you, so treat it like your sign-in.</p>`);
24
- }
25
- else if (mine.length === 0) {
26
- parts.push(post("pair", html `${password}<button type="submit">Pair my phone</button>`));
27
- }
28
- else {
29
- parts.push(post("unpair", html `${password}<p class="meta">This phone will lose access. Its existing Telegram buttons will stop working.</p><button type="submit" class="danger">Unpair my phone</button>`));
30
- }
31
- content = joinHtml(parts);
52
+ const oldest = unsent[0];
53
+ check = settingsSection({ id: "telegram-check", title: "3. Check it works",
54
+ lead: paired ? html `In your private chat with the bot, send <code class="k-mono">/status</code>. It answers without an AI model.` : "Pair your phone first.",
55
+ body: oldest === undefined ? "" : settingsRecord({
56
+ title: unsent.length === 1 ? html `A reply hasn't reached your phone` : html `${unsent.length} replies haven't reached your phone`,
57
+ sub: `Trying since ${oldest.since.slice(11, 16)} UTC${oldest.error === null ? "" : `: ${oldest.error}`}`,
58
+ action: postForm("/settings/telegram/retry", btn("Retry", { sm: true, type: "submit" })),
59
+ attrs: { role: "status", "data-unsent-replies": true },
60
+ }) });
32
61
  }
33
- if (options.problem !== undefined)
34
- content = html `<p role="status">${options.problem}</p>${content}`;
35
- return html `<h1>Telegram</h1>${content}<p class="meta">Each teammate pairs their own private chat. To connect a group, a conversation manager sends <code>/team</code> there and chooses a conversation. Everyone in the group can read its replies.</p>`;
62
+ return settingsSubPage("alerts", "Telegram", joinHtml([problem, bot, pair, check, group]));
36
63
  }
package/dist/tidy.d.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { type Store } from "./store.js";
2
+ import type { WorkSummaryAccess } from "./work-summary.js";
3
+ export type TidyRule = "release" | "routine" | "ci" | "finished";
4
+ /** The preview's groups, in order. "finished" is a person's own choice only; tidy never applies it on its own. */
5
+ export declare const TIDY_GROUPS: readonly {
6
+ rule: TidyRule;
7
+ label: string;
8
+ }[];
9
+ export declare const TIDY_DEFAULT_DAYS = 14;
10
+ /** At most this many tasks are put away automatically per pass. */
11
+ export declare const TIDY_PASS_LIMIT = 25;
12
+ /** One task tidy would put away: its family's current task, every version that goes with it (`members`, task_ref ids),
13
+ * why, and under which rule. */
14
+ export type TidyItem = {
15
+ rule: TidyRule;
16
+ taskId: string;
17
+ title: string;
18
+ repo: string | null;
19
+ reason: string;
20
+ updatedAt: string;
21
+ members: number[];
22
+ };
23
+ /** What the release journals say about one release task (release.ts writes them; read only). */
24
+ export type ReleaseFact = {
25
+ taskId: string;
26
+ version: string;
27
+ product: string;
28
+ candidate: string;
29
+ superseded: boolean;
30
+ done: boolean;
31
+ published: boolean;
32
+ stopped: boolean;
33
+ };
34
+ /** Every release journal's facts, or why they can't be trusted: one unreadable journal leaves the release rule out. */
35
+ export type ReleaseFacts = {
36
+ facts: ReleaseFact[];
37
+ problem: string | null;
38
+ };
39
+ /** Reads `releases/*.json` under the plane's state folder (the database's folder), written only by `toolroll release`.
40
+ * A symlink, a non-file, an oversized or unreadable journal makes the whole answer untrusted (`problem`), so the
41
+ * release rule then puts nothing away: a journal it can't read might be the one that says a release is in flight. */
42
+ export declare function readReleaseFacts(stateDir: string): ReleaseFacts;
43
+ /** The release journals beside this database (its folder is the plane's state folder). An in-memory or relative
44
+ * database has no state folder, so the release rule stays off rather than reading whatever folder it runs in. */
45
+ export declare function releaseFactsFor(databaseFile: string): ReleaseFacts;
46
+ /** -1, 0 or 1: numeric parts first, then a release above any of its pre-releases. */
47
+ export declare function compareVersions(a: string, b: string): number;
48
+ /** The failing runs older "Checks failed" cards of this trigger name (their own GitHub run links) that aren't on record
49
+ * yet, newest first, at most `limit`: the checks trigger reads each from GitHub so tidy can tell where it ran. */
50
+ export declare function unprovenNoticeRuns(store: Store, trigger: number, repo: string, limit?: number): number[];
51
+ /** A completed workflow run on the watched branch, as the checks trigger read it from GitHub (its poll through the
52
+ * person's own gh login, or its signed webhook): kept once per run. Only a push to the watched branch of the watched
53
+ * repository counts (a pull request from a fork whose branch is also called main does not). */
54
+ export declare function recordBranchCheckRun(store: Store, watched: {
55
+ repo: string;
56
+ branch: string;
57
+ }, run: {
58
+ id?: number | undefined;
59
+ name?: string | undefined;
60
+ head_sha?: string | undefined;
61
+ head_branch?: string | undefined;
62
+ conclusion?: string | undefined;
63
+ created_at?: string | undefined;
64
+ event?: string | undefined;
65
+ head_repository?: {
66
+ full_name?: string | undefined;
67
+ } | undefined;
68
+ }, now: Date): boolean;
69
+ /** What tidy would put away now, in group order, oldest first within a group. `days`: also offer finished tasks
70
+ * untouched that long (a person's preview only); null is tidy's own automatic rules. `access`: whose lists. */
71
+ export declare function tidyPreview(store: Store, now: Date, input: {
72
+ releases: ReleaseFacts;
73
+ days: number | null;
74
+ access?: WorkSummaryAccess;
75
+ }): TidyItem[];
76
+ /** The preview's identity: exactly which tasks under which rules, for how many days. A person's confirmation names it. */
77
+ export declare function tidyDigest(items: readonly TidyItem[], days: number | null): string;
78
+ /** The preview in its groups (empty ones left out), with each group's label. */
79
+ export declare function tidyGroups(items: readonly TidyItem[], days: number): {
80
+ rule: TidyRule;
81
+ label: string;
82
+ items: TidyItem[];
83
+ }[];
84
+ /** A day count from a person (1–365), or the default. */
85
+ export declare function tidyDays(raw: string | null | undefined): number | null;
86
+ export type TidyApply = {
87
+ ok: true;
88
+ archived: TidyItem[];
89
+ skipped: {
90
+ taskId: string;
91
+ message: string;
92
+ }[];
93
+ } | {
94
+ ok: false;
95
+ reason: "changed" | "outside" | "empty";
96
+ message: string;
97
+ items: TidyItem[];
98
+ digest: string;
99
+ };
100
+ /** A person puts away exactly what they previewed (or the ticked part of it), in one write transaction that recomputes
101
+ * the preview and refuses when its digest moved or a ticked task isn't in it. */
102
+ export declare function applyTidy(store: Store, now: Date, input: {
103
+ by: string;
104
+ days: number;
105
+ digest: string;
106
+ only: readonly string[] | null;
107
+ releases: ReleaseFacts;
108
+ access?: WorkSummaryAccess;
109
+ }): TidyApply;
110
+ /** Tidy on its own (the worker's reconcile pass): its automatic rules only, at most `limit` tasks, in one write
111
+ * transaction. Idempotent: what is put away is skipped next time, and a restored task is never put away again. */
112
+ export declare function runTidy(store: Store, now: Date, releases: ReleaseFacts, limit?: number, access?: WorkSummaryAccess): TidyItem[];
113
+ /** Whether this person may still put away or restore work in this project, read under the write lock (check and write
114
+ * together): an active approver who can see it. Null when they may; otherwise why not. */
115
+ export declare function approverStandsNow(store: Store, name: string, repo: string | null): string | null;
116
+ /** The repository of a task's family, from its first version (all versions share it). */
117
+ export declare function familyRepoOf(store: Store, refs: readonly number[]): string | null;
118
+ /** "Archived by tidy: <reason>" or "Archived by <person>: <reason>": the one sentence every surface shows. */
119
+ export declare function archivedWords(archived: {
120
+ by: string;
121
+ byKind: "person" | "tidy";
122
+ reason: string;
123
+ }): string;
124
+ /** The release journals beside the store's own database file (none for an in-memory one). */
125
+ export declare function releaseFactsOf(store: Store): ReleaseFacts;
126
+ /** The lists one person sees, for tidy: every project for an unrestricted account, otherwise exactly theirs. The CLI's
127
+ * preview (`--as <name>`) and its apply read the same scope, so the same records give the same digest. */
128
+ export declare function accessOf(store: Store, name: string): WorkSummaryAccess;
129
+ /** Narrows `access` to the caller's own lens (a remote person's project limit): never wider. No lens: unchanged. */
130
+ export declare function withinLens(access: WorkSummaryAccess, lens: readonly string[] | null): WorkSummaryAccess;