toolroll 0.9.67 → 0.9.69

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 (250) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/acp-client.js +1 -3
  3. package/dist/agent-board.d.ts +0 -1
  4. package/dist/agent-board.js +0 -1
  5. package/dist/agent-fence.d.ts +58 -0
  6. package/dist/agent-fence.js +214 -19
  7. package/dist/agentconfig.d.ts +1 -1
  8. package/dist/agentconfig.js +6 -2
  9. package/dist/batch-checks.js +14 -26
  10. package/dist/browser/workspace.css +1 -1
  11. package/dist/browser/workspace.js +31 -29
  12. package/dist/builder.d.ts +9 -0
  13. package/dist/builder.js +30 -27
  14. package/dist/canonical-json.d.ts +14 -0
  15. package/dist/canonical-json.js +36 -0
  16. package/dist/chat-actions.js +1 -2
  17. package/dist/chat-admission.d.ts +19 -0
  18. package/dist/chat-admission.js +22 -0
  19. package/dist/chat-channel.js +1 -1
  20. package/dist/chat-delivery.js +0 -1
  21. package/dist/chat-follow.d.ts +17 -0
  22. package/dist/chat-follow.js +32 -0
  23. package/dist/chat-messages.js +0 -1
  24. package/dist/chat-settings-page.d.ts +23 -0
  25. package/dist/chat-settings-page.js +41 -0
  26. package/dist/check-access.d.ts +54 -0
  27. package/dist/check-access.js +80 -0
  28. package/dist/claim.js +3 -6
  29. package/dist/claude-print.d.ts +12 -0
  30. package/dist/claude-print.js +34 -0
  31. package/dist/cli-http.d.ts +0 -7
  32. package/dist/cli-http.js +0 -21
  33. package/dist/cli.d.ts +3 -10
  34. package/dist/cli.js +15 -24
  35. package/dist/coding-provider.js +2 -1
  36. package/dist/coding-workspace.js +1 -1
  37. package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
  38. package/dist/connect-cli.js +331 -0
  39. package/dist/containment.d.ts +0 -2
  40. package/dist/containment.js +2 -6
  41. package/dist/contracts/chat-actions.d.ts +0 -1
  42. package/dist/contracts/chat-content.d.ts +0 -4
  43. package/dist/contracts/chat-content.js +0 -2
  44. package/dist/contracts/chat-tables.d.ts +0 -1
  45. package/dist/contracts/cli.d.ts +0 -62
  46. package/dist/contracts/cli.js +0 -4
  47. package/dist/contracts/coding-workspace.js +1 -1
  48. package/dist/contracts/console-api.d.ts +1 -0
  49. package/dist/contracts/console-api.js +1 -0
  50. package/dist/contracts/contract.js +1 -1
  51. package/dist/contracts/flow-card.d.ts +0 -1
  52. package/dist/contracts/flow-propose.d.ts +0 -1
  53. package/dist/contracts/flow-send.js +1 -1
  54. package/dist/contracts/gateway-tools.d.ts +0 -1
  55. package/dist/contracts/integration-metadata.js +1 -1
  56. package/dist/contracts/lead-tools.d.ts +0 -1
  57. package/dist/contracts/memory-pass.d.ts +0 -1
  58. package/dist/contracts/memory-pass.js +1 -1
  59. package/dist/contracts/plan.js +1 -1
  60. package/dist/contracts/project-knowledge.js +1 -1
  61. package/dist/contracts/project-skills.d.ts +0 -1
  62. package/dist/contracts/proof.js +1 -1
  63. package/dist/contracts/recipes.js +1 -1
  64. package/dist/contracts/scope.js +1 -1
  65. package/dist/contracts/scout-report.js +1 -1
  66. package/dist/contracts/teams-callback.d.ts +0 -1
  67. package/dist/converse.d.ts +0 -6
  68. package/dist/converse.js +0 -19
  69. package/dist/daemon.js +3 -2
  70. package/dist/decision.d.ts +1 -3
  71. package/dist/deprecations.d.ts +12 -4
  72. package/dist/deprecations.js +27 -4
  73. package/dist/desktop-update-recovery.js +1 -1
  74. package/dist/desktop-update.js +3 -2
  75. package/dist/discord-chat.js +4 -15
  76. package/dist/discord-settings.d.ts +2 -6
  77. package/dist/discord-settings.js +10 -38
  78. package/dist/discord.js +12 -23
  79. package/dist/evidence.d.ts +0 -1
  80. package/dist/evidence.js +0 -3
  81. package/dist/exec.d.ts +0 -9
  82. package/dist/exec.js +3 -81
  83. package/dist/flow-actions.js +0 -3
  84. package/dist/flow-draft.d.ts +1 -3
  85. package/dist/flow-draft.js +15 -28
  86. package/dist/flow-scripts.d.ts +1 -8
  87. package/dist/flow-scripts.js +0 -13
  88. package/dist/flow-send.js +1 -1
  89. package/dist/flow-share.d.ts +0 -2
  90. package/dist/flow-share.js +1 -6
  91. package/dist/flow-steps.js +1 -1
  92. package/dist/flow-triggers.d.ts +0 -1
  93. package/dist/flow-triggers.js +1 -2
  94. package/dist/flow-unattended.js +1 -12
  95. package/dist/flows.js +1 -1
  96. package/dist/health-cli.d.ts +2 -2
  97. package/dist/health-cli.js +1 -1
  98. package/dist/invoke.d.ts +39 -5
  99. package/dist/invoke.js +181 -24
  100. package/dist/is-record.d.ts +2 -0
  101. package/dist/is-record.js +2 -0
  102. package/dist/lead-doors.d.ts +1 -5
  103. package/dist/lead-doors.js +0 -4
  104. package/dist/lead-tools.js +3 -6
  105. package/dist/live-detail.d.ts +0 -1
  106. package/dist/live-detail.js +0 -6
  107. package/dist/live-terminal.js +2 -1
  108. package/dist/live.d.ts +0 -1
  109. package/dist/live.js +1 -2
  110. package/dist/mcp-core.js +0 -2
  111. package/dist/memory-pass.js +1 -1
  112. package/dist/model-catalog.js +1 -1
  113. package/dist/modes.js +1 -12
  114. package/dist/operate.d.ts +4 -0
  115. package/dist/operate.js +456 -750
  116. package/dist/password-bearer.d.ts +0 -2
  117. package/dist/password-bearer.js +0 -4
  118. package/dist/peek-cli.js +2 -1
  119. package/dist/peek.d.ts +2 -26
  120. package/dist/peek.js +2 -168
  121. package/dist/phase-routing.d.ts +0 -4
  122. package/dist/phase-routing.js +2 -21
  123. package/dist/planner-source.d.ts +2 -1
  124. package/dist/planner-source.js +2 -12
  125. package/dist/planner.js +2 -14
  126. package/dist/process-tree.d.ts +0 -2
  127. package/dist/process-tree.js +0 -10
  128. package/dist/project-delete-ui.d.ts +16 -0
  129. package/dist/project-delete-ui.js +60 -1
  130. package/dist/project-delete.d.ts +0 -2
  131. package/dist/project-delete.js +1 -3
  132. package/dist/project-learning.d.ts +0 -14
  133. package/dist/project-skills.d.ts +0 -1
  134. package/dist/project-skills.js +0 -14
  135. package/dist/project-tools.js +2 -10
  136. package/dist/provider.d.ts +0 -17
  137. package/dist/provider.js +0 -26
  138. package/dist/pulls.d.ts +0 -2
  139. package/dist/pulls.js +0 -9
  140. package/dist/release-adapters.d.ts +3 -0
  141. package/dist/release-adapters.js +15 -5
  142. package/dist/remote-exec.d.ts +3 -2
  143. package/dist/remote-exec.js +3 -2
  144. package/dist/repos.d.ts +0 -5
  145. package/dist/repos.js +2 -10
  146. package/dist/repository-context.d.ts +0 -2
  147. package/dist/repository-context.js +0 -4
  148. package/dist/restart-certification.d.ts +0 -2
  149. package/dist/restart-certification.js +1 -5
  150. package/dist/result-follow-ups.d.ts +1 -4
  151. package/dist/result-follow-ups.js +11 -21
  152. package/dist/result-review.d.ts +0 -2
  153. package/dist/result-review.js +0 -2
  154. package/dist/retention.d.ts +0 -1
  155. package/dist/retention.js +0 -1
  156. package/dist/review-context.d.ts +0 -1
  157. package/dist/review-context.js +1 -12
  158. package/dist/reviewer.d.ts +0 -16
  159. package/dist/reviewer.js +0 -18
  160. package/dist/run-closeout.js +2 -1
  161. package/dist/scope.d.ts +9 -0
  162. package/dist/scope.js +13 -18
  163. package/dist/scout-browser.js +2 -1
  164. package/dist/serve.js +11 -2
  165. package/dist/server/console.css +5 -133
  166. package/dist/server/flows.js +2 -2
  167. package/dist/server/http.d.ts +4 -0
  168. package/dist/server/http.js +5 -0
  169. package/dist/server/pages.js +2 -2
  170. package/dist/server/people-tokens.js +3 -4
  171. package/dist/server/remote.js +1 -6
  172. package/dist/server/render-chat.d.ts +9 -2
  173. package/dist/server/render-chat.js +53 -7
  174. package/dist/server/render-results.d.ts +3 -6
  175. package/dist/server/render-results.js +4 -9
  176. package/dist/server/render-tasks.js +6 -6
  177. package/dist/server/route-table.d.ts +0 -4
  178. package/dist/server/route-table.js +1 -6
  179. package/dist/server/settings.js +60 -23
  180. package/dist/server/task-views.js +1 -1
  181. package/dist/server/tasks.js +5 -3
  182. package/dist/setup-guide.d.ts +2 -2
  183. package/dist/skills.d.ts +0 -6
  184. package/dist/skills.js +0 -8
  185. package/dist/slack-chat.d.ts +1 -7
  186. package/dist/slack-chat.js +5 -18
  187. package/dist/slack-settings.d.ts +2 -6
  188. package/dist/slack-settings.js +10 -38
  189. package/dist/slack.js +12 -23
  190. package/dist/sleep.d.ts +2 -0
  191. package/dist/sleep.js +2 -0
  192. package/dist/store.d.ts +29 -75
  193. package/dist/store.js +89 -180
  194. package/dist/structured-output.d.ts +0 -1
  195. package/dist/structured-output.js +0 -1
  196. package/dist/subagent-work.d.ts +0 -2
  197. package/dist/subagent-work.js +0 -2
  198. package/dist/subagents.d.ts +1 -3
  199. package/dist/subagents.js +20 -33
  200. package/dist/subscription-chat.d.ts +1 -3
  201. package/dist/subscription-chat.js +2 -3
  202. package/dist/surface.js +2 -1
  203. package/dist/task-sizing.d.ts +1 -4
  204. package/dist/task-sizing.js +3 -10
  205. package/dist/teams-chat.js +4 -11
  206. package/dist/teams.js +12 -19
  207. package/dist/telegram-lead.js +2 -2
  208. package/dist/telegram.js +1 -1
  209. package/dist/token-notices.d.ts +0 -11
  210. package/dist/token-notices.js +1 -5
  211. package/dist/tool-secret-store.d.ts +0 -3
  212. package/dist/tool-secret-store.js +2 -11
  213. package/dist/toolroll-update.d.ts +0 -2
  214. package/dist/toolroll-update.js +2 -2
  215. package/dist/verification-evidence.d.ts +0 -1
  216. package/dist/verification-evidence.js +0 -1
  217. package/dist/webhooks.d.ts +0 -1
  218. package/dist/webhooks.js +0 -1
  219. package/dist/workspace-ui.d.ts +0 -8
  220. package/dist/workspace-ui.js +1 -6
  221. package/package.json +1 -1
  222. package/dist/contracts/native-census.d.ts +0 -42
  223. package/dist/contracts/native-census.js +0 -36
  224. package/dist/process-recovery-anchor.d.ts +0 -75
  225. package/dist/process-recovery-anchor.js +0 -188
  226. package/dist/process-recovery-coalition.d.ts +0 -25
  227. package/dist/process-recovery-coalition.js +0 -105
  228. package/dist/process-recovery-eligibility.d.ts +0 -179
  229. package/dist/process-recovery-eligibility.js +0 -174
  230. package/dist/process-recovery-native.d.ts +0 -161
  231. package/dist/process-recovery-native.js +0 -595
  232. package/dist/process-recovery-provenance.d.ts +0 -163
  233. package/dist/process-recovery-provenance.js +0 -292
  234. package/dist/process-recovery-services.d.ts +0 -56
  235. package/dist/process-recovery-services.js +0 -191
  236. package/dist/process-recovery-settlement.d.ts +0 -32
  237. package/dist/process-recovery-settlement.js +0 -133
  238. package/dist/process-recovery.d.ts +0 -41
  239. package/dist/process-recovery.js +0 -91
  240. package/dist/session-brief.d.ts +0 -8
  241. package/dist/session-brief.js +0 -24
  242. package/dist/session-cli.d.ts +0 -42
  243. package/dist/session-cli.js +0 -271
  244. package/dist/session-contract.d.ts +0 -183
  245. package/dist/session-contract.js +0 -122
  246. package/dist/session-remote.d.ts +0 -19
  247. package/dist/session-remote.js +0 -86
  248. package/dist/session-service.d.ts +0 -22
  249. package/dist/session-service.js +0 -157
  250. package/dist/team-cli.js +0 -692
package/dist/scope.js CHANGED
@@ -11,6 +11,7 @@ import { claimActor, currentActor } from "./actor.js";
11
11
  import { parseLeadToken } from "./api-tokens.js";
12
12
  import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
13
13
  import { levelOfProfile } from "./policy.js";
14
+ import { canonicalJson } from "./canonical-json.js";
14
15
  export { isAcpProfile };
15
16
  /**
16
17
  * What a task is allowed to become, agreed before anything builds it.
@@ -40,6 +41,7 @@ export { isAcpProfile };
40
41
  import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
41
42
  import { networkFromValue, networkWords } from "./egress.js";
42
43
  import { isWritesMode, writesWords } from "./writes-mode.js";
44
+ import { checkAccessWords, isCheckAccess } from "./check-access.js";
43
45
  import { z } from "zod";
44
46
  import {} from "./contracts/acceptance-terms.js";
45
47
  import { parseContract } from "./contracts/contract.js";
@@ -133,19 +135,6 @@ export function watchdogsOf(profile, timeoutMs, repair = false) {
133
135
  return profile.timeoutKind === "idle" ? { idleTimeoutMs: timeoutMs } : { timeoutMs };
134
136
  }
135
137
  export const PROFILE_DIGEST_VERSION = 2;
136
- /** Deterministic JSON: object keys sorted recursively, arrays in order. */
137
- function canonicalJson(value) {
138
- if (Array.isArray(value))
139
- return `[${value.map(canonicalJson).join(",")}]`;
140
- if (value !== null && typeof value === "object") {
141
- const entries = Object.entries(value)
142
- .filter(([, v]) => v !== undefined)
143
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
144
- .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
145
- return `{${entries.join(",")}}`;
146
- }
147
- return JSON.stringify(value);
148
- }
149
138
  /** The stored snapshot bytes: version embedded IN the snapshot (finding 17). */
150
139
  export function canonicalProfileJson(profile) {
151
140
  return canonicalJson({ digestVersion: PROFILE_DIGEST_VERSION, profile });
@@ -366,9 +355,14 @@ export function profileFromJson(json) {
366
355
  const writes = p["writes"];
367
356
  if (writes !== undefined && !isWritesMode(writes))
368
357
  return null;
369
- const withNetwork = { ...(network === undefined ? {} : { network }), ...(writes === undefined ? {} : { writes }) };
358
+ // Check access is sealed only when wider than sandboxed: an explicit "sandboxed", or any other word, is not this code's.
359
+ const sealedAccess = p["checkAccess"];
360
+ if (sealedAccess !== undefined && (sealedAccess === "sandboxed" || !isCheckAccess(sealedAccess)))
361
+ return null;
362
+ const checkAccess = sealedAccess;
363
+ const withNetwork = { ...(network === undefined ? {} : { network }), ...(writes === undefined ? {} : { writes }), ...(checkAccess === undefined ? {} : { checkAccess }) };
370
364
  if (p["provider"] === "claude") {
371
- if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network", "writes"]) &&
365
+ if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network", "writes", "checkAccess"]) &&
372
366
  str(p["model"]) &&
373
367
  (p["permissionArgv"] === "auto" || p["permissionArgv"] === "acceptEdits" || p["permissionArgv"] === "bypassPermissions") &&
374
368
  num(p["maxTurns"]) && num(p["repairMaxTurns"]) &&
@@ -390,7 +384,7 @@ export function profileFromJson(json) {
390
384
  return null;
391
385
  }
392
386
  if (p["provider"] === "codex" || p["provider"] === "openrouter") {
393
- if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network", "writes"]) &&
387
+ if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network", "writes", "checkAccess"]) &&
394
388
  str(p["model"]) &&
395
389
  (p["sandboxMode"] === "workspace-write" || p["sandboxMode"] === "danger-full-access") &&
396
390
  p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
@@ -412,7 +406,7 @@ export function profileFromJson(json) {
412
406
  return null;
413
407
  }
414
408
  if (p["provider"] === "gemini") {
415
- if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network", "writes"]) &&
409
+ if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network", "writes", "checkAccess"]) &&
416
410
  str(p["model"]) &&
417
411
  (p["approvalArgv"] === "auto_edit" || p["approvalArgv"] === "yolo") &&
418
412
  p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
@@ -434,7 +428,7 @@ export function profileFromJson(json) {
434
428
  return null;
435
429
  }
436
430
  if (typeof p["provider"] === "string" && isAcpProvider(p["provider"])) {
437
- if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network", "writes"]) &&
431
+ if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network", "writes", "checkAccess"]) &&
438
432
  str(p["model"]) &&
439
433
  p["permission"] === "full" &&
440
434
  p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
@@ -1283,6 +1277,7 @@ export function describeScope(scope, readiness = NO_READINESS) {
1283
1277
  // What the build may reach (egress.ts), part of what the yes agrees to.
1284
1278
  ...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
1285
1279
  ...(scope.profile === null || scope.profile === undefined ? [] : [` writes ${writesWords(scope.profile.writes)}`]),
1280
+ ...(scope.profile?.checkAccess === undefined ? [] : [` checks ${checkAccessWords(scope.profile.checkAccess)}`]),
1286
1281
  ` reference ${scope.digest}`,
1287
1282
  ` approved ${describeApproval(approval)}`,
1288
1283
  ];
@@ -16,6 +16,7 @@ import { request as httpRequest } from "node:http";
16
16
  import { connect, createServer } from "node:net";
17
17
  import { readdirSync, rmSync } from "node:fs";
18
18
  import { join } from "node:path";
19
+ import { sleep } from "./sleep.js";
19
20
  /** The public page the check opens through the scout's proxy. */
20
21
  export const PREFLIGHT_PAGE = "https://example.com/";
21
22
  const PREFLIGHT_TIMEOUT_MS = 60_000;
@@ -90,7 +91,7 @@ async function serveBrowser(launch, options, deadline, late) {
90
91
  };
91
92
  // First runs download the pinned tool; it listens once it is ready.
92
93
  while (stopped === null && Date.now() < deadline && !(await answers(port)))
93
- await new Promise(done => setTimeout(done, 250));
94
+ await sleep(250);
94
95
  if (stopped !== null || Date.now() >= deadline) {
95
96
  const reason = stopped ?? late;
96
97
  await close();
package/dist/serve.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { taskMovedSince } from "./chat-ask.js";
2
+ import { linuxFenceAvailable, macosFenceAvailable } from "./agent-fence.js";
3
+ import { signInsUnavailableWords } from "./check-access.js";
2
4
  import { LIVE_VIEW_CONSENT, liveCommandsShown, liveViewConsentDigest } from "./live-detail.js";
3
5
  import { html, htmlString, jsonScript, textHtml } from "./html.js";
4
6
  import { browserAssetsAvailable, browserWorkspaceDocument } from './browser-shell.js';
@@ -151,7 +153,7 @@ import { loadConsoleUrl } from "./webhooks.js";
151
153
  import { reviewFactsOf } from "./workspace-ui.js";
152
154
  import { WorktreePool } from "./worktree.js";
153
155
  export function createDecisionServer(options) {
154
- const { guided, projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, earlierLiveVersions, familyHistory, freshAssignment, workRowOf, taskListPane, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, taskPullRequestOf, failureOf, explainAttempt, runChecksHere, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, progressOf, resultDetailOf } = createTaskViews({
156
+ const { projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, failureOf, explainAttempt, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, resultDetailOf } = createTaskViews({
155
157
  get store() { return store; },
156
158
  get projectCounts() { return projectCounts; },
157
159
  get admissionList() { return admissionList; },
@@ -170,7 +172,7 @@ export function createDecisionServer(options) {
170
172
  get dockedConversation() { return dockedConversation; },
171
173
  get liveRefreshSeconds() { return liveRefreshSeconds; },
172
174
  });
173
- const { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible } = createSharedGuards({
175
+ const { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible } = createSharedGuards({
174
176
  get store() { return store; },
175
177
  get liveCeiling() { return liveCeiling; },
176
178
  get codingProjects() { return codingProjects; },
@@ -448,6 +450,13 @@ export function createDecisionServer(options) {
448
450
  access: projectAccessOf(repo),
449
451
  network: { setting: store.projectNetwork(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
450
452
  writes: { setting: store.projectWrites(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
453
+ checkAccess: {
454
+ setting: store.projectCheckAccess(repo), changed: store.projectCheckAccessChange(repo),
455
+ canNarrow: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo),
456
+ canWiden: who.via === "cookie" && !store.isDemo() && store.isInstanceOperator(who.name),
457
+ unconfinedHere: !(options.fenceAvailable?.() ?? (macosFenceAvailable() || linuxFenceAvailable())) && store.unfencedAgentsAllowed(),
458
+ signInsUnavailable: signInsUnavailableWords(options.platform ?? process.platform),
459
+ },
451
460
  liveView: (() => {
452
461
  const commands = liveCommandsShown(store, repo);
453
462
  const canChange = who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo);
@@ -39,14 +39,14 @@
39
39
  :root {
40
40
  --background: var(--so-ground); --foreground: var(--so-ink); --card: var(--so-paper);
41
41
  --muted: var(--so-raised); --muted-foreground: var(--so-muted); --border: var(--so-line); --input: var(--so-input-line);
42
- --primary: var(--so-accent); --primary-foreground: var(--so-on-accent); --secondary: var(--so-raised); --secondary-foreground: var(--so-ink);
43
- --accent: var(--so-soft); --destructive: var(--so-danger); --destructive-strong: var(--so-danger); --destructive-soft: var(--so-danger-soft);
42
+ --primary: var(--so-accent); --primary-foreground: var(--so-on-accent); --secondary: var(--so-raised);
43
+ --accent: var(--so-soft); --destructive: var(--so-danger); --destructive-soft: var(--so-danger-soft);
44
44
  --success: var(--so-success); --success-soft: var(--so-success-soft); --warning: var(--so-warning); --warning-soft: var(--so-warning-soft);
45
45
  --running: var(--so-info); --running-soft: var(--so-info-soft); --ring: var(--so-signal);
46
46
  /* The accent means one thing: a person is needed (approve, answer, decide). */
47
47
  --brand: var(--so-attention); --brand-foreground: var(--so-on-attention); --brand-soft: var(--so-attention-soft);
48
48
  --glass: var(--so-paper); --glass-strong: var(--so-paper); --glass-border: var(--so-line); --glass-highlight: transparent;
49
- --ambient-one: transparent; --ambient-two: transparent; --user-message: var(--so-user-bubble);
49
+ --user-message: var(--so-user-bubble);
50
50
  --surface: var(--so-paper); --ok: var(--so-success); --danger: var(--so-danger); --fg-muted: var(--so-muted);
51
51
  --shadow: none; --shadow-overlay: var(--so-shadow-overlay);
52
52
  --radius: 0.625rem;
@@ -58,7 +58,7 @@
58
58
  and a long press never selects a control's label. Text stays selectable. */
59
59
  html { -webkit-tap-highlight-color: transparent; }
60
60
  a, button, summary, [role=button], [role=tab] { touch-action: manipulation; }
61
- button, [role=button], [role=tab], .task-context-chip, .approval-chip, .verdict-chip { -webkit-user-select: none; user-select: none; }
61
+ button, [role=button], [role=tab], .task-context-chip, .verdict-chip { -webkit-user-select: none; user-select: none; }
62
62
  ::selection { background: var(--so-selection); color: var(--so-ink); }
63
63
  ::placeholder { color: var(--muted-foreground); }
64
64
  body {
@@ -73,23 +73,9 @@
73
73
  /* Scrollbars belong to the theme, not the platform default. */
74
74
  * { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
75
75
 
76
- .topbar {
77
- position: sticky; top: 0; z-index: 10;
78
- border-bottom: 1px solid var(--border); background: var(--background);
79
- }
80
- .topbar-inner {
81
- max-width: 44rem; margin-inline: auto; padding: 0 1.25rem; height: 3.25rem;
82
- display: flex; align-items: center; gap: 1.25rem;
83
- }
84
76
  .brand { font-weight: 600; letter-spacing: -0.01em; color: var(--foreground); text-decoration: none;
85
77
  display: flex; align-items: center; height: 100%; }
86
78
  .brand .dot { color: var(--muted-foreground); }
87
- .topbar nav { display: flex; gap: .25rem; margin-left: auto; height: 100%; }
88
- .topbar nav a {
89
- color: var(--muted-foreground); text-decoration: none; font-size: 0.8125rem; font-weight: 500;
90
- display: flex; align-items: center; padding: 0 .625rem; transition: color .15s;
91
- }
92
- @media (hover: hover) and (pointer: fine) { .topbar nav a:hover { color: var(--foreground); } }
93
79
  main { max-width: 44rem; margin-inline: auto; padding: 1.75rem 1.25rem 4rem; }
94
80
  h1 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 .25rem; line-height: 1.3; }
95
81
  h1 .meta { font-weight: 400; letter-spacing: 0; }
@@ -118,13 +104,6 @@
118
104
  /* Utility classes replacing the old inline style= attributes. */
119
105
  .tight { margin-top: 0; }
120
106
  .card > h2:first-child, .card > h3:first-child { margin-top: 0; }
121
- .w-xs { width: 4.5rem; } .w-sm { width: 8rem; } .w-md { width: 12rem; } .w-lg { width: 14rem; }
122
- .field-cap { width: 100%; max-width: 28rem; }
123
- .field-cap-sm { width: 100%; max-width: 22rem; }
124
- .field-cap-lg { width: 100%; max-width: 34rem; }
125
- .prewrap { white-space: pre-wrap; }
126
- .wrap-any { overflow-wrap: anywhere; }
127
- .grab { cursor: grab; user-select: none; }
128
107
 
129
108
  .palette {
130
109
  position: fixed; top: 18vh; left: 50%; transform: translateX(-50%); width: min(32rem, 90vw);
@@ -298,12 +277,6 @@
298
277
  main:has(.task-intake) { max-width: 68rem; }
299
278
  .task-intake { width: min(100%, 50rem); margin: clamp(1rem, 5vh, 4rem) auto 0; }
300
279
  .task-intake-hero { text-align: center; margin: 0 auto 1.35rem; max-width: 38rem; }
301
- .task-intake-mark {
302
- display: grid; place-items: center; width: 3rem; height: 3rem; margin: 0 auto .85rem;
303
- border: 1px solid var(--border); border-radius: .75rem;
304
- background: var(--card); box-shadow: var(--so-pill-shadow);
305
- font: 600 .72rem/1 var(--font-mono); letter-spacing: -.05em;
306
- }
307
280
  .task-intake-hero h1 { margin: 0; font-size: clamp(1.65rem, 4vw, 2.2rem); letter-spacing: -.045em; }
308
281
  .task-intake-hero p { margin: .45rem 0 0; color: var(--muted-foreground); }
309
282
  .task-composer { padding: .75rem; border-radius: 1.45rem; box-shadow: var(--shadow-overlay); }
@@ -461,8 +434,6 @@
461
434
  .cockpit-row-meta { display: block; color: var(--muted-foreground); font-size: .68rem; overflow-wrap: anywhere; }
462
435
  .cockpit-why { display: block; color: var(--muted-foreground); font-size: .7rem; line-height: 1.35; overflow-wrap: anywhere; }
463
436
  .refusal-back a { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; }
464
- .cockpit-primary { margin: .85rem 0 1rem; }
465
- .cockpit-primary .button-link { white-space: nowrap; }
466
437
  .cockpit-detail { min-width: 0; }
467
438
  .cockpit-head h1 { margin: .2rem 0 .55rem; color: var(--foreground); font-size: clamp(1.2rem, 2vw, 1.45rem); font-weight: 600; line-height: 1.3; letter-spacing: -.025em; overflow-wrap: anywhere; }
468
439
  .cockpit-chips { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem; margin: 0 0 .35rem; }
@@ -500,22 +471,6 @@
500
471
  .cockpit-proof-group { margin: .65rem 0; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--glass-strong) 62%, transparent); }
501
472
  .cockpit-proof-group > summary { padding: .65rem .75rem; cursor: pointer; font-weight: 550; }
502
473
  .cockpit-proof-group .criterion-matrix { padding: 0 .75rem .7rem; }
503
- .cockpit-proof-summary { display: grid; gap: .25rem; margin: .15rem 0 .75rem; line-height: 1.45; }
504
- .cockpit-proof-summary strong { font-size: .88rem; }
505
- .cockpit-evidence-group { margin: .65rem 0 0; scroll-margin-top: 6rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); overflow: hidden; }
506
- .cockpit-evidence-group > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; padding: .75rem; cursor: pointer; list-style: none; font-weight: 550; }
507
- .cockpit-evidence-group > summary::-webkit-details-marker { display: none; }
508
- .cockpit-evidence-group > summary > span { flex: none; white-space: nowrap; }
509
- .cockpit-evidence-group > summary small { min-width: 0; color: var(--muted-foreground); font-size: .7rem; font-weight: 400; text-align: right; }
510
- .cockpit-evidence-group [data-primary-evidence] { scroll-margin-top: 6rem; }
511
- .cockpit-evidence-body { padding: 0 .75rem .75rem; border-top: 1px solid var(--glass-border); }
512
- .cockpit-accept { margin-top: .65rem; padding-top: .65rem; border-top: 1px solid var(--glass-border); }
513
- .cockpit-accept > summary { cursor: pointer; font-size: .78rem; font-weight: 600; }
514
- .cockpit-accept[open] > summary { margin-bottom: .55rem; }
515
- .cockpit-accept p { margin: 0 0 .55rem; }
516
- .cockpit-accept-form { display: flex; gap: .5rem; margin: 0; }
517
- .cockpit-accept-form input { margin: 0; min-width: 10rem; }
518
- .cockpit-accept-form button { flex: none; background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
519
474
  .diff-review {
520
475
  overflow: hidden; margin: .8rem 0 .5rem; border: 1px solid var(--glass-border);
521
476
  border-radius: var(--radius); background: color-mix(in srgb, var(--card) 82%, transparent);
@@ -757,7 +712,6 @@
757
712
  margin: 0; color: var(--foreground); font-size: 1.0625rem; line-height: 1.35;
758
713
  letter-spacing: -.015em;
759
714
  }
760
- .project-add-intro { margin: .25rem 0 0; max-width: 36rem; }
761
715
  .project-add-actions {
762
716
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
763
717
  gap: .625rem; margin-top: 1rem;
@@ -822,7 +776,6 @@
822
776
  .nav-group > summary .chevron { width: .875rem; height: .875rem; flex: none; transition: transform .15s; }
823
777
  .nav-group[open] > summary .chevron { transform: rotate(180deg); }
824
778
  .nav-group .nav-group-items { display: flex; flex-direction: column; gap: .125rem; margin: .125rem 0 .25rem; }
825
- .side .nav-settings { margin-top: .375rem; }
826
779
  .side nav a {
827
780
  position: relative;
828
781
  display: flex; align-items: center; gap: .625rem; padding: .4375rem .625rem; min-height: 2.125rem;
@@ -1051,15 +1004,6 @@
1051
1004
  .task-control[data-task-control="paused"] .eyebrow { color: var(--destructive); }
1052
1005
  .resume-ceremony .row { margin: .3rem 0; }
1053
1006
  @media (max-width: 480px) { .task-control { align-items: stretch; } .task-control-form, .task-control-button { width: 100%; } .task-control-form .task-control-button { width: 100%; } }
1054
- .review-attempts { margin: .55rem 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: .2rem; font-size: .8125rem; }
1055
- .review-attempts li { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .45rem; min-width: 0; }
1056
- .review-attempts .review-attempt-ordinal { font-weight: 600; }
1057
- .review-attempts .meta { min-width: 0; overflow-wrap: anywhere; }
1058
- .review-retry-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .55rem; margin-top: .65rem; }
1059
- .review-retry-actions form.inline { margin: 0; }
1060
- .review-retry-button { width: auto; max-width: 100%; white-space: normal; }
1061
- .review-retry-form .review-retry-button { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
1062
- .review-retry-button[disabled] { opacity: .65; cursor: not-allowed; }
1063
1007
  .proof-review-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .55rem; margin: .65rem 0; }
1064
1008
  .proof-review-actions > .button-link { flex: none; }
1065
1009
  details.proof-exception { margin: 0; }
@@ -1113,9 +1057,6 @@
1113
1057
  .approve-form .recap { margin: .125rem 0 .5rem; }
1114
1058
  .approval-card { padding: 1.2rem 1.3rem; border-radius: calc(var(--radius) + 3px); }
1115
1059
  .approval-card .ceremony-head { align-items: flex-start; padding-bottom: .85rem; border-bottom: 1px solid var(--glass-border); }
1116
- .approval-title { display: grid; gap: .15rem; }
1117
- .approval-kicker { color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
1118
- .approval-title strong { font-size: 1.05rem; letter-spacing: -.02em; }
1119
1060
  .approval-label { display: block; margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500 12px/1.4 var(--font-sans); }
1120
1061
  .approval-label::first-letter, .planner-plan .eyebrow::first-letter { text-transform: uppercase; }
1121
1062
  .planner-plan .eyebrow { font: 500 12px/1.4 var(--font-sans); letter-spacing: 0; text-transform: none; }
@@ -1124,7 +1065,6 @@
1124
1065
  .approval-boundary { padding: .7rem .8rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--muted) 45%, transparent); }
1125
1066
  .approval-boundary .approval-label { margin: 0 0 .2rem; }
1126
1067
  .approval-boundary p { margin: 0; color: var(--muted-foreground); font-size: .8rem; overflow-wrap: anywhere; }
1127
- .approval-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0 .3rem; }
1128
1068
  /* The filed contract (contract handoff, task 1): preserved, amended, or
1129
1069
  drafted from nothing — the same panel on the task page, in the
1130
1070
  ceremony, and on /next, so an amendment is never a surprise after the
@@ -1189,7 +1129,6 @@
1189
1129
  .task-chat-agents-aside .agents-summary { font-size: .74rem; }
1190
1130
  .task-chat-agents-aside .agents-badges { margin-top: .35rem; }
1191
1131
  .task-chat-agents-aside .agents-roles { grid-template-columns: 1fr; }
1192
- .approval-chip { padding: .25rem .6rem; border: 1px solid var(--glass-border); border-radius: 999px; color: var(--muted-foreground); background: var(--glass); font-size: .7rem; }
1193
1132
  .approval-confirm { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .75rem; align-items: end; margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--glass-border); }
1194
1133
  .approval-confirm label { margin: 0; }
1195
1134
  .approval-card .approval-confirm button { min-height: 44px; white-space: nowrap; }
@@ -1508,7 +1447,6 @@
1508
1447
  .result-facts dd form { margin: .35rem 0 0; }
1509
1448
  .result-stat { margin: .2rem 0 .4rem; }
1510
1449
  .result-files { margin: .4rem 0 .6rem; }
1511
- .result-label { display: block; margin: .5rem 0 .25rem; font: 600 .6875rem/1.3 var(--font-mono); color: var(--muted-foreground); letter-spacing: .06em; text-transform: uppercase; }
1512
1450
  .result-verdict { align-items: center; }
1513
1451
  .result-verdict > .meta { min-width: 0; overflow-wrap: anywhere; }
1514
1452
  .result-request { margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid var(--border); }
@@ -1532,7 +1470,6 @@
1532
1470
  .result-links a { font-weight: 550; }
1533
1471
  .result-links a + a::before { content: "·"; margin-right: .5rem; color: var(--muted-foreground); font-weight: 400; }
1534
1472
  .cockpit-detail .result-panel { margin: .85rem 0; }
1535
- .cockpit-accept { margin-top: .75rem; }
1536
1473
  /* The result beside the conversation (package 3): with room, two
1537
1474
  columns and no second auxiliary panel; without it, the dedicated
1538
1475
  result view with Back to chat and the conversation out of the way. */
@@ -1572,29 +1509,17 @@
1572
1509
  toggle share one wrapping line; an opened disclosure is wider than
1573
1510
  its summary, so it wraps onto its own full-width line by itself. */
1574
1511
  .work-row-status { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .875rem; }
1575
- .work-details, .work-details[open] { max-width: 100%; margin: 0; padding: 0; border: 0; background: none; border-radius: 0; }
1576
- .work-details > summary {
1577
- list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .25rem;
1578
- min-height: 2.25rem; padding: 0 .25rem; margin: 0 -.25rem; border-radius: .375rem;
1579
- font-size: .8125rem; font-weight: 500; color: var(--muted-foreground);
1580
- }
1581
- .work-details > summary::-webkit-details-marker { display: none; }
1582
- .work-details > summary::before { content: "▸"; }
1583
- .work-details[open] > summary::before { content: "▾"; }
1584
- @media (hover: hover) and (pointer: fine) { .work-details > summary:hover { color: var(--foreground); } }
1585
1512
  .work-title { display: block; font-weight: 600; font-size: .9375rem; line-height: 1.35; color: var(--foreground); text-decoration: none; overflow-wrap: anywhere; }
1586
1513
  @media (hover: hover) and (pointer: fine) { .work-title:hover { text-decoration: underline; } }
1587
1514
  .work-meta { display: flex; flex-wrap: wrap; gap: .25rem .625rem; margin: .25rem 0 0; font-size: .75rem; color: var(--muted-foreground); }
1588
1515
  .work-meta .mono { overflow-wrap: anywhere; }
1589
1516
  .project-label { display: inline-block; padding: 0 .4rem; border: 1px solid var(--border); border-radius: 999px; font-size: .6875rem; line-height: 1.5; color: var(--foreground); }
1590
1517
  .work-detail { margin: 0 0 .25rem; font-size: .8125rem; line-height: 1.45; color: var(--muted-foreground); overflow-wrap: anywhere; }
1591
- .work-details .work-id { margin: 0 0 .25rem; }
1592
1518
  .work-action { display: inline-flex; align-items: center; min-height: 44px; font-size: .8125rem; font-weight: 500; }
1593
1519
  .work-empty { padding: 2rem 0 1rem; max-width: 34rem; }
1594
1520
  .work-empty p { margin: 0 0 .75rem; color: var(--muted-foreground); line-height: 1.5; }
1595
1521
  .work-empty .row { display: flex; align-items: center; gap: 1rem; }
1596
1522
  .work-empty .row > a { display: inline-flex; align-items: center; min-height: 44px; }
1597
- .work-bound { margin-top: .75rem; }
1598
1523
  .status-line { display: inline-flex; align-items: center; gap: .45rem; min-width: 0; font-size: .875rem; font-weight: 600; color: var(--foreground); }
1599
1524
  .status-line .status-label { min-width: 0; overflow-wrap: anywhere; }
1600
1525
  .status-line .status-dot { flex: none; width: .5rem; height: .5rem; border-radius: 50%; background: var(--muted-foreground); }
@@ -1629,7 +1554,7 @@
1629
1554
  .work-row { grid-template-columns: minmax(0, 1fr); gap: .125rem; padding: .625rem 0; }
1630
1555
  .work-head { align-items: center; }
1631
1556
  .work-head h1 { margin-bottom: 0; }
1632
- .work-details > summary, .work-action { min-height: 44px; }
1557
+ .work-action { min-height: 44px; }
1633
1558
  /* The tools control sits at the head's right edge on every width, so
1634
1559
  its menu keeps the desk's right-aligned anchor here too. A phone
1635
1560
  override once re-anchored it at left: 0, which pushed the opened
@@ -1771,7 +1696,6 @@
1771
1696
  /* Caution without a claim on the operator: quiet, not magenta. */
1772
1697
  .dot-warn { background: var(--muted-foreground); }
1773
1698
  .dot-off { background: var(--muted-foreground); opacity: .5; }
1774
- .dot-bad { background: var(--destructive); }
1775
1699
  .pulse { animation: pulse 2s ease-in-out infinite; }
1776
1700
  .dot-ok.pulse { background: var(--running); }
1777
1701
  @keyframes pulse { 50% { opacity: .35; } }
@@ -1811,7 +1735,6 @@
1811
1735
 
1812
1736
  @media (max-width: 640px) {
1813
1737
  button, form.option button { min-height: 2.75rem; }
1814
- .topbar nav a { padding: 0 .5rem; }
1815
1738
  }
1816
1739
 
1817
1740
  /* The board: lanes as columns, the pipeline left to right. */
@@ -1935,23 +1858,6 @@
1935
1858
  .control-room-head h1 { font-size: 1.375rem; margin-top: .08rem; }
1936
1859
  .control-room-head .actions { display: flex; gap: .45rem; flex-wrap: wrap; justify-content: flex-end; }
1937
1860
  .control-room-head .actions a { text-decoration: none; }
1938
- .command-metrics {
1939
- display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
1940
- gap: .625rem; margin: .85rem 0 1.5rem;
1941
- }
1942
- .command-metric {
1943
- border: 1px solid var(--border); border-radius: calc(var(--radius) - 2px); background: var(--card);
1944
- padding: .8rem .9rem; min-width: 0;
1945
- }
1946
- .command-metric .value { display: block; margin-top: .25rem; font-size: 1.5rem; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
1947
- .command-metric .label { display: flex; align-items: center; gap: .4rem; font-weight: 500; font-size: .8125rem; }
1948
- .command-metric .label::before {
1949
- content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full);
1950
- background: var(--muted-foreground); flex: none;
1951
- }
1952
- .command-metric .detail { display: block; color: var(--muted-foreground); font-size: .6875rem; margin-top: .18rem; }
1953
- .command-metric.attention .label::before { background: var(--brand); }
1954
- .command-metric.live .label::before { background: var(--running); }
1955
1861
  .workspace-pulse { margin: .4rem 0 1.5rem; display: grid; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); gap: .625rem; }
1956
1862
  /* The workspace card: name and status word, four counts, the same counts
1957
1863
  as a bar, and the one tap to its board. Neutral border always; the
@@ -1980,17 +1886,8 @@
1980
1886
  .task-chat-context-head { display: flex; align-items: center; justify-content: space-between; gap: .65rem; }
1981
1887
  .task-chat-context h2 { margin: .7rem 0 .25rem; color: var(--foreground); font-size: 1rem; line-height: 1.35; letter-spacing: -.025em; }
1982
1888
  .task-chat-context > .meta { margin: 0; overflow-wrap: anywhere; font-size: .65rem; }
1983
- .task-chat-status { display: grid; gap: .2rem; margin-top: .85rem; padding: .7rem; border-radius: calc(var(--radius) - 3px); background: var(--warning-soft); }
1984
- .task-chat-status.ready { background: var(--running-soft); }
1985
- .task-chat-status strong { font-size: .75rem; }
1986
- .task-chat-status span { color: var(--muted-foreground); font-size: .68rem; line-height: 1.45; }
1987
- .task-chat-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .4rem; margin: .65rem 0; }
1988
- .task-chat-facts div { min-width: 0; padding: .45rem .5rem; border-radius: calc(var(--radius) - 5px); background: color-mix(in srgb, var(--muted) 58%, transparent); }
1989
- .task-chat-facts dt { color: var(--muted-foreground); font: 400 .58rem/1.2 var(--font-mono); text-transform: uppercase; letter-spacing: .04em; }
1990
- .task-chat-facts dd { margin: .15rem 0 0; font-size: .68rem; overflow-wrap: anywhere; }
1991
1889
  .task-chat-overview-actions { display: grid; justify-items: start; gap: .55rem; margin-top: .75rem; }
1992
1890
  .task-chat-overview-link { display: block; font-size: .72rem; text-decoration: none; }
1993
- .task-chat-recovery-link { min-height: 2rem; padding-inline: .7rem; font-size: .7rem; }
1994
1891
  #task-chat-live { display: grid; gap: 1rem; margin-bottom: 1.1rem; }
1995
1892
  .task-journey {
1996
1893
  position: relative; overflow: hidden; padding: 1rem 1.05rem;
@@ -1998,7 +1895,6 @@
1998
1895
  background: var(--card);
1999
1896
  }
2000
1897
  .task-journey::after { content: ""; position: absolute; width: 9rem; height: 9rem; right: -4rem; top: -5rem; border-radius: 50%; background: color-mix(in srgb, var(--accent) 8%, transparent); filter: blur(12px); pointer-events: none; }
2001
- .task-journey-head { position: relative; z-index: 1; display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
2002
1898
  .task-journey h2 { margin: .2rem 0 0; font-size: 1rem; letter-spacing: -.025em; }
2003
1899
  .task-journey > ol { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(5,minmax(0,1fr)); gap: 0; margin: 1rem 0 .75rem; padding: 0; list-style: none; }
2004
1900
  .task-journey > ol::before { content: ""; position: absolute; top: .7rem; left: 10%; right: 10%; height: 1px; background: var(--border); }
@@ -2026,8 +1922,6 @@
2026
1922
  .task-control-copy { min-width: 0; overflow-wrap: anywhere; }
2027
1923
  [data-primary-action] { max-width: 100%; }
2028
1924
 
2029
- .task-live-build { display: flex; align-items: center; flex-wrap: wrap; gap: .3rem; margin: .75rem 0 0; font-size: .72rem; }
2030
- .task-live-build .live-dot { width: .45rem; height: .45rem; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 4px color-mix(in srgb,var(--success) 10%,transparent); }
2031
1925
  .chat-action-card { padding: 0; overflow: hidden; }
2032
1926
  .chat-action-card:not(details) { padding: 1rem 1.05rem; }
2033
1927
  .chat-action-card > summary { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.05rem; cursor: pointer; list-style: none; }
@@ -2036,9 +1930,6 @@
2036
1930
  .chat-action-card > summary strong { font-size: .92rem; }
2037
1931
  .chat-action-card > summary small { color: var(--muted-foreground); font-size: .68rem; font-weight: 400; }
2038
1932
  .chat-action-card[open] > summary { border-bottom: 1px solid var(--border); }
2039
- .chat-run-details { padding: .65rem .75rem; border: 1px solid var(--border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb,var(--muted) 45%,transparent); }
2040
- .chat-run-details > summary { color: var(--muted-foreground); cursor: pointer; font-size: .68rem; }
2041
- .chat-run-details > .meta { margin-bottom: 0; }
2042
1933
  .chat-decisions { display: grid; gap: .7rem; }
2043
1934
  .chat-section-head { margin: .15rem .15rem 0; }
2044
1935
  .chat-section-head h2 { margin: .2rem 0 0; font-size: 1rem; }
@@ -2484,9 +2375,6 @@
2484
2375
  .workspace-bar .seg.building { background: var(--running); }
2485
2376
  .workspace-bar .seg.waiting { background: var(--muted-foreground); }
2486
2377
  .workspace-bar .seg.queued { background: color-mix(in srgb, var(--muted-foreground) 45%, transparent); }
2487
- .attention-stack { display: grid; gap: .55rem; }
2488
- .attention-stack .decide-card { margin: 0; }
2489
- .attention-stack .q { display: flex; gap: .5rem; align-items: center; justify-content: space-between; }
2490
2378
  .wb-rail-head {
2491
2379
  display: flex; align-items: flex-end; justify-content: space-between; gap: .75rem;
2492
2380
  padding: 0 .15rem .6rem; border-bottom: 1px solid var(--border);
@@ -2512,13 +2400,11 @@
2512
2400
  @media (max-width: 720px) {
2513
2401
  .control-room-head { display: block; }
2514
2402
  .control-room-head .actions { justify-content: flex-start; margin-top: .75rem; }
2515
- .command-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2516
2403
  .workspace-pulse { grid-template-columns: 1fr; }
2517
2404
  .workspace-stats .pulse-stat { padding: .375rem .125rem; font-size: .625rem; letter-spacing: -.01em; }
2518
2405
  .permission-toggle { grid-template-columns: 1fr; }
2519
2406
  .task-intake { margin-top: .25rem; }
2520
2407
  .task-intake-hero { text-align: left; margin-bottom: .9rem; }
2521
- .task-intake-mark { display: none; }
2522
2408
  .task-composer { margin-inline: 0; padding: .55rem; border-radius: 1.15rem; }
2523
2409
  .task-prompt textarea { min-height: 7.25rem; padding: .75rem; font-size: 1rem; }
2524
2410
  .task-composer-footer { flex-wrap: wrap; }
@@ -2534,8 +2420,6 @@
2534
2420
  .agents-roles { grid-template-columns: 1fr; }
2535
2421
  .agents-role-row { grid-template-columns: 1fr; }
2536
2422
  .approval-card { padding: 1rem; }
2537
- /* The orientation block says the wait on a phone; the kicker would say it twice. */
2538
- .approval-card .approval-kicker { display: none; }
2539
2423
  .approval-boundaries, .approval-confirm { grid-template-columns: 1fr; }
2540
2424
  /* The approval stays IN FLOW on phones: the password field comes
2541
2425
  first and the button follows it — never a sticky control floating
@@ -2570,20 +2454,10 @@
2570
2454
  .queue-card p.row { padding: 0; border-bottom: none; }
2571
2455
  .tracks { margin-top: 1.5rem; }
2572
2456
  .tracks > .hint { margin-bottom: .75rem; }
2573
- .track-row { margin-bottom: .6rem; }
2574
- .track-row p { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
2575
- .track-row p .right { margin-left: auto; }
2576
- .track-row a { text-decoration: none; }
2577
- @media (hover: hover) and (pointer: fine) { .track-row a:hover { text-decoration: underline; } }
2578
- .track-strip { display: inline-flex; gap: .3rem; align-items: center; vertical-align: middle; }
2579
2457
  .fire {
2580
2458
  display: inline-block; width: .65rem; height: .65rem; border-radius: 50%;
2581
2459
  background: var(--muted);
2582
2460
  }
2583
- .fire-ok { background: var(--success); }
2584
- .fire-bad { background: var(--destructive); }
2585
- .fire-live { background: var(--running); animation: pulse 1.6s ease-in-out infinite; }
2586
- .fire-skip { background: transparent; border: 1.5px solid var(--muted); }
2587
2461
 
2588
2462
  /* the phone: fingers, not cursors — tested at 320/390px */
2589
2463
  input, select, textarea { font-size: 16px; }
@@ -2646,8 +2520,6 @@ button { min-height: 44px; }
2646
2520
  .cockpit-disclosure { padding: 0; }
2647
2521
  .cockpit-disclosure > summary { padding: .8rem; }
2648
2522
  .cockpit-disclosure-body { padding: 0 .8rem .85rem; }
2649
- .cockpit-accept-form { display: grid; }
2650
- .cockpit-accept-form button { width: 100%; }
2651
2523
  .diff-review { margin-inline: -.1rem; }
2652
2524
  .diff-review-bar { padding-left: .7rem; }
2653
2525
  .diff-file > summary { padding-inline: .7rem; }
@@ -26,7 +26,7 @@ import { keyStatus } from "../keys.js";
26
26
  import { kitPageHtml, kitsGalleryHtml } from "../kits-ui.js";
27
27
  import { addKitGithubTrigger, addKitSample, kitOf, setUpKit } from "../kits.js";
28
28
  import { connectionsOf } from "../mcp-connect.js";
29
- import { authorizedProject, projectName, rowVisible } from "../project.js";
29
+ import { authorizedProject, projectName, } from "../project.js";
30
30
  import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm, recipeLibraryHtml, recipeRunHtml, recipeScript, workflowPreviewHtml } from "../recipe-ui.js";
31
31
  import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWorkflow, prepareRecipeRun, RecipeError, savedRecipes, saveWorkflowRecipe, starterRecipes, workflowPreview } from "../recipes.js";
32
32
  import { isAlive as runnerAlive } from "../runner.js";
@@ -43,7 +43,7 @@ import { redirect, respond, SAFETY, taskHref } from "./http.js";
43
43
  import { html } from "../html.js";
44
44
  import {} from "./session.js";
45
45
  export function createFlowsHandlers(runtime) {
46
- const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, flowRooms, identify, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, authenticateCredential, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
46
+ const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, authenticateCredential, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
47
47
  // ---- reads ---------------------------------------------------------------
48
48
  /** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
49
49
  function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
@@ -62,6 +62,8 @@ export type ServeOptions = {
62
62
  leadByDefault?: boolean;
63
63
  /** Test seam: the operating system the sign-in command is written for. */
64
64
  platform?: NodeJS.Platform;
65
+ /** Test seam: whether this machine has an OS fence (macOS sandbox or bubblewrap) for checks. */
66
+ fenceAvailable?: () => boolean;
65
67
  /** Extra Host values this server answers as (a Tailscale name, a LAN ip:port). */
66
68
  allowedHosts?: readonly string[];
67
69
  /**
@@ -245,6 +247,8 @@ export declare function goOutside(response: ServerResponse, to: string, words: s
245
247
  path: string;
246
248
  secure: boolean;
247
249
  }): void;
250
+ /** A form's answer: back to `page` (which may carry its own query) saying `said` or `problem` words, then an anchor. */
251
+ export declare function sayTo(response: ServerResponse, page: string, anchor?: string): (key: "said" | "problem", words: string, at?: string) => void;
248
252
  export declare function redirect(response: ServerResponse, to: string): void;
249
253
  export declare const taskChatHref: (taskId: string) => string;
250
254
  /** A project's own lead thread (v77). */
@@ -115,6 +115,11 @@ export function goOutside(response, to, words, bind) {
115
115
  "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" });
116
116
  response.end(htmlString(html `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${to}"><title>Toolroll</title>${HANDOFF_STYLE}<p>${words} <a href="${to}">Continue</a></p>`));
117
117
  }
118
+ /** A form's answer: back to `page` (which may carry its own query) saying `said` or `problem` words, then an anchor. */
119
+ export function sayTo(response, page, anchor = "") {
120
+ const join = page.includes("?") ? "&" : "?";
121
+ return (key, words, at = anchor) => redirect(response, `${page}${join}${key}=${encodeURIComponent(words)}${at}`);
122
+ }
118
123
  export function redirect(response, to) {
119
124
  // Set as a header (not only in writeHead) so the answer can be read back after: a refused act's redirect names it.
120
125
  response.setHeader("Location", to);
@@ -38,7 +38,7 @@ import { healthWords } from "../server-telemetry.js";
38
38
  import { tally } from "../summary.js";
39
39
  import { serveInstallAsset } from "./pages-assets.js";
40
40
  import { healthz } from "./remote-hooks.js";
41
- import { projectChip, refuse, regionScript, relativeAge, screen, settingsRows, transcriptScript, WORKSPACE_STYLE, workToolRows } from "./chrome.js";
41
+ import { refuse, regionScript, screen, settingsRows, WORKSPACE_STYLE, workToolRows } from "./chrome.js";
42
42
  import { redirect, respond, safeReturn, SAFETY, taskHref } from "./http.js";
43
43
  import { assignmentOf } from "../assignment.js";
44
44
  import { assignmentTaskStatusOf } from "../assignment-presentation.js";
@@ -500,7 +500,7 @@ export function createPagesHandlers(runtime) {
500
500
  return sendScreen(response, 200, screen("Agents", fallback, { chrome: chromeFor(project, "runs"), workspace: { view } }));
501
501
  }
502
502
  async function fleetPage(ctx) {
503
- const { url, who, response, project } = ctx;
503
+ const { url, response, project } = ctx;
504
504
  // Cross-project by design: which agent is on which project is the
505
505
  // question, so this screen is a survey — reads are still filtered
506
506
  // through the ceiling before a card renders. No project needed.
@@ -9,9 +9,8 @@ import { projectName } from "../project.js";
9
9
  import { requestLimitsHtml, tokenLimitWords } from "../request-budget-ui.js";
10
10
  import { authenticateAccount } from "../scope.js";
11
11
  import { SSO_CALLBACK } from "../sso-settings.js";
12
- import { requestContext } from "./request-context.js";
13
12
  import { personChip, refuse, screen } from "./chrome.js";
14
- import { redirect, respond } from "./http.js";
13
+ import { redirect, respond, sayTo } from "./http.js";
15
14
  /** people handlers, moved without changing their route bodies. */
16
15
  import { randomBytes, timingSafeEqual } from "node:crypto";
17
16
  import {} from "node:http";
@@ -40,7 +39,7 @@ export function sameVisitCheck(authTime, startedAt, now) {
40
39
  return Math.min(Math.max(at, startedAt), now);
41
40
  }
42
41
  export function createPeopleHandlers(runtime) {
43
- const { settingsUpdates, joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
42
+ const { joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, defaultProject, cookieSecure, consoleOrigin, providerFor, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
44
43
  // v101: Settings → Sessions & tokens: where you're signed in, and your API tokens. An instance operator can see everyone's.
45
44
  async function sessionsPage(ctx) {
46
45
  const { url, who, response, project } = ctx;
@@ -167,7 +166,7 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
167
166
  if (who.via !== "cookie")
168
167
  return refuse(response, who, 403, "Sign in in a browser to manage sessions and tokens.", "/settings");
169
168
  const operator = store.isInstanceOperator(who.name);
170
- const back = (key, words) => redirect(response, `/settings/sessions?${body.get("everyone") === "1" && operator ? "everyone=1&" : ""}${key}=${encodeURIComponent(words)}`);
169
+ const back = sayTo(response, `/settings/sessions${body.get("everyone") === "1" && operator ? "?everyone=1" : ""}`);
171
170
  const action = body.get("action") ?? "";
172
171
  if (action === "end-session") {
173
172
  const target = store.webSessions(null).find(one => one.idHash === (body.get("session") ?? ""));