toolroll 0.9.58 → 0.9.60

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 (86) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +8 -6
  3. package/dist/action-ledger.js +10 -1
  4. package/dist/assignment-adapters.d.ts +1 -1
  5. package/dist/assignment-adapters.js +1 -1
  6. package/dist/assignment-delivery.d.ts +16 -0
  7. package/dist/assignment-delivery.js +54 -28
  8. package/dist/assignment-status.d.ts +13 -4
  9. package/dist/assignment-status.js +70 -39
  10. package/dist/assignment.d.ts +6 -2
  11. package/dist/assignment.js +13 -8
  12. package/dist/bin.js +11 -2
  13. package/dist/brief-fence.d.ts +9 -0
  14. package/dist/brief-fence.js +22 -0
  15. package/dist/browser/workspace.css +2 -2
  16. package/dist/browser/workspace.js +19 -19
  17. package/dist/builder.js +78 -27
  18. package/dist/chat-channel.d.ts +1 -0
  19. package/dist/chat-channel.js +4 -1
  20. package/dist/chat-delivery.js +2 -0
  21. package/dist/cli-fast.d.ts +8 -0
  22. package/dist/cli-fast.js +18 -0
  23. package/dist/cli-help.d.ts +5 -0
  24. package/dist/cli-help.js +91 -0
  25. package/dist/cli.d.ts +6 -5
  26. package/dist/cli.js +17 -96
  27. package/dist/contract-changes.d.ts +52 -0
  28. package/dist/contract-changes.js +61 -0
  29. package/dist/decision.d.ts +4 -0
  30. package/dist/decision.js +8 -0
  31. package/dist/deploy-lock.d.ts +8 -0
  32. package/dist/deploy-lock.js +45 -0
  33. package/dist/design/tokens.css +234 -0
  34. package/dist/discord.js +2 -1
  35. package/dist/dispatch.js +1 -1
  36. package/dist/evidence.d.ts +22 -0
  37. package/dist/evidence.js +76 -17
  38. package/dist/flow-actions.js +2 -1
  39. package/dist/guides.js +14 -11
  40. package/dist/lead-cli.js +4 -1
  41. package/dist/lead-contract.js +1 -1
  42. package/dist/lead-doors.js +8 -2
  43. package/dist/lead-follow.d.ts +2 -0
  44. package/dist/lead-follow.js +138 -88
  45. package/dist/lead-tools.js +11 -3
  46. package/dist/mailbox.js +3 -2
  47. package/dist/mcp-core.js +1 -1
  48. package/dist/needs-you.js +3 -1
  49. package/dist/operate.js +38 -17
  50. package/dist/password-bearer.d.ts +4 -4
  51. package/dist/password-bearer.js +2 -2
  52. package/dist/planner-source.d.ts +23 -53
  53. package/dist/planner-source.js +43 -68
  54. package/dist/provider.d.ts +8 -0
  55. package/dist/provider.js +12 -1
  56. package/dist/release-gate.d.ts +2 -3
  57. package/dist/release-gate.js +44 -16
  58. package/dist/scope.js +14 -0
  59. package/dist/serve.js +3 -2
  60. package/dist/server/chrome.d.ts +6 -3
  61. package/dist/server/chrome.js +8 -4
  62. package/dist/server/console.css +9 -64
  63. package/dist/server/render-chat.js +12 -2
  64. package/dist/server/render-tasks.d.ts +2 -0
  65. package/dist/server/render-tasks.js +30 -22
  66. package/dist/server/task-views.js +1 -0
  67. package/dist/server/tasks.js +3 -1
  68. package/dist/slack-socket.d.ts +12 -0
  69. package/dist/slack-socket.js +29 -0
  70. package/dist/slack.d.ts +0 -7
  71. package/dist/slack.js +2 -24
  72. package/dist/steer-mailbox.d.ts +39 -0
  73. package/dist/steer-mailbox.js +207 -0
  74. package/dist/steer-status.d.ts +19 -0
  75. package/dist/steer-status.js +30 -0
  76. package/dist/store.d.ts +85 -8
  77. package/dist/store.js +203 -15
  78. package/dist/surface.js +2 -2
  79. package/dist/sweep-gate.d.ts +10 -0
  80. package/dist/sweep-gate.js +18 -0
  81. package/dist/task-status.js +3 -1
  82. package/dist/team-updates.js +3 -3
  83. package/dist/telegram-lead.js +6 -0
  84. package/dist/telegram.js +9 -7
  85. package/dist/toolroll-update.js +20 -1
  86. package/package.json +21 -21
package/dist/serve.js CHANGED
@@ -7,7 +7,7 @@ import { CodingWorkspace } from './coding-workspace.js';
7
7
  import { CONSOLE_FORMS, readForm } from "./contracts/console-api.js";
8
8
  import { createFlowRooms, flowFingerprint } from "./flow-live.js";
9
9
  import {} from "./google-mail.js";
10
- import { configureLeadFollow, runLeadFollowPass } from './lead-follow.js';
10
+ import { configureLeadFollow, recordLeadFollowFailure, runLeadFollowPass } from './lead-follow.js';
11
11
  import { leadNameOf } from "./lead-identity.js";
12
12
  import { createLiveBus, followWorkspace } from "./live-bus.js";
13
13
  import { startMaintenance } from './maintenance.js';
@@ -1570,7 +1570,8 @@ export function createDecisionServer(options) {
1570
1570
  run: () => runLeadFollowPass({ store, repos: managedRepos, evidenceRoot, clock,
1571
1571
  provider: () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; },
1572
1572
  fetcher: chatFetcher, ...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}) }),
1573
- onError: () => { },
1573
+ // Pending delivery is durable and is read again on the next pass; a failure is said where follow status is read.
1574
+ onError: error => recordLeadFollowFailure(store, error, clock()),
1574
1575
  });
1575
1576
  });
1576
1577
  // Keep native process custody until shutdown has verified agent/tool exit.
@@ -42,9 +42,12 @@ export declare const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a
42
42
  * inset into it. Semantic names only; every surface (server pages, the
43
43
  * React workspace, the pre-script fallback) reads these.
44
44
  *
45
- * The console's stylesheet lives in console.css, shipped beside this module:
46
- * the palette above in light, dark and pinned themes, then the page rules.
47
- * Its bytes are the served sheet's (WORKSPACE_STYLE hashes them). */
45
+ * The palette above, in light, dark and pinned themes, is Signal's generated
46
+ * tokens.css (src/design, built by youi from the design-language repo); the
47
+ * console's own rules follow in console.css, shipped beside this module.
48
+ * Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
49
+ * workspace's stylesheet loads after this one and never redefines a token, so
50
+ * a person's accent (accentHead) still wins over the generated default. */
48
51
  export declare const STYLE: string;
49
52
  /** Appearance: a three-way segmented switch, one tap per choice. */
50
53
  export declare const THEME_CONTROLS_CSS = ".task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed=\"true\"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}";
@@ -127,10 +127,14 @@ export const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model o
127
127
  * inset into it. Semantic names only; every surface (server pages, the
128
128
  * React workspace, the pre-script fallback) reads these.
129
129
  *
130
- * The console's stylesheet lives in console.css, shipped beside this module:
131
- * the palette above in light, dark and pinned themes, then the page rules.
132
- * Its bytes are the served sheet's (WORKSPACE_STYLE hashes them). */
133
- export const STYLE = readFileSync(join(import.meta.dirname, "console.css"), "utf8");
130
+ * The palette above, in light, dark and pinned themes, is Signal's generated
131
+ * tokens.css (src/design, built by youi from the design-language repo); the
132
+ * console's own rules follow in console.css, shipped beside this module.
133
+ * Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
134
+ * workspace's stylesheet loads after this one and never redefines a token, so
135
+ * a person's accent (accentHead) still wins over the generated default. */
136
+ export const STYLE = readFileSync(join(import.meta.dirname, "..", "design", "tokens.css"), "utf8") +
137
+ readFileSync(join(import.meta.dirname, "console.css"), "utf8");
134
138
  /** Appearance: a three-way segmented switch, one tap per choice. */
135
139
  export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
136
140
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
@@ -30,83 +30,28 @@
30
30
  font-family: "Geist Mono"; font-style: normal; font-weight: 600;
31
31
  font-display: swap; src: url("/fonts/geist-mono-600.woff2") format("woff2");
32
32
  }
33
- /* One palette for every page (2026-09-27): neutral grey and paper with
34
- * one chart magenta, light by day and dark after hours. The device decides unless
35
- * the person pins a theme (html[data-theme], set from their cookie). The
36
- * console's older token names map onto it, so the React workspace, the
37
- * server-rendered pages and the pre-script fallback all paint the same. */
33
+ /* Signal's colour, shadow, radius, type and spacing tokens (--so-*) are
34
+ * generated by youi into src/design/tokens.css, light and dark, and the
35
+ * server prepends that sheet to this one (chrome.ts STYLE). Edit them in the
36
+ * design-language repo, never here. The device decides the scheme unless the
37
+ * person pins one (html[data-theme], set from their cookie). What follows are
38
+ * the console's older names: views onto those tokens, never a second ramp. */
38
39
  :root {
39
- color-scheme: light;
40
-
41
- --so-ground: #efefef; --so-paper: #ffffff; --so-sidebar: #efefef; --so-raised: #f5f5f5;
42
- --so-ink: #171717; --so-muted: #666666; --so-line: #e6e6e6; --so-input-line: #d4d4d4;
43
- --so-accent: #171717; --so-accent-hover: #383838; --so-accent-text: #171717; --so-on-accent: #ffffff; --so-soft: #f2f2f2;
44
- --so-nav-ink: #525252; --so-nav-hover: #e4e4e4; --so-nav-current: #ffffff; --so-nav-current-ink: #171717;
45
- --so-signal: #171717; --so-signal-hover: #383838; --so-on-signal: #ffffff; --so-signal-soft: #e8e8e8;
46
- --so-danger: #c4320a; --so-danger-soft: #feebe7; --so-success: #218358; --so-success-soft: #e6f6eb;
47
- --so-warning: #ab6400; --so-warning-soft: #fff4d5; --so-info: #0d74ce; --so-info-soft: #e6f4fe;
48
- --so-neutral-ink: #525252; --so-neutral-soft: #f0f0f0; --so-live: #0d74ce;
49
- --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);
50
- --so-sheet-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .04), 0 4px 12px -6px rgb(0 0 0 / .06);
51
- --so-pill-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .06);
52
- --so-overlay: rgb(0 0 0 / .32); --so-shadow-overlay: 0 0 0 1px rgb(0 0 0 / .08), 0 24px 48px -12px rgb(0 0 0 / .22);
53
- --so-selection: #d4d4d4; --so-scroll: #cfcfcf; --so-code-bg: #fafafa; --so-user-bubble: #f2f2f2;
54
-
55
40
  --background: var(--so-ground); --foreground: var(--so-ink); --card: var(--so-paper);
56
41
  --muted: var(--so-raised); --muted-foreground: var(--so-muted); --border: var(--so-line); --input: var(--so-input-line);
57
42
  --primary: var(--so-accent); --primary-foreground: var(--so-on-accent); --secondary: var(--so-raised); --secondary-foreground: var(--so-ink);
58
43
  --accent: var(--so-soft); --destructive: var(--so-danger); --destructive-strong: var(--so-danger); --destructive-soft: var(--so-danger-soft);
59
44
  --success: var(--so-success); --success-soft: var(--so-success-soft); --warning: var(--so-warning); --warning-soft: var(--so-warning-soft);
60
45
  --running: var(--so-info); --running-soft: var(--so-info-soft); --ring: var(--so-signal);
61
- /* Magenta means one thing: a person is needed (approve, answer, decide). */
46
+ /* The accent means one thing: a person is needed (approve, answer, decide). */
62
47
  --brand: var(--so-attention); --brand-foreground: var(--so-on-attention); --brand-soft: var(--so-attention-soft);
63
48
  --glass: var(--so-paper); --glass-strong: var(--so-paper); --glass-border: var(--so-line); --glass-highlight: transparent;
64
49
  --ambient-one: transparent; --ambient-two: transparent; --user-message: var(--so-user-bubble);
65
50
  --surface: var(--so-paper); --ok: var(--so-success); --danger: var(--so-danger); --fg-muted: var(--so-muted);
66
51
  --shadow: none; --shadow-overlay: var(--so-shadow-overlay);
67
52
  --radius: 0.625rem;
68
- --so-ease-out: cubic-bezier(.23, 1, .32, 1);
69
- --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
70
- --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
71
- /* Phone spacing steps (760px and narrower): side gutter, gap under the
72
- header, gap between page blocks, between rows, and inside a card. */
73
- --so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
74
- }
75
- @media (prefers-color-scheme: dark) {
76
- :root:not([data-theme="light"]) {
77
- color-scheme: dark;
78
-
79
- --so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;
80
- --so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;
81
- --so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;
82
- --so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;
83
- --so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;
84
- --so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);
85
- --so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);
86
- --so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;
87
- --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);
88
- --so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);
89
- --so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);
90
- --so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);
91
- --so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;
92
- }
93
- }
94
- :root[data-theme="dark"] {
95
- color-scheme: dark;
96
-
97
- --so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;
98
- --so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;
99
- --so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;
100
- --so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;
101
- --so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;
102
- --so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);
103
- --so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);
104
- --so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;
105
- --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);
106
- --so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);
107
- --so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);
108
- --so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);
109
- --so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;
53
+ --font-sans: var(--so-font-sans);
54
+ --font-mono: var(--so-font-mono);
110
55
  }
111
56
  * { box-sizing: border-box; }
112
57
  /* Touch: no grey flash on a tap, no double-tap zoom wait on a control,
@@ -2,6 +2,7 @@
2
2
  import { assignmentSummaryHtml } from "../assignment-ui.js";
3
3
  import {} from "../assignment.js";
4
4
  import {} from "../browser-workspace.js";
5
+ import { closedQuestionReason } from "../decision.js";
5
6
  import { CHAT_ACTIONS, sharedActionNeedsReview, sharedActionPayload, sharedActionReviewPath } from "../chat-actions.js";
6
7
  import { CHAT_CONTINUITY_SCRIPT } from "../chat-continuity.js";
7
8
  import { CHAT_CONTROLS, chatControlHref, isChatControl } from "../chat-controls.js";
@@ -478,13 +479,22 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
478
479
  const text = (key) => (typeof payload[key] === "string" ? payload[key] : "");
479
480
  const task = text("task");
480
481
  const repoId = text("repoId");
482
+ if (view.kind === "answer" && decision?.supersededReason != null) {
483
+ // The proposal remains in history, but its question no longer needs an answer.
484
+ // Both the server page and browser card use the same closed view, without stale consent controls.
485
+ const body = html `<p><a href="/d/${decision.id}">${decision.question}</a></p><p class="meta">${closedQuestionReason(decision.supersededReason)}</p>`;
486
+ return {
487
+ html: html `<article class="card proposal proposal-answer" data-card-kind="answer"><header class="proposal-head"><strong>Question closed</strong></header><div class="proposal-body">${body}</div></article>`,
488
+ card: { id: view.id, kind: view.kind, label: "Question closed", state: view.state, body: htmlString(body), said: null, links: [], primary: null, dismissable: false, note: null },
489
+ };
490
+ }
481
491
  const presentations = {
482
492
  task: { label: payload["report"] === true ? "Scout investigation" : "New task", action: "file task", icon: html `<path d="M12 5v14"/><path d="M5 12h14"/>` },
483
493
  next: { label: "Queue priority", action: "move to front", icon: html `<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>` },
484
494
  reserve: { label: "Worker assignment", action: payload["worker"] === null ? "release" : "reserve", icon: html `<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>` },
485
495
  hold: { label: "Pause work", action: "hold", icon: html `<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>` },
486
496
  unhold: { label: "Resume work", action: "release hold", icon: html `<path d="m7 4 13 8-13 8z"/>` },
487
- steer: { label: "Guidance for next attempt", action: "add guidance", icon: html `<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>` },
497
+ steer: { label: "Guidance for the agent", action: "add guidance", icon: html `<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>` },
488
498
  scope: { label: "Scope revision", action: "save scope", icon: html `<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4z"/>` },
489
499
  answer: { label: "Decision answer", action: "confirm answer", icon: html `<path d="M9.1 9a3 3 0 1 1 5.8 1c0 2-3 2-3 4"/><path d="M12 18h.01"/><circle cx="12" cy="12" r="9"/>` },
490
500
  cancel: { label: "Cancel task", action: "open task", icon: html `<path d="m15 9-6 6"/><path d="m9 9 6 6"/><circle cx="12" cy="12" r="9"/>` },
@@ -562,7 +572,7 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
562
572
  }
563
573
  else if (view.kind === "steer") {
564
574
  const taskTitle = text("taskTitle") || task;
565
- what = html `<h3>Guide <a href="${taskHref(task)}">${taskTitle}</a>'s next attempt</h3><p class="proposal-summary">${text("note")}</p>${facts(["when", "next attempt"], ["project", mono(repoId)])}<p class="meta proposal-disclosure">This guides the next attempt without changing the task’s scope. It does not interrupt work already running.</p>`;
575
+ what = html `<h3>Guidance for <a href="${taskHref(task)}">${taskTitle}</a></h3><p class="proposal-summary">${text("note")}</p>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">A running agent reads it when it next checks for notes; otherwise the next attempt does. The task’s scope doesn’t change.</p>`;
566
576
  }
567
577
  else if (view.kind === "agents") {
568
578
  // The agents card (v48): exactly what changes, in the same words the
@@ -570,6 +570,8 @@ export declare function taskBodyParts(data: {
570
570
  } | null;
571
571
  /** Operator steering notes (arc 1), delivery state included. */
572
572
  steering?: SteerNote[];
573
+ /** A build's agent is running now and reads new notes when it next checks for them (v120). */
574
+ steeringLive?: boolean;
573
575
  /** The publication grant the publisher would act under — from
574
576
  * publicationGrantFor(repo) only; null when none, or no project. */
575
577
  grant?: PublicationGrant | null;
@@ -1,5 +1,6 @@
1
1
  /** Task pages: approval, agents, plans, controls, decisions and the task body. */
2
2
  import { html, htmlString, joinHtml, postForm, replaceMarkup } from "../html.js";
3
+ import { closedQuestionReason, closedQuestionWords } from "../decision.js";
3
4
  import {} from "../activity-line.js";
4
5
  import {} from "../agentconfig.js";
5
6
  import { gateWords } from "../approval-policy.js";
@@ -35,6 +36,7 @@ import { assignmentStageOf, demoChecksOf, stageOfCode, stageOfDispatch, STATUS_M
35
36
  import { buildProgressOf, dispatchActionLabel, earlierAttemptsWords, receiptPublicationWords, resultStatusOf, REVIEW_TOKENS, workStatusOf } from "../workspace-ui.js";
36
37
  import { createHash } from "node:crypto";
37
38
  import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SENSITIVE_INPUT, sentenceCase, shortDigest, transcriptScript, whenTime } from "./chrome.js";
39
+ import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
38
40
  import { reviewHref, taskChatHref, taskHref } from "./http.js";
39
41
  import { coordinatorProposalsSection } from "./render-chat.js";
40
42
  import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, terminalDiffCard, verificationExplanation, verificationRecovered } from "./render-results.js";
@@ -1369,24 +1371,21 @@ export function taskBodyParts(data) {
1369
1371
  }
1370
1372
  return box("problem", "Dispatch unknown", "Refresh this task before relying on its scheduler state.", "unknown");
1371
1373
  })();
1372
- // Operator steering (arc 1): notes for the agent, each wearing exactly
1373
- // where it stands — waiting, attached, proven delivered, or superseded.
1374
+ // Operator steering (arc 1, v120): notes for the agent, each wearing the
1375
+ // same words the CLI and the lead use — read (and when), waiting for the
1376
+ // running agent, bound for the next attempt, or ended unread.
1374
1377
  const steering = data.steering ?? [];
1375
- const steerState = (one) => one.authorshipState !== "verified"
1376
- ? "recorded before steering required a credential — never delivered"
1377
- : one.supersededAt !== null
1378
- ? "the task ended before this landed"
1379
- : one.deliveredAt !== null
1380
- ? `reached build #${one.attachedRun}`
1381
- : one.attachedRun !== null
1382
- ? `attached to build #${one.attachedRun} — delivery not yet proven`
1383
- : "waiting for the next attempt";
1378
+ const steeringLive = data.steeringLive === true;
1379
+ const steerState = (one) => {
1380
+ const { words, readAt } = steerStandingParts(one, steeringLive);
1381
+ return readAt === null ? html `${words}` : html `${words} ${whenTime(readAt)}`;
1382
+ };
1384
1383
  const steerRows = steering.length === 0 ? "" : joinHtml(steering.map(one => html `<p class="row"><span class="meta">${one.author} · ${whenTime(one.createdAt)} · ${steerState(one)}</span> ${one.note}</p>`), "\n");
1385
1384
  const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled"
1386
1385
  ? ""
1387
1386
  : joinHtml([
1388
- postForm(`${taskHref(task.id)}/steer`, html `<input type="text" name="note" placeholder="guidance for the next attempt" aria-label="steering note" style="width:100%;max-width:28rem"><button type="submit">Steer</button>`, { attrs: { class: "row" } }),
1389
- html `<p class="meta">Lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`,
1387
+ postForm(`${taskHref(task.id)}/steer`, html `<input type="text" name="note" placeholder="guidance for the agent" aria-label="steering note" style="width:100%;max-width:28rem"><button type="submit">Steer</button>`, { attrs: { class: "row" } }),
1388
+ html `<p class="meta">${steerDestinationWords(steeringLive)} A note can't widen the approved scope.</p>`,
1390
1389
  ]);
1391
1390
  const steeringCard = steerRows === "" && steerForm === "" ? "" : html `<h2>Steering</h2>${steerRows}${steerForm}`;
1392
1391
  const holds = data.holds.length === 0
@@ -1469,9 +1468,12 @@ export function taskBodyParts(data) {
1469
1468
  !(data.revision != null && "problem" in data.revision) && scope.profileState !== "unresolved" && consentDoorOf(scope, data.route).open;
1470
1469
  const planCard = data.planDocument === null
1471
1470
  ? data.plan === "requested"
1472
- ? data.revision != null && !("problem" in data.revision)
1473
- ? html `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong><span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
1474
- : html `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong><span class="meta">${data.planAuto ? "Automatic approval is enabled for a verified plan that preserves your filed contract. Amendments and unanswered questions still pause." : "The agent is inspecting the repository and drafting the goal, acceptance criteria, and approach. It will ask only if a missing answer changes the work."}</span></p></div>`
1471
+ // A request the planner cannot be given waits on a person; the status card above already says what to shorten.
1472
+ ? data.dispatch?.code === "planner-source"
1473
+ ? ""
1474
+ : data.revision != null && !("problem" in data.revision)
1475
+ ? html `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong><span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
1476
+ : html `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong><span class="meta">${data.planAuto ? "Automatic approval is enabled for a verified plan that preserves your filed contract. Amendments and unanswered questions still pause." : "The agent is inspecting the repository and drafting the goal, acceptance criteria, and approach. It will ask only if a missing answer changes the work."}</span></p></div>`
1475
1477
  : ""
1476
1478
  : joinHtml([
1477
1479
  approval.approved ? html `<details class="card planner-plan planner-plan-collapsed"><summary class="execution-plan-head">` : html `<section class="card planner-plan"><div class="execution-plan-head">`,
@@ -1788,7 +1790,7 @@ export function taskBodyParts(data) {
1788
1790
  })();
1789
1791
  const decisions = data.decisions.length === 0
1790
1792
  ? ""
1791
- : html `<h2>Decisions</h2>${joinHtml(data.decisions.map(decision => html `<p class="row"><a href="/d/${decision.id}">${decision.question}</a> <span class="meta">${decision.state}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`), "\n")}`;
1793
+ : html `<h2>Decisions</h2>${joinHtml(data.decisions.map(decision => html `<p class="row"><a href="/d/${decision.id}">${decision.question}</a> <span class="meta">${decision.supersededReason !== null ? closedQuestionWords(decision.supersededReason) : decision.state}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`), "\n")}`;
1792
1794
  const incidents = data.incidents.length === 0
1793
1795
  ? ""
1794
1796
  : html `<h2>Incidents</h2>${joinHtml(data.incidents.map(one => one.resolvedAt === null
@@ -2068,7 +2070,11 @@ export function taskBodyParts(data) {
2068
2070
  continue;
2069
2071
  thread.push({ key: `question-${decision.id}`, at: decision.createdAt, kind: "question", who: "agent", author: "", title: "Asked",
2070
2072
  text: decision.question, link: { label: "Question", href: `/d/${decision.id}` }, html: "", more: null });
2071
- if (decision.answeredAt !== null) {
2073
+ if (decision.supersededReason !== null) {
2074
+ thread.push({ key: `answer-${decision.id}`, at: decision.answeredAt ?? decision.createdAt, kind: "progress", who: "agent", author: "", title: "Closed without an answer",
2075
+ text: closedQuestionReason(decision.supersededReason), link: null, html: "", more: null });
2076
+ }
2077
+ else if (decision.answeredAt !== null) {
2072
2078
  const chosen = decision.options.find(one => one.id === decision.choice)?.label ?? decision.choice;
2073
2079
  thread.push({ key: `answer-${decision.id}`, at: decision.answeredAt, kind: "reply", who: "person", author: decision.answeredBy ?? "You", title: "Answered",
2074
2080
  text: [chosen, decision.note].filter((one) => one !== null && one !== "").join(" — ") || null, link: null, html: "", more: null });
@@ -2320,9 +2326,11 @@ export function decisionOptionForms(decision, returnTo) {
2320
2326
  }
2321
2327
  export function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo = null) {
2322
2328
  const options = decisionOptionForms(decision, returnTo);
2323
- const answered = decision.state === "answered"
2324
- ? html `<div class="answered">Answered: <strong>${decision.choice ?? ""}</strong> by ${decision.answeredBy ?? ""}${decision.note === null ? "" : ` — ${decision.note}`}</div>`
2325
- : "";
2329
+ const answered = decision.supersededReason !== null
2330
+ ? html `<p class="meta">${closedQuestionReason(decision.supersededReason)}</p>`
2331
+ : decision.state === "answered"
2332
+ ? html `<div class="answered">Answered: <strong>${decision.choice ?? ""}</strong> by ${decision.answeredBy ?? ""}${decision.note === null ? "" : ` — ${decision.note}`}</div>`
2333
+ : "";
2326
2334
  const evidence = artifacts.length === 0
2327
2335
  ? ""
2328
2336
  : joinHtml([
@@ -2331,7 +2339,7 @@ export function decisionPage(chrome, decision, taskId, artifacts, who, now, retu
2331
2339
  html `</div>`,
2332
2340
  ]);
2333
2341
  return screen(`decide · ${taskId}`, joinHtml([
2334
- html `<h1>${taskId} <span class="badge badge-${decision.state}">${decision.state}</span>${isOverdue(decision, now) ? html ` <span class="badge badge-overdue">Overdue</span>` : ""}${decision.deadline === null ? "" : html ` <span class="meta">deadline ${decision.deadline}</span>`}</h1>`,
2342
+ html `<h1>${taskId} <span class="badge badge-${decision.state}">${decision.supersededReason !== null ? "closed" : decision.state}</span>${isOverdue(decision, now) ? html ` <span class="badge badge-overdue">Overdue</span>` : ""}${decision.deadline === null ? "" : html ` <span class="meta">deadline ${decision.deadline}</span>`}</h1>`,
2335
2343
  html `<div class="recap">${decision.recap}</div>`,
2336
2344
  html `<div class="question">${decision.question}</div>`,
2337
2345
  decision.state === "answered" ? answered : options,
@@ -473,6 +473,7 @@ export function createTaskViews(runtime) {
473
473
  return { rows, decisions: decisionsFor(runtime.store, rows), now };
474
474
  })(),
475
475
  steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
476
+ steeringLive: ref !== null && runtime.store.steeringLiveBuild(ref.id, now) !== null,
476
477
  // "publishes as" reads ONLY publicationGrantFor(repo) — the grant
477
478
  // the publisher would act under — never listGrants(), which is
478
479
  // dispatch authority (slice 1c).
@@ -1,4 +1,5 @@
1
1
  import { handlersOf } from './handler-registry.js';
2
+ import { closedQuestionWords } from "../decision.js";
2
3
  import { DEPRECATED_PAGE } from "../deprecations.js";
3
4
  /** tasks handlers, moved without changing their route bodies. */
4
5
  import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
@@ -1118,7 +1119,8 @@ export function createTasksHandlers(runtime) {
1118
1119
  }, now);
1119
1120
  if (!answered.ok) {
1120
1121
  const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
1121
- const why = answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
1122
+ const closed = answered.reason === "closed" ? store.getDecision(id)?.supersededReason ?? null : null;
1123
+ const why = closed !== null ? closedQuestionWords(closed) : answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
1122
1124
  return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
1123
1125
  }
1124
1126
  return redirect(response, decisionBack);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The Socket Mode client, apart from the follower so the Slack SDK loads only
3
+ * once Slack is set up (slack.ts imports this when it finds credentials).
4
+ */
5
+ import { SocketModeClient } from "@slack/socket-mode";
6
+ export { LogLevel } from "@slack/socket-mode";
7
+ /** The bot and socket tokens must belong to the same app, including after reconnect. */
8
+ export declare class ToolrollSlackSocket extends SocketModeClient {
9
+ expectedApp: string;
10
+ wrongApp: () => void;
11
+ protected onWebSocketMessage(data: string | ArrayBuffer, isBinary: boolean): Promise<void>;
12
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The Socket Mode client, apart from the follower so the Slack SDK loads only
3
+ * once Slack is set up (slack.ts imports this when it finds credentials).
4
+ */
5
+ import { SocketModeClient } from "@slack/socket-mode";
6
+ export { LogLevel } from "@slack/socket-mode";
7
+ /** The bot and socket tokens must belong to the same app, including after reconnect. */
8
+ export class ToolrollSlackSocket extends SocketModeClient {
9
+ expectedApp = "";
10
+ wrongApp = () => { };
11
+ async onWebSocketMessage(data, isBinary) {
12
+ let packet;
13
+ try {
14
+ packet = JSON.parse(typeof data === "string" ? data : new TextDecoder().decode(data));
15
+ }
16
+ catch {
17
+ return;
18
+ }
19
+ if (packet.type === "hello") {
20
+ const info = packet.connection_info;
21
+ if (info?.app_id !== this.expectedApp) {
22
+ this.wrongApp();
23
+ await this.disconnect();
24
+ return;
25
+ }
26
+ }
27
+ return super.onWebSocketMessage(data, isBinary);
28
+ }
29
+ }
package/dist/slack.d.ts CHANGED
@@ -1,11 +1,4 @@
1
- import { SocketModeClient } from "@slack/socket-mode";
2
1
  import { type SlackChatOptions } from "./slack-chat.js";
3
- /** The bot and socket tokens must belong to the same app, including after reconnect. */
4
- export declare class ToolrollSlackSocket extends SocketModeClient {
5
- expectedApp: string;
6
- wrongApp: () => void;
7
- protected onWebSocketMessage(data: string | ArrayBuffer, isBinary: boolean): Promise<void>;
8
- }
9
2
  export declare function followSlack(options: Omit<SlackChatOptions, "identity" | "api" | "owner" | "current"> & {
10
3
  dir: string;
11
4
  signal: AbortSignal;
package/dist/slack.js CHANGED
@@ -1,33 +1,9 @@
1
1
  /** One leased Socket Mode connection, hosted by the ordinary worker. */
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { setTimeout as sleep } from "node:timers/promises";
4
- import { SocketModeClient, LogLevel } from "@slack/socket-mode";
5
4
  import { loadSlackCredentials, slackApi } from "./slack-api.js";
6
5
  import { ChatState } from "./chat-delivery-state.js";
7
6
  import { receiveSlack, processSlackEvent, deliverSlackPart, planSlackNotifications, planSlackRooms, } from "./slack-chat.js";
8
- /** The bot and socket tokens must belong to the same app, including after reconnect. */
9
- export class ToolrollSlackSocket extends SocketModeClient {
10
- expectedApp = "";
11
- wrongApp = () => { };
12
- async onWebSocketMessage(data, isBinary) {
13
- let packet;
14
- try {
15
- packet = JSON.parse(typeof data === "string" ? data : new TextDecoder().decode(data));
16
- }
17
- catch {
18
- return;
19
- }
20
- if (packet.type === "hello") {
21
- const info = packet.connection_info;
22
- if (info?.app_id !== this.expectedApp) {
23
- this.wrongApp();
24
- await this.disconnect();
25
- return;
26
- }
27
- }
28
- return super.onWebSocketMessage(data, isBinary);
29
- }
30
- }
31
7
  export async function followSlack(options) {
32
8
  const owner = randomBytes(16).toString("hex"), state = new ChatState(options.store, "slack");
33
9
  const pause = async (ms) => {
@@ -56,6 +32,8 @@ export async function followSlack(options) {
56
32
  state.owns(credentials.installation, owner));
57
33
  };
58
34
  const problem = (text) => state.setProblem(credentials.installation, owner, text);
35
+ // The SDK loads once Slack is set up, not with every command that could host the follower.
36
+ const { ToolrollSlackSocket, LogLevel } = await import("./slack-socket.js");
59
37
  // SDK log output can include envelopes/tokens. Only fixed status phrases are saved.
60
38
  const client = new ToolrollSlackSocket({
61
39
  appToken: credentials.appToken,
@@ -0,0 +1,39 @@
1
+ import type { Store } from "./store.js";
2
+ export declare const STEER_HANDLED = "handled";
3
+ export type SteerMailbox = {
4
+ runId: number;
5
+ taskRef: number;
6
+ /** Worktree-relative, as the brief names it. */
7
+ folder: string;
8
+ dir: string;
9
+ /** The folder's real path, resolved once when it was made inside the worktree. */
10
+ realDir: string;
11
+ /** This attempt's note key: in the brief and in every genuine note, nowhere else. */
12
+ key: string;
13
+ /** The highest number this pulse has written. */
14
+ last: number;
15
+ /** Mailed and not yet moved to handled/: number → note id. */
16
+ waiting: Map<number, number>;
17
+ };
18
+ export declare const steerNoteName: (number: number) => string;
19
+ /** Make the folder and its handled/ inside the worktree; null when it cannot be made. */
20
+ export declare function openSteerMailbox(worktree: string, folder: string, runId: number, taskRef: number, key: string): SteerMailbox | null;
21
+ /** One beat: record what the agent read, then mail what is new while the agent runs. */
22
+ export declare function pulseSteerMailbox(store: Store, box: SteerMailbox, now: Date, agentRunning: boolean): void;
23
+ /** The attempt is over: one last read, then the folder goes. Unread notes stay pending for the next attempt. */
24
+ export declare function closeSteerMailbox(store: Store, box: SteerMailbox, now: Date): void;
25
+ /**
26
+ * Claude's doorbell: a synchronous PostToolUse hook that lists the folder and,
27
+ * while a note is unread, hands the agent one line of context between tool
28
+ * calls. It only says notes are waiting — it never quotes one, and its firing
29
+ * is never an acknowledgement. Rendered into the run's own --settings.
30
+ */
31
+ export declare function claudeSteerHooks(dir: string, folder: string): {
32
+ PostToolUse: {
33
+ matcher: string;
34
+ hooks: {
35
+ type: "command";
36
+ command: string;
37
+ }[];
38
+ }[];
39
+ };