toolroll 0.9.49 → 0.9.50
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.
- package/CHANGELOG.md +8 -0
- package/dist/cli-http.js +3 -0
- package/dist/flow-triggers.js +7 -0
- package/dist/mcp-http.js +6 -11
- package/dist/mcp-oauth.js +5 -21
- package/dist/mcp-person.d.ts +0 -9
- package/dist/mcp-person.js +2 -1
- package/dist/operate.js +4 -1
- package/dist/serve.d.ts +2 -605
- package/dist/serve.js +423 -23783
- package/dist/server/chat.d.ts +41 -0
- package/dist/server/chat.js +1458 -0
- package/dist/server/edge-refusal.d.ts +5 -0
- package/dist/server/edge-refusal.js +56 -0
- package/dist/server/flows.d.ts +12 -0
- package/dist/server/flows.js +832 -0
- package/dist/server/guards.d.ts +72 -0
- package/dist/server/guards.js +357 -0
- package/dist/server/handler-context.d.ts +22 -0
- package/dist/server/handler-context.js +1 -0
- package/dist/server/handler-registry.d.ts +21 -0
- package/dist/server/handler-registry.js +34 -0
- package/dist/server/pages-assets.d.ts +10 -0
- package/dist/server/pages-assets.js +103 -0
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +910 -0
- package/dist/server/people-tokens.d.ts +20 -0
- package/dist/server/people-tokens.js +694 -0
- package/dist/server/remote-hooks.d.ts +29 -0
- package/dist/server/remote-hooks.js +130 -0
- package/dist/server/remote.d.ts +59 -0
- package/dist/server/remote.js +185 -0
- package/dist/server/route-policy.d.ts +18 -0
- package/dist/server/route-policy.js +30 -0
- package/dist/server/route-table.d.ts +74 -0
- package/dist/server/route-table.js +350 -0
- package/dist/server/runtime.d.ts +280 -0
- package/dist/server/runtime.js +25 -0
- package/dist/server/settings.d.ts +22 -0
- package/dist/server/settings.js +2388 -0
- package/dist/server/shared.d.ts +3334 -0
- package/dist/server/shared.js +12708 -0
- package/dist/server/task-views.d.ts +82 -0
- package/dist/server/task-views.js +1099 -0
- package/dist/server/tasks.d.ts +87 -0
- package/dist/server/tasks.js +3579 -0
- package/dist/session-http.js +3 -0
- package/dist/team-http.d.ts +5 -1
- package/dist/team-http.js +6 -2
- package/dist/teams.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3334 @@
|
|
|
1
|
+
/** Shared console rendering, types and request guards, moved from serve.ts. */
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
4
|
+
import type { RunActivity } from "../activity-line.js";
|
|
5
|
+
import { type AgentChoice } from "../agentconfig.js";
|
|
6
|
+
import { type AssignmentSnapshot } from '../assignment.js';
|
|
7
|
+
import type { BoardCard } from "../board.js";
|
|
8
|
+
import type { BrandIconId } from "../brand-icons.js";
|
|
9
|
+
import { type BrowserActionCard, type BrowserCheckItem, type BrowserLimits, type BrowserNeedAction, type BrowserPhoneCard, type BrowserResultPanel, type BrowserResultView, type BrowserSettingsGroup, type BrowserSettingsView, type BrowserSignIn, type BrowserTaskView, type BrowserUpdateNotice, type BrowserUpdates, type BrowserWorkspace } from '../browser-workspace.js';
|
|
10
|
+
import { type CheckLevel } from '../check-levels.js';
|
|
11
|
+
import { type CliHttpOptions, type RunOperateAs } from '../cli-http.js';
|
|
12
|
+
import { CodingWorkspace } from '../coding-workspace.js';
|
|
13
|
+
import type { AgentView } from "../contest.js";
|
|
14
|
+
import { type FormView } from "../contracts/console-api.js";
|
|
15
|
+
import { type ChatDraft } from "../converse.js";
|
|
16
|
+
import { type DispatchDiagnosis } from "../dispatch.js";
|
|
17
|
+
import { type ReportShot, type ReportView } from "../evidence.js";
|
|
18
|
+
import { run as execRun } from "../exec.js";
|
|
19
|
+
import { type FirstRunStep } from "../first-run.js";
|
|
20
|
+
import { type MailSender } from "../flow-actions.js";
|
|
21
|
+
import { type FetchLike } from "../flow-share.js";
|
|
22
|
+
import { type TriggerIo } from "../flow-triggers.js";
|
|
23
|
+
import { type Gap } from "../gaps.js";
|
|
24
|
+
import { type InstallMethod } from "../install-method.js";
|
|
25
|
+
import { type IntegrationIo } from "../integrations.js";
|
|
26
|
+
import { type LeadIdentity } from "../lead-identity.js";
|
|
27
|
+
import type { MateLiveStep } from "../mate-progress.js";
|
|
28
|
+
import { type VersionRunner } from "../model-catalog.js";
|
|
29
|
+
import { type FailureExplanation } from '../needs-you.js';
|
|
30
|
+
import { cloneGithubRepo, listGithubRepos, previewGithubRepo, type ListOutcome } from "../onboard.js";
|
|
31
|
+
import { type Principal } from "../operate-remote.js";
|
|
32
|
+
import { parseBaseTreeSnapshot } from "../peek.js";
|
|
33
|
+
import { type RiskLevel, type RouteOverride, type RouteProjection, type RouteStamp } from "../phase-routing.js";
|
|
34
|
+
import { type MilestoneState } from "../plan.js";
|
|
35
|
+
import { type ContractChange } from "../planner-source.js";
|
|
36
|
+
import { type CriterionMatrixRow, type ProofVerdict } from "../proof.js";
|
|
37
|
+
import { type ProviderConnection } from "../provider-connection.js";
|
|
38
|
+
import { type Phase } from "../provider.js";
|
|
39
|
+
import type { PublishExec } from '../publish.js';
|
|
40
|
+
import { type PullRequestView } from '../pull-request-flow.js';
|
|
41
|
+
import { type QualityMode } from "../quality.js";
|
|
42
|
+
import { type StructuredHandoffView, type TerminalDiffView } from "../result-evidence-readers.js";
|
|
43
|
+
import { type FollowUpCheck } from '../result-follow-ups.js';
|
|
44
|
+
import { type ResultScreenshot, type ResultTab, type SharedResultFacts } from "../result-review.js";
|
|
45
|
+
import { type BuildReviewView } from "../review-switch.js";
|
|
46
|
+
import type { Runner } from "../runner.js";
|
|
47
|
+
import { approvalOf, type AcceptanceCriterion, type Scope, type UnattendedPermissionMode } from "../scope.js";
|
|
48
|
+
import { type ReportItem } from "../scout-report.js";
|
|
49
|
+
import type { ChatConfig, ChatProviderId, ChatSnapshot, ChatTurn, Contest, CoordinatorProposal, DirectChatProviderId, MateMessage, MateProposal, MateSession, MateTurn, PlanRevisionKind, PlanRevisionStatus, PublicationGrant, PushSubscription, RepairChainRow, ReviewRetryState, RevisionLineage, Routine, SteerNote, SubscriptionChatProviderId, TaskFamily, TournamentTerms } from "../store.js";
|
|
50
|
+
import { type Artifact, type Capability, type CheckProgress, type Decision, type DiffComment, type ExternalMirror, type Hold, type Incident, type MateAsk, type Publication, type ResultScreenshots, type Run, type SessionTurn, type Store, type Task, type TaskState, type WorktreeRow } from "../store.js";
|
|
51
|
+
import type { SubscriptionMateRunner } from "../subscription-chat.js";
|
|
52
|
+
import { tally } from "../summary.js";
|
|
53
|
+
import { type TaskControlView } from "../task-control.js";
|
|
54
|
+
import { type PullRequestFact, type TaskStatus } from '../task-status.js';
|
|
55
|
+
import type { TeamChatProviderResolver } from '../team-chat-authorization.js';
|
|
56
|
+
import type { TeamSnapshot } from '../team-contract.js';
|
|
57
|
+
import { type TokenSource } from "../telegram.js";
|
|
58
|
+
import { launchRuntimeUpdate } from "../toolroll-update.js";
|
|
59
|
+
import { workCountsByProject, type WorkIndexGroup, type WorkIndexPage } from "../work-index.js";
|
|
60
|
+
import { type DisplayStatus, type PublicationFacts, type ReviewFacts, type WorkFacts, type WorkStatus, type WorkView } from "../workspace-ui.js";
|
|
61
|
+
/** A user agent, reduced to safe display words — never echoed raw. */
|
|
62
|
+
export declare function oneLineUa(raw: string | string[] | undefined): string;
|
|
63
|
+
export type ServeOptions = {
|
|
64
|
+
/** Tests: runs a person's command for the HTTP MCP gateway instead of operate.ts's. */
|
|
65
|
+
runOperateAs?: import("../mcp-person.js").RunOperateAs;
|
|
66
|
+
/** `toolroll demo`: the scripted lead that answers Chat instead of a model. */
|
|
67
|
+
demoLead?: import("../demo.js").DemoLead;
|
|
68
|
+
/** Tests: the bin whose real path says how Toolroll was installed (Settings → Updates' command). */
|
|
69
|
+
installBin?: string;
|
|
70
|
+
/** Native coding workspace injection for isolated integration tests. */
|
|
71
|
+
codingWorkspace?: CodingWorkspace;
|
|
72
|
+
/** Read-only worker connections for heavy reads (read-executor.ts); 0 reads in-process. Default: off under tests. */
|
|
73
|
+
readWorkers?: number;
|
|
74
|
+
store: Store;
|
|
75
|
+
evidenceRoot: string;
|
|
76
|
+
clock?: () => Date;
|
|
77
|
+
/**
|
|
78
|
+
* The console's canonical https origin, when TLS terminates in front
|
|
79
|
+
* (arc 3 finding 2/16): EXACTLY an origin — no path, query, credentials.
|
|
80
|
+
* The one trust anchor for secure-context features: it joins the allowed
|
|
81
|
+
* hosts, its origin authorizes POSTs, cookies turn Secure, and the
|
|
82
|
+
* install/push cards light up. X-Forwarded-* is consulted only from a
|
|
83
|
+
* loopback peer, the same-host proxy (public-access.ts).
|
|
84
|
+
*/
|
|
85
|
+
publicUrl?: string;
|
|
86
|
+
/** Tests: the shared command boundary `POST /api/cli` runs (default: operate.ts's runOperateAs). */
|
|
87
|
+
cliRunner?: RunOperateAs;
|
|
88
|
+
/** Tests: remote command metadata until the shared contract declares it. */
|
|
89
|
+
cliModeOf?: CliHttpOptions['modeOf'];
|
|
90
|
+
/** Tests: the clock request budgets for API tokens count by (request-budget.ts). */
|
|
91
|
+
requestBudgetClock?: () => number;
|
|
92
|
+
/** Where repos.json lives — every enrollment locks exactly this file. */
|
|
93
|
+
registryPath?: string;
|
|
94
|
+
/** This console fronts an `up` process: onboarding copy says how to watch. */
|
|
95
|
+
upConsole?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* The lead on by default (onboarding): with no lead set up, the agent CLI
|
|
98
|
+
* signed in on this computer runs it, once. `up` and `serve` turn this on;
|
|
99
|
+
* tests opt in.
|
|
100
|
+
*/
|
|
101
|
+
leadByDefault?: boolean;
|
|
102
|
+
/** Test seam: the operating system the sign-in command is written for. */
|
|
103
|
+
platform?: NodeJS.Platform;
|
|
104
|
+
/** Extra Host values this server answers as (a Tailscale name, a LAN ip:port). */
|
|
105
|
+
allowedHosts?: readonly string[];
|
|
106
|
+
/**
|
|
107
|
+
* This computer's names on its tailnet (onboarding): read at start and every
|
|
108
|
+
* few minutes; the console answers to each on its own port without an
|
|
109
|
+
* --allow-host, and the phone card names the address. Absent = none.
|
|
110
|
+
*/
|
|
111
|
+
tailnetNames?: () => Promise<readonly string[]>;
|
|
112
|
+
/**
|
|
113
|
+
* The first-account road (setup review): while NO approver exists, the
|
|
114
|
+
* login page offers "create the first account", gated by this code —
|
|
115
|
+
* printed once by the process that started the server, never stored.
|
|
116
|
+
* Five wrong codes close the road until the server restarts. The moment
|
|
117
|
+
* an approver exists, the page is the ordinary sign-in.
|
|
118
|
+
*/
|
|
119
|
+
setupCode?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Where the Telegram bot token lives when set from here. Present = the
|
|
122
|
+
* settings card renders; absent = no settings surface at all.
|
|
123
|
+
*/
|
|
124
|
+
telegramTokenFile?: string;
|
|
125
|
+
/** Where messaging config files live (beside the database) — enables the
|
|
126
|
+
* primary-messenger selector on the settings screen. */
|
|
127
|
+
configDir?: string;
|
|
128
|
+
/** Test seam for the Slack setup handshake. */
|
|
129
|
+
slackFetcher?: typeof fetch;
|
|
130
|
+
/** Settings → Updates seams: the latest release, how this Toolroll was
|
|
131
|
+
* installed, its version, and how the updater job starts. Tests and
|
|
132
|
+
* screenshots inject these; production reads the registry and launchd. */
|
|
133
|
+
updates?: {
|
|
134
|
+
latest?: () => Promise<{
|
|
135
|
+
version: string;
|
|
136
|
+
}>;
|
|
137
|
+
method?: InstallMethod;
|
|
138
|
+
current?: string;
|
|
139
|
+
dist?: string;
|
|
140
|
+
launch?: typeof launchRuntimeUpdate;
|
|
141
|
+
};
|
|
142
|
+
discordFetcher?: typeof fetch;
|
|
143
|
+
/** Injected by tests: Microsoft sign-in, key metadata and Teams conversation calls. */
|
|
144
|
+
teamsFetcher?: typeof fetch;
|
|
145
|
+
/** Injected by tests: how "Check now" reaches GitHub (gh) and Linear (fetch). */
|
|
146
|
+
flowTriggerIo?: Partial<TriggerIo>;
|
|
147
|
+
/**
|
|
148
|
+
* The repo this console serves. Scopes run evidence to that repo's tasks
|
|
149
|
+
* (and unplaced ones) and turns on the gaps and capabilities views —
|
|
150
|
+
* without it those pages say so instead of guessing.
|
|
151
|
+
*/
|
|
152
|
+
repo?: string;
|
|
153
|
+
/**
|
|
154
|
+
* The full authorization ceiling (v2 review, finding 1): `repos` this
|
|
155
|
+
* server may show and operate on, plus `projectRoots` under which any git
|
|
156
|
+
* repository qualifies. `repo` above is sugar for one entry in `repos`.
|
|
157
|
+
* No configuration at all is the legacy unscoped mode — everything
|
|
158
|
+
* visible, and stated as such where the code decides.
|
|
159
|
+
*/
|
|
160
|
+
repos?: readonly string[];
|
|
161
|
+
projectRoots?: readonly string[];
|
|
162
|
+
/** Repositories the co-located `up` process has proved and is watching.
|
|
163
|
+
* Kept as a callback so projects added after startup appear immediately
|
|
164
|
+
* without turning the durable registry itself into an authorization source. */
|
|
165
|
+
currentRepos?: () => readonly string[];
|
|
166
|
+
/** Injected by tests: the fetch direct-API chat turns use, and where chat
|
|
167
|
+
* keys are read from (defaults to process.env). */
|
|
168
|
+
chatFetcher?: typeof fetch;
|
|
169
|
+
/** Subscription-backed mate transport; injected in tests so no real
|
|
170
|
+
* Codex or Claude membership turn is consumed. */
|
|
171
|
+
subscriptionChatRunner?: SubscriptionMateRunner;
|
|
172
|
+
/** Tests: stands in for `codex mcp list --json` in the project (the Tools page's "Found on this computer"). */
|
|
173
|
+
codexToolList?: (cwd: string) => Promise<string | null>;
|
|
174
|
+
/** Tests: the home whose ~/.toolroll (or older ~/.standing-orders) tool-secrets and ~/.claude.json the Tools page uses. */
|
|
175
|
+
toolHome?: string;
|
|
176
|
+
/** v87: sends Send email steps' mail and the settings test (tests inject one). */
|
|
177
|
+
mailSender?: MailSender;
|
|
178
|
+
/** Injected by tests: how Flows → Import fetches a flow file's address. */
|
|
179
|
+
flowFetch?: FetchLike;
|
|
180
|
+
/** v89: Google's token endpoint (tests inject a scripted one). */
|
|
181
|
+
googleFetch?: typeof fetch;
|
|
182
|
+
/** Tests: every request a one-click connection makes (discovery, registration, tokens). */
|
|
183
|
+
connectFetch?: typeof fetch;
|
|
184
|
+
/** Tests: every request sign-in with the identity provider makes (discovery, keys, tokens). */
|
|
185
|
+
ssoFetch?: typeof fetch;
|
|
186
|
+
chatEnv?: Record<string, string | undefined>;
|
|
187
|
+
/**
|
|
188
|
+
* The live peek's locality ASSERTION (live-peek v3 §3): the administrator
|
|
189
|
+
* who starts serve names the runner this machine owns. This is documented
|
|
190
|
+
* as an assertion, not machine-bound credential enforcement — the product
|
|
191
|
+
* has none anywhere. Absent = the peek is off, and says so.
|
|
192
|
+
*/
|
|
193
|
+
desktopIdentity?: string;
|
|
194
|
+
/** Trusted native admission callback; enrollment rows alone never supply this authority. */
|
|
195
|
+
additionalProjectRepos?: () => readonly string[];
|
|
196
|
+
/** The co-located `up`'s exact admitted repositories, read on every request: its startup projects plus those
|
|
197
|
+
* added since (`repos add`, the lead, the console) and minus those removed. When given it replaces `repos` as
|
|
198
|
+
* the exact part of the ceiling, so an addition or removal shows without a restart. */
|
|
199
|
+
admittedRepos?: () => readonly string[];
|
|
200
|
+
connectionProbe?: typeof execRun;
|
|
201
|
+
connectionHome?: string;
|
|
202
|
+
/** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
|
|
203
|
+
firstTaskRunner?: typeof execRun;
|
|
204
|
+
/** Test seam for Settings → Integrations: how its checks reach services (fetch, gh, mail servers). */
|
|
205
|
+
integrationIo?: Partial<IntegrationIo>;
|
|
206
|
+
modelCatalogFetcher?: typeof fetch;
|
|
207
|
+
/** Test seams for Settings → Models: CLI version probes and the PATH they search. */
|
|
208
|
+
modelRunner?: VersionRunner;
|
|
209
|
+
modelPath?: string;
|
|
210
|
+
localRunner?: string;
|
|
211
|
+
/** The checkout pool root the peek confines itself to (realpath-proved). */
|
|
212
|
+
poolRoot?: string;
|
|
213
|
+
/**
|
|
214
|
+
* Editor deep links (arc 6): a DEPLOYMENT capability, not an activation.
|
|
215
|
+
* vscode:// links open on the BROWSER's machine, so links render only
|
|
216
|
+
* when three statements align: the operator started serve with
|
|
217
|
+
* --editor vscode AND --runner (this machine owns the worktrees), the
|
|
218
|
+
* run belongs to that runner, and THIS session turned links on for
|
|
219
|
+
* this device. "vscode" is the only value; the scheme is never data.
|
|
220
|
+
*/
|
|
221
|
+
editorLinks?: "vscode";
|
|
222
|
+
/**
|
|
223
|
+
* The attended-mint capability (Phase 2E): present only on a co-located
|
|
224
|
+
* `up` console, which alone can hold a session. `headOf` reads the
|
|
225
|
+
* repository's CURRENT head — the exact commit the signed terms pin.
|
|
226
|
+
*/
|
|
227
|
+
attended?: {
|
|
228
|
+
runner: string;
|
|
229
|
+
headOf: (repo: string) => Promise<string | null>;
|
|
230
|
+
coordinator?: import("../held.js").HeldSessionCoordinator;
|
|
231
|
+
};
|
|
232
|
+
/** Injected by tests: the onboarding ceremony's gh-facing halves — the
|
|
233
|
+
* ceremony's gating, nonce, and enrollment logic is what the HTTP tests
|
|
234
|
+
* prove; gh itself is proved by onboard.test.ts. */
|
|
235
|
+
ghPreview?: typeof previewGithubRepo;
|
|
236
|
+
ghClone?: typeof cloneGithubRepo;
|
|
237
|
+
ghList?: typeof listGithubRepos;
|
|
238
|
+
/** Injected by tests: the git and gh calls pull-request setup and Merge make. */
|
|
239
|
+
publishExec?: PublishExec;
|
|
240
|
+
};
|
|
241
|
+
export declare const SESSION_COOKIE = "standing-orders_session";
|
|
242
|
+
/** Stands in for "no project" in a ceiling: no folder resolves to it, so it admits nothing. */
|
|
243
|
+
export declare const NO_PROJECT = "/\0no-project";
|
|
244
|
+
/** Where `up`'s one-time sign-in link points, and how long it works. */
|
|
245
|
+
export declare const SIGN_IN_LINK_PATH = "/login/once/";
|
|
246
|
+
export declare const SIGN_IN_LINK_MS: number;
|
|
247
|
+
/** The installation fact that the lead was turned on by default, once (its value: the provider). */
|
|
248
|
+
export declare const LEAD_BY_DEFAULT_FACT = "lead-on-by-default";
|
|
249
|
+
/** The installation fact that the phone card was put away. */
|
|
250
|
+
export declare const PHONE_CARD_FACT = "phone-card-dismissed";
|
|
251
|
+
export declare const BODY_CAP: number;
|
|
252
|
+
export declare const TASK_FORM_BODY_CAP: number;
|
|
253
|
+
/** A cookie idles out after half a day and dies outright after a week. */
|
|
254
|
+
export declare const SESSION_IDLE_MS: number;
|
|
255
|
+
export declare const SESSION_ABSOLUTE_MS: number;
|
|
256
|
+
/** An approval nonce is a rendered form, not a standing right — it ages out fast. */
|
|
257
|
+
export declare const NONCE_TTL_MS: number;
|
|
258
|
+
export declare const NONCE_CAP = 500;
|
|
259
|
+
export declare const RUNS_PAGE = 50;
|
|
260
|
+
export declare const TASK_STATES: readonly TaskState[];
|
|
261
|
+
/** Read-only fragment polls that must never refresh session activity (arc 1). */
|
|
262
|
+
export declare const NO_TOUCH_FRAGMENTS: ReadonlySet<string>;
|
|
263
|
+
export type Session = {
|
|
264
|
+
name: string;
|
|
265
|
+
csrf: string;
|
|
266
|
+
/** v29: the account's standing at login — the central gate reads it;
|
|
267
|
+
* the revocation cascade kills the session outright. */
|
|
268
|
+
role: "approver" | "viewer";
|
|
269
|
+
/** The approver generation at login: credential rotation kills the cookie. */
|
|
270
|
+
generation: number;
|
|
271
|
+
createdAt: number;
|
|
272
|
+
lastSeen: number;
|
|
273
|
+
/** The open project — a VIEW FILTER chosen inside the ceiling, never authorization. */
|
|
274
|
+
project: string | null;
|
|
275
|
+
/** Bumped on every open: stale tabs carry the revision they were rendered under. */
|
|
276
|
+
projectRevision: number;
|
|
277
|
+
/** v100: signed in with the identity provider, and when it last checked them (a step-up within 10 minutes needs no password). */
|
|
278
|
+
sso?: {
|
|
279
|
+
at: number;
|
|
280
|
+
};
|
|
281
|
+
/** v101: the browser and address it signed in from, for the person to recognise it. */
|
|
282
|
+
agent?: string | null;
|
|
283
|
+
address?: string | null;
|
|
284
|
+
/** Onboarding preview records (arc: repo onboarding, finding 14) —
|
|
285
|
+
* session-held, swept at mint, at most 3, consumed exactly once. */
|
|
286
|
+
onboard?: Map<string, {
|
|
287
|
+
nameWithOwner: string;
|
|
288
|
+
rootIndex: number;
|
|
289
|
+
target: string;
|
|
290
|
+
diskUsageKib: number | null;
|
|
291
|
+
large: boolean;
|
|
292
|
+
mintedAt: number;
|
|
293
|
+
}>;
|
|
294
|
+
/** When this session last READ the board — the anchor for "since you
|
|
295
|
+
* last looked". Full page loads move it; fragment polls never do. */
|
|
296
|
+
sawBoardAt: number | null;
|
|
297
|
+
/** Fleet chat (v13): drafts and the last reply live HERE and nowhere
|
|
298
|
+
* durable — restart or logout loses them by design (v2 finding 12). */
|
|
299
|
+
chat?: SessionChat;
|
|
300
|
+
/** Editor links (arc 6): the SESSION's half of the activation — "this
|
|
301
|
+
* browser runs on the machine that holds the worktrees" is a statement
|
|
302
|
+
* only the person at the browser can make. Dies with the session. */
|
|
303
|
+
editorLinks?: boolean;
|
|
304
|
+
};
|
|
305
|
+
export type ChatCandidate = {
|
|
306
|
+
key: string;
|
|
307
|
+
draft: ChatDraft;
|
|
308
|
+
/** Resolved server-side at parse time from the opaque repoId. */
|
|
309
|
+
repoPath: string;
|
|
310
|
+
provider: string;
|
|
311
|
+
approver: string;
|
|
312
|
+
/** Digest of the frozen explicit repo list at turn time — filing
|
|
313
|
+
* re-proves it (v2 new finding 5). */
|
|
314
|
+
ceilingDigest: string;
|
|
315
|
+
createdAt: number;
|
|
316
|
+
state: "pending" | "filing";
|
|
317
|
+
};
|
|
318
|
+
export type SessionChat = {
|
|
319
|
+
candidates: Map<string, ChatCandidate>;
|
|
320
|
+
lastTurn: {
|
|
321
|
+
id: number;
|
|
322
|
+
reply: string | null;
|
|
323
|
+
staticError: string | null;
|
|
324
|
+
proposalsDiscarded: boolean;
|
|
325
|
+
} | null;
|
|
326
|
+
};
|
|
327
|
+
/** One rendered approval form: who saw which digest of which task, once. */
|
|
328
|
+
export type ApprovalNonce = {
|
|
329
|
+
name: string;
|
|
330
|
+
taskId: string;
|
|
331
|
+
digest: string;
|
|
332
|
+
expiresAt: number;
|
|
333
|
+
};
|
|
334
|
+
export type Who = {
|
|
335
|
+
name: string;
|
|
336
|
+
via: "cookie";
|
|
337
|
+
session: Session;
|
|
338
|
+
role: "approver" | "viewer";
|
|
339
|
+
} | {
|
|
340
|
+
name: string;
|
|
341
|
+
via: "bearer";
|
|
342
|
+
role: "approver" | "viewer"; /** v101: the API token it came with. */
|
|
343
|
+
token?: string;
|
|
344
|
+
/** The complete proved API-token authority, retained across body reads. */ principal?: Principal;
|
|
345
|
+
generation: number;
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* v101: browser sessions in memory and in the database (by a hash of the
|
|
349
|
+
* cookie, never the cookie), so a restart signs no one out and a person can
|
|
350
|
+
* see and end their sessions. The in-memory map is the working set; a cookie
|
|
351
|
+
* it hasn't seen is looked up by its hash.
|
|
352
|
+
*/
|
|
353
|
+
export declare class PersistentSessions extends Map<string, Session> {
|
|
354
|
+
private readonly db;
|
|
355
|
+
private readonly ids;
|
|
356
|
+
private readonly savedAt;
|
|
357
|
+
constructor(db: Store);
|
|
358
|
+
static hash(id: string): string;
|
|
359
|
+
set(id: string, session: Session): this;
|
|
360
|
+
delete(id: string): boolean;
|
|
361
|
+
/** Keep a changed session (its project, its provider check); `seen` only once a minute. */
|
|
362
|
+
persist(session: Session, seen?: boolean): void;
|
|
363
|
+
/** A cookie from before a restart, if its session is still kept. */
|
|
364
|
+
load(id: string): Session | null;
|
|
365
|
+
/** End one session by its kept hash (from the Sessions page). */
|
|
366
|
+
endByHash(idHash: string): void;
|
|
367
|
+
hashOf(session: Session): string | null;
|
|
368
|
+
}
|
|
369
|
+
/** The console's server, and the one-time sign-in link `up` opens: a path on this server, or null for no such approver.
|
|
370
|
+
* closeCoding starts the coding shutdown on its own, ahead of the rest of a stop; close() awaits the same promise. */
|
|
371
|
+
export type DecisionServer = Server & {
|
|
372
|
+
mintSignInLink(account: string): string | null;
|
|
373
|
+
closeCoding(): Promise<void>;
|
|
374
|
+
};
|
|
375
|
+
/** How long a stop waits for teammates and the lead's follow pass before closing anyway. */
|
|
376
|
+
export declare const SHUTDOWN_WAIT_MS = 5000;
|
|
377
|
+
/**
|
|
378
|
+
* Match `/t/<id>` (suffix "") or `/t/<id>/<verb>` — the id percent-decoded
|
|
379
|
+
* exactly once, refused when it does not decode, is oversized, or carries
|
|
380
|
+
* control characters. Legacy CLI-created ids are free-form; the URL is not.
|
|
381
|
+
*/
|
|
382
|
+
export declare function matchTaskPath(pathname: string, suffixPattern: string): {
|
|
383
|
+
taskId: string;
|
|
384
|
+
verb: string;
|
|
385
|
+
} | null;
|
|
386
|
+
export declare function taskHref(taskId: string): string;
|
|
387
|
+
/** The spend line for a 7am reader: whole cents, "runs", the gap still named. */
|
|
388
|
+
export declare function consoleSpend(summary: ReturnType<typeof tally<Run & {
|
|
389
|
+
taskId: string;
|
|
390
|
+
}>>): string;
|
|
391
|
+
/** ISO to the minute — "2026-08-12 17:56" — for anywhere a person reads a time. */
|
|
392
|
+
export declare function when(iso: string | null): string;
|
|
393
|
+
/** The same minute as a `<time>`: the full stamp on a desk, "16:39" / "Yesterday 16:39" / "Sep 28" on a phone. */
|
|
394
|
+
export declare function whenTime(iso: string | null): string;
|
|
395
|
+
/** Overdue is derived at render — display never writes. */
|
|
396
|
+
export declare function isOverdue(decision: Decision, now: Date): boolean;
|
|
397
|
+
export declare const SAFETY: {
|
|
398
|
+
readonly "Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
|
|
399
|
+
readonly "X-Content-Type-Options": "nosniff";
|
|
400
|
+
readonly "Referrer-Policy": "no-referrer";
|
|
401
|
+
readonly "Cache-Control": "no-store";
|
|
402
|
+
};
|
|
403
|
+
export declare function respond(response: ServerResponse, status: number, type: string, body: string): void;
|
|
404
|
+
/**
|
|
405
|
+
* A page response. With a nonce, this response's CSP admits exactly the one
|
|
406
|
+
* inline script the shell stamped with the same value — generated per
|
|
407
|
+
* response, never shared, never 'unsafe-inline' (Codex board review,
|
|
408
|
+
* finding 9). Everything else keeps the constant script-free policy.
|
|
409
|
+
*/
|
|
410
|
+
export declare function page(response: ServerResponse, status: number, html: string, nonce?: string, fetches?: boolean): void;
|
|
411
|
+
/**
|
|
412
|
+
* An outside sign-in comes back only to the browser that started it: the
|
|
413
|
+
* start leaves the visit's state in this cookie (Lax, so it rides the
|
|
414
|
+
* service's redirect back; the session cookie is Strict and doesn't), and a
|
|
415
|
+
* return whose state isn't the one this browser holds is refused. So nobody
|
|
416
|
+
* can start a sign-in and have someone else finish it into their project.
|
|
417
|
+
*/
|
|
418
|
+
/** A moment's page on the way to or back from another site's sign-in: no stylesheet may load there, so the palette rides inline. */
|
|
419
|
+
export declare const HANDOFF_STYLE = "<style>body{font:15px/1.5 \"Geist\",system-ui,sans-serif;margin:2rem;background:#efefef;color:#171717}a{color:#171717;text-decoration-color:#8f8f8f;text-underline-offset:3px}@media(prefers-color-scheme:dark){body{background:#0b0b0b;color:#ededed}a{color:#ededed;text-decoration-color:#ff6fb5}}</style>";
|
|
420
|
+
export declare const SIGN_IN_COOKIE = "so-sign-in";
|
|
421
|
+
export declare function startedHere(request: IncomingMessage, state: string): boolean;
|
|
422
|
+
export declare const signInSpent: (path: string) => string;
|
|
423
|
+
/**
|
|
424
|
+
* Off to another site's sign-in (Google, a service's one-click connection):
|
|
425
|
+
* a page that moves on by itself. Not a redirect: every page's form-action
|
|
426
|
+
* 'self' also covers where a form's answer redirects, so a browser stops a
|
|
427
|
+
* form that is answered with another site's address.
|
|
428
|
+
*/
|
|
429
|
+
export declare function goOutside(response: ServerResponse, to: string, words: string, bind: {
|
|
430
|
+
state: string;
|
|
431
|
+
path: string;
|
|
432
|
+
secure: boolean;
|
|
433
|
+
}): void;
|
|
434
|
+
export declare function redirect(response: ServerResponse, to: string): void;
|
|
435
|
+
/**
|
|
436
|
+
* A refusal that stays inside the console: same shell, a problem banner, and
|
|
437
|
+
* a way back — the error path is the one place a console must not stop
|
|
438
|
+
* being a console. Bearer callers still get plain text; they parse, not read.
|
|
439
|
+
*/
|
|
440
|
+
export declare function refuse(response: ServerResponse, who: Who | null, status: number, message: string, backHref?: string): void;
|
|
441
|
+
/**
|
|
442
|
+
* A vscode://file href, or null (arc 6, finding 2): the link exists only
|
|
443
|
+
* when every part is provably tame — an absolute, control-free worktree;
|
|
444
|
+
* a relative, single-line path whose segments contain no empty, dot,
|
|
445
|
+
* dot-dot, or backslash components (so lexical resolution stays below the
|
|
446
|
+
* worktree); a line inside the same 1..1,000,000 range the comment form
|
|
447
|
+
* enforces. Encoding failures return null — a file row degrades to plain
|
|
448
|
+
* text, never to a 500. The scheme is a constant, never data.
|
|
449
|
+
*/
|
|
450
|
+
export declare function editorFileHref(worktree: string, path: string, line?: number | null): string | null;
|
|
451
|
+
/** The execution profile in plain words (v24): what the password signs
|
|
452
|
+
* says WHAT RUNS — provider, exact model, permissions, and the real
|
|
453
|
+
* bounds — or says honestly that it cannot yet. */
|
|
454
|
+
/** The state badges the criterion-to-evidence matrix renders with — one
|
|
455
|
+
* shared vocabulary, so a "failed" row reads the same shade of trouble on
|
|
456
|
+
* the task page, the run page, the board, done, builds, the inbox, and
|
|
457
|
+
* chat (v39, extending Priority 2's "one surface, six places" rule from
|
|
458
|
+
* the verdict word to the per-criterion state). */
|
|
459
|
+
export declare function matrixStateBadge(state: CriterionMatrixRow["state"]): string;
|
|
460
|
+
/** v40: the bounded repair chain's own line — presented as the user-facing
|
|
461
|
+
* recovery it represents, without exposing the internal workflow name.
|
|
462
|
+
* Renders on the task page and the run page identically. */
|
|
463
|
+
export declare function repairChainHtml(chain: RepairChainRow | null): string;
|
|
464
|
+
/**
|
|
465
|
+
* The bounded review-retry panel (v50), shared by the task page and the
|
|
466
|
+
* result cockpit so both say the same thing: which attempt is running or
|
|
467
|
+
* queued, how the latest one ended, every root attempt on record, and the
|
|
468
|
+
* ONE explicit act — a Retry review form — rendered only while the store's
|
|
469
|
+
* allowance admits it and this session may ask (an approver's cookie
|
|
470
|
+
* session). Every other state shows the same control disabled, in words,
|
|
471
|
+
* so a person always sees why nothing more will happen by itself. Empty
|
|
472
|
+
* when no review was ever asked for.
|
|
473
|
+
*/
|
|
474
|
+
export declare function reviewRetryPanel(_taskId: string, _sourceRun: number, retry: ReviewRetryState | null, _options: {
|
|
475
|
+
csrf: string;
|
|
476
|
+
canAct: boolean;
|
|
477
|
+
returnTo: string | null;
|
|
478
|
+
}): string;
|
|
479
|
+
/** A one-line summary of the matrix for list rows too dense for the full
|
|
480
|
+
* table (done, builds, board, inbox) — "2/3 criteria", plus a worst-state
|
|
481
|
+
* badge so trouble is visible without opening the row. `[]` renders
|
|
482
|
+
* nothing. */
|
|
483
|
+
export declare function criterionMatrixSummary(matrix: readonly CriterionMatrixRow[]): string;
|
|
484
|
+
/** Where a criterion's own typed evidence ref resolves to a stored
|
|
485
|
+
* artifact, keyed `${kind}:${ref}` — built once per run from its
|
|
486
|
+
* artifacts (v39 review finding: "link artifacts where possible"). A
|
|
487
|
+
* `changed-path` ref never gets its own per-file artifact, so every one of
|
|
488
|
+
* those shares the single terminal-diff patch, under the wildcard key. A
|
|
489
|
+
* `manual-review` ref never resolves — it names nothing machine-checkable. */
|
|
490
|
+
export type EvidenceLinkMap = ReadonlyMap<string, number>;
|
|
491
|
+
export declare const CHECK_LOG_CAPTURE: RegExp;
|
|
492
|
+
export declare function evidenceLinksFor(artifacts: readonly Artifact[]): EvidenceLinkMap;
|
|
493
|
+
/** Read the approved requirement first; inspect its saved evidence on demand.
|
|
494
|
+
* Machine results, reviewer judgements and missing context remain separate.
|
|
495
|
+
* Warnings stay outside the disclosure, including on compact surfaces. */
|
|
496
|
+
export declare function criterionMatrixHtml(matrix: readonly CriterionMatrixRow[], options?: {
|
|
497
|
+
compact?: boolean;
|
|
498
|
+
runId?: number;
|
|
499
|
+
links?: EvidenceLinkMap;
|
|
500
|
+
fileAnchors?: ReadonlyMap<string, string>;
|
|
501
|
+
verdict?: string | null;
|
|
502
|
+
}): string;
|
|
503
|
+
/** What the task and approval views show about a drafted plan's contract
|
|
504
|
+
* (contract handoff, task 1). A problem is a named problem, never a blank. */
|
|
505
|
+
export type PlanContractView = {
|
|
506
|
+
run: number;
|
|
507
|
+
problem: string;
|
|
508
|
+
} | {
|
|
509
|
+
run: number;
|
|
510
|
+
sourceDigest: string;
|
|
511
|
+
/** Whether a scope was filed before planning (false: legacy road). */
|
|
512
|
+
filed: boolean;
|
|
513
|
+
amendment: string | null;
|
|
514
|
+
changes: ContractChange[];
|
|
515
|
+
changeWords: string[];
|
|
516
|
+
/** The proposed terms are still exactly the scope row. */
|
|
517
|
+
current: boolean;
|
|
518
|
+
/** A revision: the terms before planning were copied from the previous version, not filed by a person. */
|
|
519
|
+
revision?: boolean;
|
|
520
|
+
};
|
|
521
|
+
export declare function contractChangeHtml(change: ContractChange): string;
|
|
522
|
+
/**
|
|
523
|
+
* The contract panel (contract handoff, task 1): whether the drafted plan
|
|
524
|
+
* reproduced the filed goal, exclusions, touches, and rubric exactly, or
|
|
525
|
+
* proposes an amendment — every addition, change, and removal listed, the
|
|
526
|
+
* planner's stated reason beside them, and the plain consequence that
|
|
527
|
+
* approving binds the AMENDED terms. "full" is the task page's card;
|
|
528
|
+
* "ceremony" is the restatement inside the approval form and /next.
|
|
529
|
+
*/
|
|
530
|
+
export declare function planContractHtml(view: PlanContractView | null, mode: "full" | "ceremony"): string;
|
|
531
|
+
/** One policy summary beneath the requirements. Per-requirement concerns
|
|
532
|
+
* are already visible in the matrix; do not repeat their full text here. */
|
|
533
|
+
export declare function semanticCoverageHtml(matrix: readonly CriterionMatrixRow[], qualityMode: "default" | "strict"): string;
|
|
534
|
+
/** The exact path limits, one per line (UI polish 2026-09-13): a long
|
|
535
|
+
* comma run was the least readable term on a phone. Every path, verbatim. */
|
|
536
|
+
export declare function approvalPathsHtml(touches: readonly string[]): string;
|
|
537
|
+
/** The rubric, restated above the seal (v39) — the same claim the digest
|
|
538
|
+
* line already makes ("approval binds to this exact wording") extended to
|
|
539
|
+
* the acceptance terms: an id in Geist Mono (a machine fact the proof must
|
|
540
|
+
* answer by), a statement in Geist, the signed evidence kinds after
|
|
541
|
+
* it. `how` never renders here — it is advisory, never signed. Empty
|
|
542
|
+
* renders nothing: a grandfathered scope's ceremony is unchanged. */
|
|
543
|
+
export declare function acceptanceCeremonyHtml(criteria: readonly AcceptanceCriterion[]): string;
|
|
544
|
+
/** "claude" + "opus" → "Claude Opus"; "claude-opus-5-5" → "Claude Opus 5.5"; anything else keeps its exact id. */
|
|
545
|
+
export declare function agentNameWords(provider: string, model: string | null): string;
|
|
546
|
+
/** The permission a sealed profile grants, in two or three words. */
|
|
547
|
+
export declare function permissionWordsOf(profile: Scope["profile"] | null | undefined): string | null;
|
|
548
|
+
/** One plain sentence for a revision (approval critique, Oct 2):
|
|
549
|
+
* "Fixes what build #9 missed: <criterion>." The notes, paths and lineage
|
|
550
|
+
* stay in the Details fold. */
|
|
551
|
+
export declare function revisionSentence(revision: Extract<RevisionView, {
|
|
552
|
+
sourceRun: number;
|
|
553
|
+
}>, acceptance: readonly AcceptanceCriterion[]): string;
|
|
554
|
+
/** A planner's amendment, said plainly and kept in view: approving binds
|
|
555
|
+
* the amended terms, so this is never folded away. */
|
|
556
|
+
export declare function approvalAmendmentHtml(view: PlanContractView | null | undefined): string;
|
|
557
|
+
/**
|
|
558
|
+
* The approval sheet (approval critique, Oct 2): the plan open, in plain
|
|
559
|
+
* rows — Goal, Changes, Won't touch, Done when — one line of who builds,
|
|
560
|
+
* one Approve & start beside the password, and every remaining signed
|
|
561
|
+
* term (lineage, routing, runtime limits, the seal) in one Details fold.
|
|
562
|
+
* Presentation only: the same hidden fields, nonce, digest and password
|
|
563
|
+
* post to the same route, and the task page and chat card share it.
|
|
564
|
+
*/
|
|
565
|
+
export declare function approvalSheetHtml(input: {
|
|
566
|
+
surface: "task" | "chat";
|
|
567
|
+
action: string;
|
|
568
|
+
csrf: string;
|
|
569
|
+
nonce: string;
|
|
570
|
+
digest: string;
|
|
571
|
+
returnTo: string | null;
|
|
572
|
+
scope: Scope;
|
|
573
|
+
planDocument: string | null;
|
|
574
|
+
planContract: PlanContractView | null | undefined;
|
|
575
|
+
revision: Extract<RevisionView, {
|
|
576
|
+
sourceRun: number;
|
|
577
|
+
}> | null;
|
|
578
|
+
revisionSourceHref: string;
|
|
579
|
+
/** A machine-drafted repair: the build it repairs. */
|
|
580
|
+
repairChain: RepairChainRow | null;
|
|
581
|
+
raceTerms: TournamentTerms | null;
|
|
582
|
+
route: RouteView | null | undefined;
|
|
583
|
+
coordinator: {
|
|
584
|
+
label: string;
|
|
585
|
+
filedAgo: string | null;
|
|
586
|
+
} | null;
|
|
587
|
+
deliverable: "branch" | "report";
|
|
588
|
+
editHref: string;
|
|
589
|
+
notNowHref: string;
|
|
590
|
+
sticky: boolean;
|
|
591
|
+
/** Earlier versions of this task still queued or running, and how many of them are running. */
|
|
592
|
+
earlier: {
|
|
593
|
+
active: number;
|
|
594
|
+
running: number;
|
|
595
|
+
};
|
|
596
|
+
/** The in-place editor (task page): the scope route, a draft the server
|
|
597
|
+
* refused (with why), whether it opens on arrival, and the plan's steps editor. */
|
|
598
|
+
edit: {
|
|
599
|
+
action: string;
|
|
600
|
+
draft: URLSearchParams | null;
|
|
601
|
+
problem: string | null;
|
|
602
|
+
open: boolean;
|
|
603
|
+
stepsHref: string | null;
|
|
604
|
+
} | null;
|
|
605
|
+
}): string;
|
|
606
|
+
/** Earlier versions of a task still in flight, in plain words: queued
|
|
607
|
+
* while they only wait, running once one has started; null when none. */
|
|
608
|
+
export declare function earlierVersionsWords(active: number, running: number): string | null;
|
|
609
|
+
/** An in-place plan edit that would change the agent's permissions: thrown to roll the save back. */
|
|
610
|
+
export declare class PermissionsWouldChange extends Error {
|
|
611
|
+
}
|
|
612
|
+
/** Evidence kinds in plain words, for the signed criteria in Details. */
|
|
613
|
+
export declare const EVIDENCE_PLAIN: Record<string, string>;
|
|
614
|
+
/** The permission choice a sealed profile carries, as the scope form names it. */
|
|
615
|
+
export declare function permissionModeOfProfile(profile: Scope["profile"] | null | undefined): UnattendedPermissionMode | null;
|
|
616
|
+
/** Done when, from the in-place editor: each line keeps its requirement's
|
|
617
|
+
* id by position, and its evidence and guidance while its words stand; a
|
|
618
|
+
* rewritten one is yours to check, like an added one; an emptied one is
|
|
619
|
+
* dropped. */
|
|
620
|
+
export declare function requirementsFromEditor(body: FormView<"requirement" | "requirement-new">, current: readonly AcceptanceCriterion[]): unknown[];
|
|
621
|
+
/**
|
|
622
|
+
* THE CONSENT DOOR (v48): whether a yes can be given on this scope right
|
|
623
|
+
* now — one answer for the task page, the focused chat, and /next, so no
|
|
624
|
+
* surface mints a nonce or shows a password the others would refuse.
|
|
625
|
+
* Closed when the scope cannot say exactly what would run: an unresolved
|
|
626
|
+
* profile, a routed row whose route cannot be read, a route with a stated
|
|
627
|
+
* problem, or a pre-routing row whose old approval no longer stands (its
|
|
628
|
+
* profile alone can no longer be agreed to — re-filing routes it). An
|
|
629
|
+
* approved legacy row never reaches here: its old yes is grandfathered.
|
|
630
|
+
* Every closed answer carries the words and the one road that opens it.
|
|
631
|
+
*/
|
|
632
|
+
export type ConsentDoor = {
|
|
633
|
+
open: true;
|
|
634
|
+
} | {
|
|
635
|
+
open: false;
|
|
636
|
+
title: string;
|
|
637
|
+
why: string;
|
|
638
|
+
road: "scope" | "agents";
|
|
639
|
+
};
|
|
640
|
+
export declare function consentDoorOf(scope: Scope | null, route: RouteView | null | undefined): ConsentDoor;
|
|
641
|
+
/** The closed door, rendered: no nonce, no password, no approve button —
|
|
642
|
+
* the reason and the one act that opens it. */
|
|
643
|
+
export declare function consentClosedHtml(taskId: string, door: ConsentDoor & {
|
|
644
|
+
open: false;
|
|
645
|
+
}, surface: "task" | "chat" | "next"): string;
|
|
646
|
+
export declare function profileWords(scope: Pick<Scope, "profile" | "profileState" | "unresolvedReason" | "digestVersion" | "proposedChainJson">): string;
|
|
647
|
+
/** The Agents view every console surface renders (v47): the SAME
|
|
648
|
+
* projection the CLI prints — who plans, builds, and revises,
|
|
649
|
+
* whether each was recommended, overridden, or pinned, its reasons, and
|
|
650
|
+
* the readiness the task's runners have reported (ready / unavailable /
|
|
651
|
+
* unknown — an unknown is said, never upgraded). Readiness is volatile and
|
|
652
|
+
* never part of what an approval signs, so the ceremony shows the agents
|
|
653
|
+
* and their reasons only. */
|
|
654
|
+
export type RouteView = {
|
|
655
|
+
kind: "route" | "legacy" | "unreadable";
|
|
656
|
+
source: "approved" | "proposed" | "live" | null;
|
|
657
|
+
projection: RouteProjection | null;
|
|
658
|
+
/** A proven pre-routing row: its approved agent profile, nothing more. */
|
|
659
|
+
legacy: {
|
|
660
|
+
provider: string;
|
|
661
|
+
model: string;
|
|
662
|
+
repairModel: string;
|
|
663
|
+
approved: boolean;
|
|
664
|
+
} | null;
|
|
665
|
+
/** A routed row whose agents cannot be read — fail closed, in words. */
|
|
666
|
+
problem: string | null;
|
|
667
|
+
riskLevel: RiskLevel;
|
|
668
|
+
overrides: RouteOverride[];
|
|
669
|
+
/** An approver may edit: a live claim, a tournament, or a viewer session refuses. */
|
|
670
|
+
editable: boolean;
|
|
671
|
+
editableWhy: string | null;
|
|
672
|
+
/** A planner change after a draft landed asks for a real re-plan. */
|
|
673
|
+
replanOnPlanChange: boolean;
|
|
674
|
+
/** The scope digest the change forms CAS against; null = no scope yet. */
|
|
675
|
+
digest: string | null;
|
|
676
|
+
/** v48: the configured, role-valid agents an approver may choose from —
|
|
677
|
+
* every option an exact pair the operator named once in configuration.
|
|
678
|
+
* The form offers these and nothing else. */
|
|
679
|
+
choices: Record<Phase, AgentChoice[]>;
|
|
680
|
+
};
|
|
681
|
+
export declare const ROLE_NOUN: Record<RouteProjection["legs"][number]["phase"], string>;
|
|
682
|
+
export declare function agentsStandingWords(view: RouteView): string;
|
|
683
|
+
/** The size line an approval leads its agents with — "Small change: fast
|
|
684
|
+
* model, no plan" — and, quietly, why it was sized so. */
|
|
685
|
+
export declare function sizeLineHtml(projection: RouteProjection): string;
|
|
686
|
+
/** What a size does, for a proposal card. */
|
|
687
|
+
export declare function sizeConsequence(size: string, risky: boolean): string;
|
|
688
|
+
/** The one plain-English line: who does what. */
|
|
689
|
+
export declare function agentsSummaryWords(view: RouteView): string;
|
|
690
|
+
export declare function agentsRiskWords(projection: RouteProjection): string;
|
|
691
|
+
/** Neutral metadata badges: risk, posture, and standing. */
|
|
692
|
+
export declare function agentsBadgesHtml(view: RouteView): string;
|
|
693
|
+
/** Volatile availability, per provider on the route — shown beside the
|
|
694
|
+
* agents, never inside the approval's terms. */
|
|
695
|
+
export declare function agentsAvailabilityHtml(projection: RouteProjection): string;
|
|
696
|
+
/** The closed details: one row per role, with the reasons. */
|
|
697
|
+
export declare function agentsWhyHtml(projection: RouteProjection, summaryLabel?: string): string;
|
|
698
|
+
/** The ceremony's agents block: what the yes agrees to — the agents and
|
|
699
|
+
* their reasons. Availability is volatile and deliberately absent here. */
|
|
700
|
+
export declare function agentsCeremonyHtml(view: RouteView | null | undefined): string;
|
|
701
|
+
/** A standing order's agents (v48): the four-role route its approval
|
|
702
|
+
* froze (or will freeze), in the same concise block a task's ceremony
|
|
703
|
+
* shows — a firing can never be re-routed by a later configuration
|
|
704
|
+
* change, and the page says so. */
|
|
705
|
+
export declare function routineAgentsHtml(routine: Routine, approved: boolean, ceremony?: boolean): string;
|
|
706
|
+
/** The runtime limits the sealed profile binds — permissions, turn and
|
|
707
|
+
* time bounds, repairs, the fallback chain — restated on the ceremony as
|
|
708
|
+
* a CLOSED disclosure (v48): a term the yes covers, one tap away, never a
|
|
709
|
+
* wall of switches between the reader and the password. An unresolved
|
|
710
|
+
* profile still speaks in the open: that is a refusal, not a detail. */
|
|
711
|
+
export declare function runtimeDetailsHtml(scope: Pick<Scope, "profile" | "profileState" | "unresolvedReason" | "digestVersion" | "proposedChainJson">): string;
|
|
712
|
+
/** The task page's Agents card: the summary, availability, closed reasons,
|
|
713
|
+
* and — for an approver — closed change controls. Every change re-files
|
|
714
|
+
* the scope; an approval given under the earlier agents needs renewing. */
|
|
715
|
+
export declare function agentsCardHtml(taskId: string, view: RouteView | null | undefined, csrf: string, canEdit: boolean): string;
|
|
716
|
+
/** The chat's always-visible strip: the summary and a way to the details. */
|
|
717
|
+
export declare function agentsStripHtml(view: RouteView | null, taskId: string): string;
|
|
718
|
+
/** The demo's one-line promise, on every page. */
|
|
719
|
+
export declare const DEMO_BANNER = "Demo: a scripted lead and sample projects. Nothing calls a model, reaches outside or spends. For your own project, run npx toolroll up in its folder.";
|
|
720
|
+
/** The demo notice on a phone: one line. */
|
|
721
|
+
export declare const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model or spends.";
|
|
722
|
+
/** Every character that could open a tag or an attribute, dead at the sink. */
|
|
723
|
+
export declare function escape(text: string): string;
|
|
724
|
+
/**
|
|
725
|
+
* The Operations Ledger: the design system Alex approved in Figma and the
|
|
726
|
+
* design/ shadcn package, carried as pure CSS on server-rendered HTML.
|
|
727
|
+
* Deliberately not the React library: the console ships zero dependencies
|
|
728
|
+
* and zero page JavaScript under a CSP that forbids scripts, and a look is
|
|
729
|
+
* not worth that posture. The same semantic tokens render both system light
|
|
730
|
+
* and dark themes.
|
|
731
|
+
*/
|
|
732
|
+
/** The shared palette, light and dark (2026-09-27, "Signal"): neutral grey
|
|
733
|
+
* grounds, ink for every act a person can take, and one chart magenta, the
|
|
734
|
+
* colour nautical charts print lights and cautions in, for what needs a
|
|
735
|
+
* person (the needs-you count, the act that resolves a screen, focus and
|
|
736
|
+
* selection). Status keeps its own four hues; magenta never means "failed".
|
|
737
|
+
* The frame is the ground the sidebar sits on; work sits on paper sheets
|
|
738
|
+
* inset into it. Semantic names only; every surface (server pages, the
|
|
739
|
+
* React workspace, the pre-script fallback) reads these. */
|
|
740
|
+
export declare const THEME_LIGHT = "\n --so-ground: #efefef; --so-paper: #ffffff; --so-sidebar: #efefef; --so-raised: #f5f5f5;\n --so-ink: #171717; --so-muted: #666666; --so-line: #e6e6e6; --so-input-line: #d4d4d4;\n --so-accent: #171717; --so-accent-hover: #383838; --so-accent-text: #171717; --so-on-accent: #ffffff; --so-soft: #f2f2f2;\n --so-nav-ink: #525252; --so-nav-hover: #e4e4e4; --so-nav-current: #ffffff; --so-nav-current-ink: #171717;\n --so-signal: #171717; --so-signal-hover: #383838; --so-on-signal: #ffffff; --so-signal-soft: #e8e8e8;\n --so-danger: #c4320a; --so-danger-soft: #feebe7; --so-success: #218358; --so-success-soft: #e6f6eb;\n --so-warning: #ab6400; --so-warning-soft: #fff4d5; --so-info: #0d74ce; --so-info-soft: #e6f4fe;\n --so-neutral-ink: #525252; --so-neutral-soft: #f0f0f0; --so-live: #0d74ce;\n --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);\n --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);\n --so-pill-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .06);\n --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);\n --so-selection: #d4d4d4; --so-scroll: #cfcfcf; --so-code-bg: #fafafa; --so-user-bubble: #f2f2f2;";
|
|
741
|
+
export declare const THEME_DARK = "\n --so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;\n --so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;\n --so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;\n --so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;\n --so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;\n --so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);\n --so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);\n --so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;\n --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);\n --so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);\n --so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);\n --so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);\n --so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;";
|
|
742
|
+
/** The console's original token names, now views onto the palette. */
|
|
743
|
+
export declare const THEME_MAPPING = "\n --background: var(--so-ground); --foreground: var(--so-ink); --card: var(--so-paper);\n --muted: var(--so-raised); --muted-foreground: var(--so-muted); --border: var(--so-line); --input: var(--so-input-line);\n --primary: var(--so-accent); --primary-foreground: var(--so-on-accent); --secondary: var(--so-raised); --secondary-foreground: var(--so-ink);\n --accent: var(--so-soft); --destructive: var(--so-danger); --destructive-strong: var(--so-danger); --destructive-soft: var(--so-danger-soft);\n --success: var(--so-success); --success-soft: var(--so-success-soft); --warning: var(--so-warning); --warning-soft: var(--so-warning-soft);\n --running: var(--so-info); --running-soft: var(--so-info-soft); --ring: var(--so-signal);\n /* Magenta means one thing: a person is needed (approve, answer, decide). */\n --brand: var(--so-attention); --brand-foreground: var(--so-on-attention); --brand-soft: var(--so-attention-soft);\n --glass: var(--so-paper); --glass-strong: var(--so-paper); --glass-border: var(--so-line); --glass-highlight: transparent;\n --ambient-one: transparent; --ambient-two: transparent; --user-message: var(--so-user-bubble);\n --surface: var(--so-paper); --ok: var(--so-success); --danger: var(--so-danger); --fg-muted: var(--so-muted);\n --shadow: none; --shadow-overlay: var(--so-shadow-overlay);";
|
|
744
|
+
export declare const STYLE = "\n/* The Console \u2014 the design system, v4 \"Signal\" (2026-09-27). A neutral\n frame with paper sheets inset into it (Arc, Linear), compact Raycast\n density, Geist for words and Geist Mono for machine facts. Ink is every\n act a person can take; the accent (ink by default) means \"waits on you\" and nothing\n else; blue means live. Flat surfaces: hairlines and one soft sheet\n shadow, no glass. Zero dependencies, zero page JS beyond the nonce'd\n chrome layer. */\n @font-face {\n font-family: \"Geist\"; font-style: normal; font-weight: 400;\n font-display: swap; src: url(\"/fonts/geist-sans-400.woff2\") format(\"woff2\");\n }\n @font-face {\n font-family: \"Geist\"; font-style: normal; font-weight: 500;\n font-display: swap; src: url(\"/fonts/geist-sans-500.woff2\") format(\"woff2\");\n }\n @font-face {\n font-family: \"Geist\"; font-style: normal; font-weight: 600;\n font-display: swap; src: url(\"/fonts/geist-sans-600.woff2\") format(\"woff2\");\n }\n @font-face {\n font-family: \"Geist Mono\"; font-style: normal; font-weight: 400;\n font-display: swap; src: url(\"/fonts/geist-mono-400.woff2\") format(\"woff2\");\n }\n @font-face {\n font-family: \"Geist Mono\"; font-style: normal; font-weight: 500;\n font-display: swap; src: url(\"/fonts/geist-mono-500.woff2\") format(\"woff2\");\n }\n @font-face {\n font-family: \"Geist Mono\"; font-style: normal; font-weight: 600;\n font-display: swap; src: url(\"/fonts/geist-mono-600.woff2\") format(\"woff2\");\n }\n /* One palette for every page (2026-09-27): neutral grey and paper with\n * one chart magenta, light by day and dark after hours. The device decides unless\n * the person pins a theme (html[data-theme], set from their cookie). The\n * console's older token names map onto it, so the React workspace, the\n * server-rendered pages and the pre-script fallback all paint the same. */\n :root {\n color-scheme: light;\n\n --so-ground: #efefef; --so-paper: #ffffff; --so-sidebar: #efefef; --so-raised: #f5f5f5;\n --so-ink: #171717; --so-muted: #666666; --so-line: #e6e6e6; --so-input-line: #d4d4d4;\n --so-accent: #171717; --so-accent-hover: #383838; --so-accent-text: #171717; --so-on-accent: #ffffff; --so-soft: #f2f2f2;\n --so-nav-ink: #525252; --so-nav-hover: #e4e4e4; --so-nav-current: #ffffff; --so-nav-current-ink: #171717;\n --so-signal: #171717; --so-signal-hover: #383838; --so-on-signal: #ffffff; --so-signal-soft: #e8e8e8;\n --so-danger: #c4320a; --so-danger-soft: #feebe7; --so-success: #218358; --so-success-soft: #e6f6eb;\n --so-warning: #ab6400; --so-warning-soft: #fff4d5; --so-info: #0d74ce; --so-info-soft: #e6f4fe;\n --so-neutral-ink: #525252; --so-neutral-soft: #f0f0f0; --so-live: #0d74ce;\n --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);\n --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);\n --so-pill-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .06);\n --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);\n --so-selection: #d4d4d4; --so-scroll: #cfcfcf; --so-code-bg: #fafafa; --so-user-bubble: #f2f2f2;\n\n --background: var(--so-ground); --foreground: var(--so-ink); --card: var(--so-paper);\n --muted: var(--so-raised); --muted-foreground: var(--so-muted); --border: var(--so-line); --input: var(--so-input-line);\n --primary: var(--so-accent); --primary-foreground: var(--so-on-accent); --secondary: var(--so-raised); --secondary-foreground: var(--so-ink);\n --accent: var(--so-soft); --destructive: var(--so-danger); --destructive-strong: var(--so-danger); --destructive-soft: var(--so-danger-soft);\n --success: var(--so-success); --success-soft: var(--so-success-soft); --warning: var(--so-warning); --warning-soft: var(--so-warning-soft);\n --running: var(--so-info); --running-soft: var(--so-info-soft); --ring: var(--so-signal);\n /* Magenta means one thing: a person is needed (approve, answer, decide). */\n --brand: var(--so-attention); --brand-foreground: var(--so-on-attention); --brand-soft: var(--so-attention-soft);\n --glass: var(--so-paper); --glass-strong: var(--so-paper); --glass-border: var(--so-line); --glass-highlight: transparent;\n --ambient-one: transparent; --ambient-two: transparent; --user-message: var(--so-user-bubble);\n --surface: var(--so-paper); --ok: var(--so-success); --danger: var(--so-danger); --fg-muted: var(--so-muted);\n --shadow: none; --shadow-overlay: var(--so-shadow-overlay);\n --radius: 0.625rem;\n --so-ease-out: cubic-bezier(.23, 1, .32, 1);\n --font-sans: \"Geist\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n --font-mono: \"Geist Mono\", ui-monospace, \"SF Mono\", SFMono-Regular, Menlo, Consolas, monospace;\n /* Phone spacing steps (760px and narrower): side gutter, gap under the\n header, gap between page blocks, between rows, and inside a card. */\n --so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;\n }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n color-scheme: dark;\n\n --so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;\n --so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;\n --so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;\n --so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;\n --so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;\n --so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);\n --so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);\n --so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;\n --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);\n --so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);\n --so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);\n --so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);\n --so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;\n }\n }\n :root[data-theme=\"dark\"] {\n color-scheme: dark;\n\n --so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;\n --so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;\n --so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;\n --so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;\n --so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;\n --so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);\n --so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);\n --so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;\n --so-attention: var(--so-signal); --so-on-attention: var(--so-on-signal); --so-attention-soft: var(--so-signal-soft);\n --so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);\n --so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);\n --so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);\n --so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;\n }\n * { box-sizing: border-box; }\n /* Touch: no grey flash on a tap, no double-tap zoom wait on a control,\n and a long press never selects a control's label. Text stays selectable. */\n html { -webkit-tap-highlight-color: transparent; }\n a, button, summary, [role=button], [role=tab] { touch-action: manipulation; }\n button, [role=button], [role=tab], .task-context-chip, .approval-chip, .verdict-chip { -webkit-user-select: none; user-select: none; }\n ::selection { background: var(--so-selection); color: var(--so-ink); }\n ::placeholder { color: var(--muted-foreground); }\n body {\n margin: 0; color: var(--foreground);\n background: var(--background);\n caret-color: var(--so-signal);\n font: 400 0.875rem/1.5 var(--font-sans); font-feature-settings: \"ss01\" 0;\n -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;\n }\n :focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }\n\n /* Scrollbars belong to the theme, not the platform default. */\n * { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }\n\n .topbar {\n position: sticky; top: 0; z-index: 10;\n border-bottom: 1px solid var(--border); background: var(--background);\n }\n .topbar-inner {\n max-width: 44rem; margin-inline: auto; padding: 0 1.25rem; height: 3.25rem;\n display: flex; align-items: center; gap: 1.25rem;\n }\n .brand { font-weight: 600; letter-spacing: -0.01em; color: var(--foreground); text-decoration: none;\n display: flex; align-items: center; height: 100%; }\n .brand .dot { color: var(--muted-foreground); }\n .topbar nav { display: flex; gap: .25rem; margin-left: auto; height: 100%; }\n .topbar nav a {\n color: var(--muted-foreground); text-decoration: none; font-size: 0.8125rem; font-weight: 500;\n display: flex; align-items: center; padding: 0 .625rem; transition: color .15s;\n }\n @media (hover: hover) and (pointer: fine) { .topbar nav a:hover { color: var(--foreground); } }\n main { max-width: 44rem; margin-inline: auto; padding: 1.75rem 1.25rem 4rem; }\n h1 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 .25rem; line-height: 1.3; }\n h1 .meta { font-weight: 400; letter-spacing: 0; }\n /* Section headers speak in the human voice (mono is for machine facts\n only): small, semibold, dim \u2014 Linear's \"In Progress 5\" register. */\n h2 {\n font-size: 0.8125rem; font-weight: 600; letter-spacing: -0.005em;\n color: var(--muted-foreground); margin: 2rem 0 .5rem; font-family: var(--font-sans);\n }\n a { color: var(--foreground); text-decoration: underline; text-decoration-color: var(--border); text-underline-offset: 3px; }\n @media (hover: hover) and (pointer: fine) { a:hover { text-decoration-color: var(--muted-foreground); } }\n p { margin: .4rem 0; }\n code { background: var(--muted); border-radius: .3rem; padding: .1rem .35rem; font-family: var(--font-mono); font-size: .8125rem; }\n .mono { font-family: var(--font-mono); font-size: .8125rem; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }\n\n .meta { font-size: 0.8125rem; color: var(--muted-foreground); }\n .meta a { color: var(--muted-foreground); }\n .hint { font-size: 0.75rem; color: var(--muted-foreground); margin: -.375rem 0 .625rem; }\n .eyebrow {\n display: block; color: var(--muted-foreground); font-size: .625rem;\n font-weight: 500; letter-spacing: .08em; line-height: 1.3; text-transform: uppercase;\n font-family: var(--font-mono);\n }\n .num { font-variant-numeric: tabular-nums; }\n\n /* Utility classes replacing the old inline style= attributes. */\n .tight { margin-top: 0; }\n .card > h2:first-child, .card > h3:first-child { margin-top: 0; }\n .w-xs { width: 4.5rem; } .w-sm { width: 8rem; } .w-md { width: 12rem; } .w-lg { width: 14rem; }\n .field-cap { width: 100%; max-width: 28rem; }\n .field-cap-sm { width: 100%; max-width: 22rem; }\n .field-cap-lg { width: 100%; max-width: 34rem; }\n .prewrap { white-space: pre-wrap; }\n .wrap-any { overflow-wrap: anywhere; }\n .grab { cursor: grab; user-select: none; }\n\n .palette {\n position: fixed; top: 18vh; left: 50%; transform: translateX(-50%); width: min(32rem, 90vw);\n background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);\n box-shadow: var(--shadow-overlay);\n padding: .625rem; z-index: 50;\n }\n .palette input { width: 100%; margin: 0; }\n .palette ul { list-style: none; margin: .5rem 0 0; padding: 0; max-height: 40vh; overflow-y: auto; }\n .palette li { padding: .4375rem .625rem; border-radius: calc(var(--radius) - 4px); cursor: pointer; font-size: .875rem; }\n .palette li[aria-selected=\"true\"] { background: var(--muted); }\n\n /* The ledger: the window's harvest as strong figures in a sentence. */\n .ledger { font-size: 0.9375rem; color: var(--muted-foreground); margin: .875rem 0 0; line-height: 1.9; }\n .ledger b { font-weight: 600; font-size: 1.25rem; color: var(--foreground); font-variant-numeric: tabular-nums; padding-right: .1rem; font-family: var(--font-mono); }\n .ledger .good b { color: var(--success); }\n .ledger .bad b { color: var(--destructive); }\n\n /* Status labels are quiet metadata, not decoration. Their words carry\n the meaning; the restrained tint only speeds scanning. Count badges\n remain round so a number cannot be confused with a state. */\n .badge {\n display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--border); border-radius: .375rem;\n padding: .0625rem .4rem; font-size: 0.6875rem; font-weight: 500; line-height: 1.45;\n background: color-mix(in srgb, var(--card) 58%, transparent); color: var(--muted-foreground); vertical-align: middle;\n font-family: var(--font-sans); font-variant-numeric: tabular-nums; white-space: nowrap;\n }\n .badge-done, .badge-answered, .badge-verified, .badge-built {\n background: color-mix(in srgb, var(--muted) 58%, var(--card)); color: var(--foreground);\n border-color: var(--border);\n }\n .badge-failed {\n background: color-mix(in srgb, var(--muted) 58%, var(--card));\n color: color-mix(in srgb, var(--destructive) 68%, var(--foreground)); border-color: var(--border);\n }\n .badge-note {\n background: color-mix(in srgb, var(--muted) 58%, var(--card));\n color: var(--so-warning, #ab6400); border-color: var(--border);\n }\n .badge-overdue {\n background: color-mix(in srgb, var(--muted) 58%, var(--card)); color: var(--foreground);\n border-color: var(--border);\n }\n /* The criterion matrix's fourth state (v39): neither a pass nor a\n failure \u2014 evidence resolved everywhere except a manual-review kind,\n which by design nothing here can machine-verify. Quieter than\n badge-failed; still visibly distinct from a plain pass. */\n .badge-manual-review {\n background: color-mix(in srgb, var(--muted) 58%, var(--card));\n color: color-mix(in srgb, var(--muted-foreground) 80%, var(--foreground)); border-color: var(--border);\n }\n /* \"open\" and \"parked\" are neutral facts (an open PR, a parked decision);\n the MAGENTA form is the attention count \u2014 the number that waits on you.\n One accent, two places (reduction pass \u00A73): the needs-you count and\n the act that resolves the screen. Cards, frames, and seals are neutral. */\n .badge-open, .badge-parked { color: var(--foreground); }\n .count {\n min-width: 1.25rem; padding-inline: .35rem; border-radius: 9999px;\n }\n .count.badge-open {\n background: var(--brand-soft); color: var(--brand);\n border-color: color-mix(in srgb, var(--brand) 24%, var(--border));\n }\n .badge-running {\n background: color-mix(in srgb, var(--muted) 58%, var(--card));\n color: color-mix(in srgb, var(--running) 72%, var(--foreground)); border-color: var(--border);\n }\n .badge-cut { background: var(--muted); }\n\n .card {\n border: 1px solid var(--glass-border); border-radius: var(--radius); background: var(--glass);\n padding: 1rem 1.125rem; margin: .75rem 0;\n }\n .problem {\n border: 1px solid color-mix(in srgb, var(--destructive) 35%, transparent);\n background: var(--destructive-soft); color: var(--destructive);\n border-radius: var(--radius); padding: .625rem .875rem; margin: .75rem 0; font-size: 0.8125rem;\n }\n /* A row is 2.25rem of quiet: hairline below, hover fills, nothing else. */\n .row {\n display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;\n padding: .5rem .375rem; min-height: 2.25rem; border-bottom: 1px solid var(--border);\n margin: 0; border-radius: calc(var(--radius) - 4px);\n }\n .row:last-of-type { border-bottom: none; }\n .row .right { margin-left: auto; }\n a.row { text-decoration: none; }\n @media (hover: hover) and (pointer: fine) { a.row:hover { background: var(--muted); } }\n\n /* A parked decision: the question in full weight, the whole card the\n tap target, the neutral border \u2014 the \"needs you\" header above it\n carries the colour for every card beneath. */\n .decide-card {\n display: block; border: 1px solid var(--border);\n border-radius: var(--radius);\n background: var(--card); padding: .875rem 1.125rem; margin: .625rem 0;\n text-decoration: none; transition: border-color .15s;\n }\n @media (hover: hover) and (pointer: fine) { .decide-card:hover { border-color: color-mix(in srgb, var(--border) 55%, var(--muted-foreground)); } }\n .decide-card .q { font-weight: 600; margin: 0 0 .25rem; }\n\n /* Buttons: secondary by default (paper + hairline); a form's one\n submit is primary (ink on paper, paper on ink); the approve act is\n the accent; danger is red and outlined. 2.125rem at a desk, 2.75rem to a thumb. */\n button {\n font: 500 0.8125rem/1.4 var(--font-sans); cursor: pointer; border-radius: calc(var(--radius) - 3px);\n border: 1px solid var(--input); background: var(--card); color: var(--foreground);\n padding: .35rem .75rem; min-height: 2.125rem;\n transition: background .12s var(--so-ease-out), border-color .12s var(--so-ease-out), color .12s var(--so-ease-out);\n }\n @media (hover: hover) and (pointer: fine) { button:hover { background: var(--so-soft); } }\n button:active { background: var(--so-nav-hover); }\n form.card > button[type=submit], .sticky-actions button[type=submit], form.card .sticky-actions button {\n background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600;\n }\n @media (hover: hover) and (pointer: fine) { form.card > button[type=submit]:hover, .sticky-actions button[type=submit]:hover {\n background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background));\n } }\n /* The approve act is the one magenta verb: it resolves what waits on you.\n The ceremony's frame is neutral so the button is the only coloured thing\n in it; a danger act stays red even inside one. */\n .approve-form button[type=submit], .approve-form .sticky-actions button[type=submit] {\n background: var(--brand); color: var(--brand-foreground); border-color: var(--brand); font-weight: 600;\n }\n @media (hover: hover) and (pointer: fine) { .approve-form button[type=submit]:hover, .approve-form .sticky-actions button[type=submit]:hover { background: color-mix(in srgb, var(--brand) 85%, var(--foreground)); border-color: color-mix(in srgb, var(--brand) 85%, var(--foreground)); } }\n button.danger, .approve-form button[type=submit].danger {\n color: var(--destructive); border-color: color-mix(in srgb, var(--destructive) 50%, transparent);\n background: transparent; font-weight: 500;\n }\n @media (hover: hover) and (pointer: fine) { button.danger:hover, .approve-form button[type=submit].danger:hover { background: var(--destructive-soft); } }\n /* Removing a credential takes a second, deliberate tap. */\n details.confirm-remove { display: inline-block; margin: 0 0 0 .5rem; vertical-align: top; }\n details.confirm-remove > summary { cursor: pointer; color: var(--destructive); font-size: .8125rem; min-height: 2.5rem; display: inline-flex; align-items: center; }\n details.confirm-remove[open] { display: block; margin: .75rem 0 0; }\n .project-card .project-name:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: .375rem; }\n\n label { display: block; font-size: 0.8125rem; font-weight: 500; margin: .75rem 0 0; color: var(--foreground); }\n input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select {\n width: 100%; margin: .35rem 0 0; padding: .45rem .7rem; font: 400 0.875rem/1.4 var(--font-sans);\n color: var(--foreground); background: var(--card); min-height: 2.125rem;\n border: 1px solid var(--input); border-radius: calc(var(--radius) - 3px);\n transition: border-color .15s, box-shadow .15s;\n }\n @media (hover: hover) and (pointer: fine) { input:hover, textarea:hover, select:hover { border-color: var(--muted-foreground); } }\n input[type=number] { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }\n input[type=radio], input[type=checkbox] { accent-color: var(--ring); }\n .permission-field { border: 0; padding: 0; margin: 1rem 0 0; min-width: 0; }\n .permission-field legend { padding: 0; font-size: .8125rem; font-weight: 600; }\n .permission-toggle { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; margin-top: .5rem; }\n .permission-choice {\n display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .6rem; align-items: start;\n margin: 0; padding: .72rem .78rem; border: 1px solid var(--glass-border);\n border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--card) 66%, transparent);\n cursor: pointer; transition: border-color .15s, background .15s, box-shadow .15s;\n }\n @media (hover: hover) and (pointer: fine) { .permission-choice:hover { background: var(--muted); } }\n .permission-choice:has(input:checked) {\n border-color: color-mix(in srgb, var(--foreground) 42%, var(--border));\n background: var(--card); box-shadow: 0 0 0 1px color-mix(in srgb, var(--foreground) 8%, transparent), var(--shadow);\n }\n .permission-choice input { margin: .14rem 0 0; }\n .permission-choice strong, .permission-choice small { display: block; }\n .permission-choice strong { font-size: .8125rem; }\n .permission-choice small { margin-top: .16rem; color: var(--muted-foreground); font-size: .8125rem; font-weight: 400; line-height: 1.4; }\n .permission-note { margin: .55rem 0 0; }\n .scope-editor .permission-toggle { grid-template-columns: 1fr; }\n .scope-editor .problem { color: var(--foreground); }\n /* New work starts like a conversation, not a configuration sheet. The\n planner turns the one intent into the detailed, signed contract; these\n controls expose the uncommon overrides without making them the door. */\n main:has(.task-intake) { max-width: 68rem; }\n .task-intake { width: min(100%, 50rem); margin: clamp(1rem, 5vh, 4rem) auto 0; }\n .task-intake-hero { text-align: center; margin: 0 auto 1.35rem; max-width: 38rem; }\n .task-intake-mark {\n display: grid; place-items: center; width: 3rem; height: 3rem; margin: 0 auto .85rem;\n border: 1px solid var(--border); border-radius: .75rem;\n background: var(--card); box-shadow: var(--so-pill-shadow);\n font: 600 .72rem/1 var(--font-mono); letter-spacing: -.05em;\n }\n .task-intake-hero h1 { margin: 0; font-size: clamp(1.65rem, 4vw, 2.2rem); letter-spacing: -.045em; }\n .task-intake-hero p { margin: .45rem 0 0; color: var(--muted-foreground); }\n .task-composer { padding: .75rem; border-radius: 1.45rem; box-shadow: var(--shadow-overlay); }\n .task-prompt { margin: 0; font-size: 0; }\n .task-prompt textarea {\n min-height: 8.5rem; max-height: 18rem; margin: 0; padding: .9rem 1rem; resize: vertical;\n border: 0; background: transparent; box-shadow: none; font-size: 1.05rem; line-height: 1.55;\n }\n .task-prompt textarea:focus-visible { border: 0; box-shadow: none; }\n @media (hover: hover) and (pointer: fine) { .task-prompt textarea:hover { border: 0; box-shadow: none; } }\n .task-repo { margin: .25rem .45rem .7rem; }\n .task-composer-footer { display: flex; align-items: center; gap: .5rem; padding: .25rem; }\n .task-context { display: flex; align-items: center; gap: .4rem; flex: 1 1 auto; min-width: 0; }\n .task-context-chip {\n display: inline-flex; align-items: center; min-height: 2rem; max-width: 13rem; padding: .25rem .65rem;\n border: 1px solid var(--glass-border); border-radius: 999px; color: var(--muted-foreground);\n background: color-mix(in srgb, var(--glass) 76%, transparent); font-size: .72rem; white-space: nowrap;\n overflow: hidden; text-overflow: ellipsis;\n }\n .task-quality { margin: 0; flex: none; font-size: 0; }\n .task-quality select {\n width: auto; min-height: 2rem; margin: 0; padding: .25rem 1.75rem .25rem .65rem;\n border-radius: 999px; color: var(--muted-foreground); font-size: .72rem; background-color: var(--glass);\n }\n .task-submit {\n flex: none; min-height: 2.45rem; padding: .45rem .95rem; border-radius: 999px;\n background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600;\n }\n @media (hover: hover) and (pointer: fine) { .task-submit:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: transparent; } }\n details.task-options {\n margin: .65rem .25rem 0; padding: .15rem .5rem 0; border: 0; border-top: 1px solid var(--glass-border);\n border-radius: 0; background: transparent;\n }\n details.task-options[open] { padding-bottom: .25rem; }\n .task-options > summary { display: flex; align-items: center; gap: .5rem; list-style: none; }\n .task-options > summary::-webkit-details-marker { display: none; }\n .task-options > summary::after {\n content: \"\"; width: .4rem; height: .4rem; margin-left: auto; margin-right: .25rem;\n border-right: 1.5px solid var(--muted-foreground); border-bottom: 1.5px solid var(--muted-foreground);\n transform: rotate(45deg) translateY(-.1rem); transition: transform .15s;\n }\n .task-options[open] > summary::after { transform: rotate(225deg) translateY(-.1rem); }\n .task-options > summary small { color: var(--muted-foreground); font-weight: 400; }\n .task-options-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 .8rem; padding: 0 .25rem .5rem; }\n .task-options-grid .wide, .task-options-grid .permission-field { grid-column: 1 / -1; }\n .task-check { display: flex; gap: .55rem; align-items: flex-start; }\n .task-check input { margin-top: .2rem; }\n .task-check > span { display: grid; gap: .1rem; }\n .task-check small { display: block; font-weight: 400; line-height: 1.45; }\n .task-agent-note { text-align: center; max-width: 42rem; margin: .85rem auto 0; }\n .visually-hidden {\n position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important;\n margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important; border: 0 !important;\n }\n input[type=password] { font-family: var(--font-mono); }\n input:focus-visible, textarea:focus-visible, select:focus-visible {\n outline: none; border-color: var(--ring); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 25%, transparent);\n }\n button:focus-visible, a:focus-visible, summary:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }\n\n .inline { display: inline-block; width: auto; margin: 0 .375rem .375rem 0; vertical-align: middle; }\n .assistant-picker .button-link[aria-current=page] { outline: 2px solid var(--ring); outline-offset: 2px; }\n .inline input[type=text] { display: inline-block; width: auto; margin: 0 .375rem 0 0; vertical-align: middle; }\n .inline button { width: auto; }\n\n /* One option = one container: consequence first, then the act. */\n form.option {\n margin: .75rem 0; border: 1px solid var(--border); border-radius: var(--radius);\n background: var(--card); padding: .875rem 1rem;\n }\n form.option button {\n display: block; width: 100%; text-align: left; font-size: 0.9375rem; font-weight: 600;\n min-height: 2.75rem;\n }\n /* Recommended is a suggestion, not attention: neutral emphasis, no magenta. */\n form.option.recommended { border-color: color-mix(in srgb, var(--foreground) 30%, var(--border)); }\n form.option.recommended .badge { background: var(--muted); color: var(--foreground); }\n .consequence { font-size: 0.8125rem; color: var(--muted-foreground); margin: 0 0 .625rem; white-space: pre-wrap; }\n form.option input[type=text] { font-size: 0.8125rem; margin-top: .5rem; min-height: 2.25rem; }\n\n .recap { color: var(--muted-foreground); margin: .75rem 0; white-space: pre-wrap; }\n #scope .recap, #scope .scope-paths, .approval-goal { overflow-wrap: anywhere; }\n .result-card {\n margin: 1.25rem 0; border-color: color-mix(in srgb, var(--success) 32%, var(--glass-border));\n background: var(--card);\n box-shadow: var(--shadow-card);\n }\n .result-card h2 { margin: 0; }\n .completion-receipt {\n position: relative; overflow: hidden; margin: 1rem 0 1.25rem; padding: 1.15rem;\n border-color: color-mix(in srgb, var(--foreground) 13%, var(--glass-border));\n background: var(--card);\n box-shadow: var(--shadow-card);\n }\n .completion-receipt::after {\n content: \"\"; position: absolute; width: 11rem; height: 11rem; right: -5rem; top: -7rem;\n border-radius: 50%; background: color-mix(in srgb, var(--running) 9%, transparent); filter: blur(4px); pointer-events: none;\n }\n .receipt-head { position: relative; z-index: 1; display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }\n .receipt-head h2 { margin: .15rem 0 0; font-size: 1.15rem; }\n /* The receipt's status line is the shared component (.status-line);\n the head keeps it from wrapping under the heading. */\n .receipt-head .status-line { flex: none; font-size: .8125rem; }\n .receipt-summary { position: relative; z-index: 1; max-width: 43rem; margin: .75rem 0 1rem; font-size: 1rem; line-height: 1.55; }\n .receipt-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .55rem; }\n .receipt-facts > span { min-width: 0; padding: .7rem .75rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--glass-strong) 64%, transparent); }\n .receipt-facts strong, .receipt-facts small { display: block; overflow-wrap: anywhere; }\n .receipt-facts strong { font-size: .78rem; font-weight: 600; }\n .receipt-facts small { margin-top: .18rem; color: var(--muted-foreground); font-size: .75rem; line-height: 1.4; }\n /* Thumbnails, never a full-width poster: auto-fill leaves a lone\n screenshot at thumbnail size (UI polish 2026-09-13). */\n .receipt-visuals { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 14rem)); gap: .55rem; margin-top: .75rem; }\n .receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .75rem; text-decoration: none; }\n .receipt-shot img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--muted); }\n @media (hover: hover) and (pointer: fine) { .receipt-shot:hover { color: var(--foreground); } }\n .report-items ol { margin: .35rem 0 .75rem; padding-left: 1.25rem; display: grid; gap: .75rem; }\n .report-items li p { margin: .15rem 0; }\n .report-link { overflow-wrap: anywhere; }\n .receipt-caveats, .receipt-coverage { margin-top: .8rem; padding: .7rem .8rem; border-left: 1px solid var(--border); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: color-mix(in srgb, var(--muted) 62%, transparent); font-size: .78rem; }\n .receipt-caveats ul, .receipt-coverage ul { margin: .3rem 0 0; padding-left: 1.15rem; }\n /* Secondary receipt detail (concise pass, 2026-09-13): native\n disclosures in the receipt's own quiet tone \u2014 no card chrome. */\n details.receipt-coverage, details.receipt-coverage[open] { padding: 0 .8rem .1rem; border-top: 0; border-right: 0; border-bottom: 0; }\n details.receipt-coverage > summary { padding: .5rem 0; font-weight: 600; color: var(--foreground); font-size: .78rem; }\n details.receipt-coverage[open] > summary { padding-bottom: .2rem; }\n details.receipt-coverage ul { margin: 0 0 .5rem; }\n .receipt-history, .receipt-history[open] { margin: .35rem 0 0; padding: 0; border: 0; background: none; border-radius: 0; }\n .receipt-history > summary { padding: .35rem 0; min-height: 2.25rem; display: inline-flex; align-items: center; gap: .25rem; font-size: .8125rem; list-style: none; }\n .receipt-history > summary::-webkit-details-marker { display: none; }\n .receipt-history > summary::before { content: \"\u25B8\"; }\n .receipt-history[open] > summary::before { content: \"\u25BE\"; }\n .receipt-history .receipt-review { margin: 0 0 .35rem; }\n .semantic-coverage p { margin: .2rem 0; }\n .receipt-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .65rem 1rem; margin-top: .9rem; }\n .receipt-actions > a:not(.button-link) { font-size: .78rem; font-weight: 550; }\n /* The review cockpit (Priority 5): a master/detail over completed work.\n The queue is a ranked list, the detail one scan path \u2014 intent, proof,\n changes, publication \u2014 and the primary act sits under the header. */\n main:has(.cockpit) { max-width: 82rem; }\n .cockpit { display: grid; grid-template-columns: minmax(15rem, 19rem) minmax(0, 1fr); gap: 0 1.5rem; align-items: start; }\n .cockpit-queue { position: sticky; top: 1rem; min-width: 0; }\n .cockpit-queue h2 { display: flex; align-items: center; gap: .4rem; margin: 0 0 .25rem; }\n .cockpit-queue .lane-count { border: 1px solid var(--border); border-radius: 999px; padding: .04rem .5rem; font-weight: 500; color: var(--muted-foreground); font-size: .6875rem; }\n .cockpit-queue-hint { margin: 0 0 .6rem; font-size: .7rem; line-height: 1.4; }\n .cockpit-queue-list { list-style: none; margin: 0; padding: 0; max-height: calc(100vh - 9rem); overflow-y: auto; }\n .cockpit-row {\n display: grid; gap: .2rem; padding: .55rem .65rem; margin-bottom: .2rem; min-width: 0;\n border: 1px solid transparent; border-radius: calc(var(--radius) - 3px); text-decoration: none; color: var(--foreground);\n }\n @media (hover: hover) and (pointer: fine) { .cockpit-row:hover { background: var(--card); } }\n .cockpit-row.current { background: var(--muted); border-color: color-mix(in srgb, var(--foreground) 12%, var(--border)); }\n .cockpit-row-head { display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem; min-width: 0; }\n .cockpit-row-head strong { min-width: 0; overflow-wrap: anywhere; font-size: .8125rem; font-weight: 550; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }\n .cockpit-row-meta { display: block; color: var(--muted-foreground); font-size: .68rem; overflow-wrap: anywhere; }\n .cockpit-why { display: block; color: var(--muted-foreground); font-size: .7rem; line-height: 1.35; overflow-wrap: anywhere; }\n .refusal-back a { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; }\n .cockpit-primary { margin: .85rem 0 1rem; }\n .cockpit-primary .button-link { white-space: nowrap; }\n .cockpit-detail { min-width: 0; }\n .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; }\n .cockpit-chips { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem; margin: 0 0 .35rem; }\n .cockpit-next { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: .85rem 0 1rem; padding: .85rem 1rem;\n border-color: var(--glass-border); background: var(--glass-strong); }\n .cockpit-next > div { display: grid; gap: .2rem; min-width: 0; overflow-wrap: anywhere; }\n .cockpit-next .meta { font-size: .78rem; line-height: 1.45; }\n .cockpit-next form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0; }\n .cockpit-next form input[type=text] { min-width: 9rem; margin: 0; }\n .cockpit-next .button-link, .cockpit-next button[type=submit] { white-space: nowrap; }\n .cockpit-section { margin: .85rem 0; padding: .95rem 1.05rem; }\n .cockpit-section h3 { margin: 0 0 .5rem; font-size: .8rem; letter-spacing: .02em; text-transform: uppercase; color: var(--muted-foreground); }\n .cockpit-section .recap { margin: .35rem 0; }\n .cockpit-section .result-section { margin-top: .6rem; }\n .cockpit-section .result-section ul, .cockpit-section ul { margin: .25rem 0 0; padding-left: 1.15rem; }\n .cockpit-section li { overflow-wrap: anywhere; }\n .cockpit-drift { margin: .6rem 0; font-size: .8rem; }\n .cockpit-files { margin: .4rem 0 .6rem; padding-left: 1.25rem; font-size: .78rem; }\n .cockpit-files li { margin: .2rem 0; }\n .cockpit-files .pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }\n .cockpit-section .diff-review { margin-top: .6rem; }\n .cockpit-section .receipt-visuals { margin-top: .6rem; grid-template-columns: repeat(auto-fill, minmax(9rem, 14rem)); }\n .cockpit-disclosure { padding: 0; overflow: hidden; }\n .cockpit-disclosure > summary {\n display: flex; align-items: center; justify-content: space-between; gap: 1rem; min-height: 4rem;\n padding: .85rem 1.05rem; list-style: none; cursor: pointer;\n }\n .cockpit-disclosure > summary::-webkit-details-marker { display: none; }\n .cockpit-disclosure > summary > h3 { display: grid; gap: .12rem; min-width: 0; margin: 0; font-size: .82rem; }\n .cockpit-disclosure > summary small { color: var(--muted-foreground); font-size: .72rem; font-weight: 400; }\n .cockpit-disclosure-action { color: var(--muted-foreground); font-size: .72rem; white-space: nowrap; }\n .cockpit-disclosure-action::after { content: \"\u2193\"; margin-left: .35rem; }\n .cockpit-disclosure[open] .cockpit-disclosure-action::after { content: \"\u2191\"; }\n .cockpit-disclosure-body { padding: 0 1.05rem .95rem; border-top: 1px solid var(--glass-border); }\n .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); }\n .cockpit-proof-group > summary { padding: .65rem .75rem; cursor: pointer; font-weight: 550; }\n .cockpit-proof-group .criterion-matrix { padding: 0 .75rem .7rem; }\n .cockpit-proof-summary { display: grid; gap: .25rem; margin: .15rem 0 .75rem; line-height: 1.45; }\n .cockpit-proof-summary strong { font-size: .88rem; }\n .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; }\n .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; }\n .cockpit-evidence-group > summary::-webkit-details-marker { display: none; }\n .cockpit-evidence-group > summary > span { flex: none; white-space: nowrap; }\n .cockpit-evidence-group > summary small { min-width: 0; color: var(--muted-foreground); font-size: .7rem; font-weight: 400; text-align: right; }\n .cockpit-evidence-group [data-primary-evidence] { scroll-margin-top: 6rem; }\n .cockpit-evidence-body { padding: 0 .75rem .75rem; border-top: 1px solid var(--glass-border); }\n .cockpit-accept { margin-top: .65rem; padding-top: .65rem; border-top: 1px solid var(--glass-border); }\n .cockpit-accept > summary { cursor: pointer; font-size: .78rem; font-weight: 600; }\n .cockpit-accept[open] > summary { margin-bottom: .55rem; }\n .cockpit-accept p { margin: 0 0 .55rem; }\n .cockpit-accept-form { display: flex; gap: .5rem; margin: 0; }\n .cockpit-accept-form input { margin: 0; min-width: 10rem; }\n .cockpit-accept-form button { flex: none; background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n .diff-review {\n overflow: hidden; margin: .8rem 0 .5rem; border: 1px solid var(--glass-border);\n border-radius: var(--radius); background: color-mix(in srgb, var(--card) 82%, transparent);\n \n }\n .diff-review-bar {\n display: flex; align-items: center; justify-content: space-between; gap: .75rem;\n min-height: 2.8rem; padding: .4rem .55rem .4rem .85rem;\n border-bottom: 1px solid var(--glass-border); background: color-mix(in srgb, var(--glass-strong) 68%, transparent);\n }\n .diff-modes { display: inline-flex; padding: .18rem; border: 1px solid var(--glass-border); border-radius: 999px; background: var(--muted); }\n .diff-modes button {\n min-height: 1.75rem; padding: .2rem .7rem; border: 0; border-radius: 999px;\n background: transparent; box-shadow: none; color: var(--muted-foreground); font-size: .7rem;\n }\n @media (hover: hover) and (pointer: fine) { .diff-modes button:hover { transform: none; color: var(--foreground); } }\n .diff-modes button[aria-pressed=\"true\"] { background: var(--card); color: var(--foreground); box-shadow: 0 1px 3px color-mix(in srgb, var(--background) 20%, transparent); }\n .diff-review-help {\n margin: 0; padding: .62rem .85rem; border-bottom: 1px solid var(--glass-border);\n background: color-mix(in srgb, var(--running) 7%, transparent); color: var(--muted-foreground); font-size: .75rem;\n }\n .diff-review[data-mode=\"view\"] .diff-review-help { display: none; }\n .diff-file { margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }\n .diff-file + .diff-file { border-top: 1px solid var(--glass-border); }\n .diff-file[open] { padding-bottom: 0; }\n .diff-file > summary {\n display: flex; align-items: center; gap: .7rem; min-height: 2.75rem; padding: .45rem .85rem;\n list-style: none; color: var(--foreground); background: color-mix(in srgb, var(--glass) 55%, transparent);\n }\n .diff-file > summary::-webkit-details-marker { display: none; }\n .diff-file > summary::before {\n content: \"\"; flex: none; width: .35rem; height: .35rem;\n border-right: 1.5px solid var(--muted-foreground); border-bottom: 1.5px solid var(--muted-foreground);\n transform: rotate(-45deg); transition: transform .15s ease;\n }\n .diff-file[open] > summary::before { transform: rotate(45deg) translateY(-.1rem); }\n .diff-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 .75rem/1.4 var(--font-mono); }\n .diff-file-counts { display: inline-flex; gap: .45rem; flex: none; margin-left: auto; font: 500 .75rem/1 var(--font-mono); }\n .diff-file-counts b { color: var(--success); font-weight: 500; }\n .diff-file-counts i { color: var(--destructive); font-style: normal; }\n .diff-rename { margin: 0; padding: .4rem .85rem; border-top: 1px solid var(--glass-border); }\n .diff-hunk + .diff-hunk { border-top: 1px solid var(--glass-border); }\n .diff-hunk-head {\n overflow-x: auto; padding: .42rem .85rem; border-top: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);\n background: color-mix(in srgb, var(--running) 8%, var(--card)); color: color-mix(in srgb, var(--running) 72%, var(--foreground));\n font: 500 .75rem/1.4 var(--font-mono); white-space: pre;\n }\n .diff-lines { max-width: 100%; overflow-x: auto; background: color-mix(in srgb, var(--background) 48%, var(--card)); }\n .diff-line {\n display: grid; grid-template-columns: 2rem 3.2rem 3.2rem minmax(max-content, 1fr); align-items: stretch;\n min-width: max-content; min-height: 1.8rem; font: 400 .75rem/1.55 var(--font-mono);\n }\n @media (hover: hover) and (pointer: fine) { .diff-line:hover { background: color-mix(in srgb, var(--foreground) 4%, transparent); } }\n .diff-line code { display: flex; min-width: 0; padding: .3rem .75rem .3rem .6rem; color: inherit; white-space: pre; }\n .diff-line code b { display: inline-block; width: 1rem; flex: none; font-weight: 500; opacity: .72; }\n .diff-gutter {\n display: flex; align-items: flex-start; justify-content: flex-end; min-width: 0; padding: .3rem .45rem;\n border-right: 1px solid color-mix(in srgb, var(--border) 72%, transparent); color: var(--muted-foreground); user-select: none;\n }\n .diff-addition { background: color-mix(in srgb, var(--success) 10%, transparent); color: color-mix(in srgb, var(--success) 38%, var(--foreground)); }\n .diff-deletion { background: color-mix(in srgb, var(--destructive) 9%, transparent); color: color-mix(in srgb, var(--destructive) 38%, var(--foreground)); }\n .diff-meta { color: var(--muted-foreground); }\n .diff-annotate, .diff-annotate-space {\n position: sticky; left: 0; z-index: 1; display: grid; place-items: center; width: 2rem; min-width: 2rem; min-height: 1.8rem;\n border: 0; border-right: 1px solid color-mix(in srgb, var(--border) 72%, transparent); border-radius: 0;\n }\n .diff-annotate { padding: 0; background: color-mix(in srgb, var(--card) 94%, transparent); color: var(--muted-foreground); box-shadow: none; opacity: .35; }\n .diff-annotate svg { width: .78rem; height: .78rem; }\n @media (hover: hover) and (pointer: fine) { .diff-annotate:hover { transform: none; background: color-mix(in srgb, var(--running) 15%, var(--card)); color: var(--running); opacity: 1; } }\n .diff-annotate-space { background: color-mix(in srgb, var(--card) 94%, transparent); }\n .diff-review[data-mode=\"view\"] .diff-line { grid-template-columns: 0 3.2rem 3.2rem minmax(max-content, 1fr); }\n .diff-review[data-mode=\"view\"] .diff-annotate,\n .diff-review[data-mode=\"view\"] .diff-annotate-space { visibility: hidden; width: 0; min-width: 0; overflow: hidden; border: 0; pointer-events: none; }\n .diff-review[data-mode=\"annotate\"] .diff-annotate { opacity: .72; }\n .diff-cut { margin: 0; padding: .65rem .85rem; border-top: 1px solid var(--glass-border); color: var(--warning); font-size: .75rem; }\n .diff-comments { display: grid; gap: .5rem; margin: .7rem 0; }\n .diff-comment {\n position: relative; padding: .7rem .8rem .7rem 1rem; border: 1px solid var(--glass-border);\n border-radius: calc(var(--radius) - 2px); background: color-mix(in srgb, var(--glass) 76%, transparent);\n }\n .diff-comment-pin { position: absolute; left: -.2rem; top: .75rem; width: .38rem; height: 1.2rem; border-radius: 999px; background: var(--running); }\n .diff-comment p { margin: 0 0 .25rem; overflow-wrap: anywhere; }\n .diff-comment .meta { font-size: .68rem; }\n .diff-comment-form { display: grid; gap: .65rem; margin: .75rem 0; padding: .85rem; }\n .diff-comment-target { display: grid; grid-template-columns: minmax(0, 1fr) 5.5rem; gap: .6rem; }\n .diff-comment-form label { margin: 0; color: var(--muted-foreground); font-size: .68rem; font-weight: 550; }\n .diff-comment-form input, .diff-comment-form textarea { margin-top: .28rem; }\n .diff-comment-form button { justify-self: start; }\n .diff-comment-limit { margin-top: -.35rem; font-size: .66rem; font-variant-numeric: tabular-nums; }\n .revision-from-comments {\n display: flex; align-items: center; justify-content: space-between; gap: 1rem;\n margin: .75rem 0; padding: .85rem; border-color: color-mix(in srgb, var(--running) 24%, var(--glass-border));\n background: var(--card);\n }\n .revision-from-comments > div { display: grid; gap: .15rem; }\n .revision-from-comments > div > span { display: block; }\n .revision-from-comments button { flex: none; }\n .result-section { margin-top: .875rem; }\n .result-section > strong {\n display: block; font: 600 .6875rem/1.3 var(--font-mono); color: var(--muted-foreground);\n letter-spacing: .06em; text-transform: uppercase;\n }\n .result-section ul { margin: .375rem 0 0; padding-left: 1.25rem; }\n .result-section li { margin: .25rem 0; }\n .criterion-matrix .requirement-list { list-style: none; margin: .4rem 0 0; padding: 0; }\n .criterion-matrix .requirement { margin: 0; padding: 1rem 0; border-bottom: 1px solid var(--border); min-width: 0; overflow-wrap: anywhere; }\n .criterion-matrix .requirement:last-child { border-bottom: 0; }\n .requirement-heading { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem .75rem; font-size: .75rem; color: var(--muted-foreground); }\n .requirement-heading .badge { margin: 0; white-space: normal; }\n .requirement-statement { margin: .5rem 0; line-height: 1.55; white-space: pre-wrap; }\n .requirement-review { margin: .4rem 0; font-size: .78rem; color: var(--muted-foreground); }\n .requirement-warning { margin: .65rem 0; padding: .6rem .75rem; border: 1px solid color-mix(in srgb, var(--warning) 35%, var(--border)); border-radius: 8px; background: var(--warning-soft); font-size: .8125rem; }\n .requirement-warning strong { font-size: .78rem; }\n .requirement-warning p { margin: .3rem 0 0; }\n .requirement-issues { font-size: .8125rem; }\n .criterion-matrix .requirement-evidence { margin: .35rem 0 0; padding: 0; border: 0; border-radius: 0; background: none; }\n .requirement-evidence > summary { display: flex; align-items: center; gap: .5rem; min-height: 44px; width: fit-content; padding: .35rem .1rem; cursor: pointer; list-style: none; font-size: .8125rem; }\n .requirement-evidence > summary::-webkit-details-marker { display: none; }\n .requirement-evidence > summary::after { content: \"+\"; color: var(--muted-foreground); }\n .requirement-evidence[open] > summary::after { content: \"\u2212\"; }\n .requirement-evidence-body { padding: .25rem .75rem .65rem; border-left: 1px solid var(--border); }\n .requirement-evidence-body p { margin: .35rem 0; }\n .requirement-evidence-group { margin: .7rem 0; }\n .requirement-evidence-group > strong { font-size: .75rem; color: var(--muted-foreground); }\n .requirement-evidence-group code { white-space: normal; overflow-wrap: anywhere; font-size: .75rem; }\n .requirement-evidence-group a { display: inline-block; padding: .4rem 0; }\n .question { font-size: 1.125rem; font-weight: 600; letter-spacing: -0.01em; margin: 1rem 0; white-space: pre-wrap; }\n .answered {\n border: 1px solid color-mix(in srgb, var(--success) 35%, transparent); background: var(--success-soft);\n border-radius: var(--radius); padding: .875rem 1rem; margin: 1rem 0;\n }\n details {\n margin: .75rem 0; border: 1px solid var(--border); border-radius: var(--radius);\n padding: .25rem .875rem; background: var(--card);\n }\n details[open] { padding-bottom: .875rem; }\n details.arm-danger { border-color: color-mix(in srgb, var(--destructive) 30%, transparent); }\n summary { padding: .5rem 0; cursor: pointer; font-weight: 500; font-size: 0.8125rem; color: var(--muted-foreground); min-height: 2.25rem; }\n @media (hover: hover) and (pointer: fine) { summary:hover { color: var(--foreground); } }\n details form.option { border: none; padding: .25rem 0 0; margin: 0; }\n .evidence { margin-top: 1.5rem; font-size: 0.8125rem; }\n .evidence a { display: block; padding: .55rem 0; border-bottom: 1px solid var(--border); text-decoration: none; }\n @media (hover: hover) and (pointer: fine) { .evidence a:hover { color: var(--muted-foreground); } }\n .evidence strong { display: block; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted-foreground); font-family: var(--font-mono); }\n\n .filters { font-size: 0.8125rem; color: var(--muted-foreground); }\n .filters a, .filters strong {\n display: inline-block; padding: .25rem .625rem; border-radius: 9999px; text-decoration: none;\n color: var(--muted-foreground); font-weight: 500;\n }\n .filters strong { background: var(--muted); color: var(--foreground); }\n @media (hover: hover) and (pointer: fine) { .filters a:hover { color: var(--foreground); } }\n\n /* A picked tournament result: marked in the built green, not magenta. */\n .card.picked { border-color: color-mix(in srgb, var(--success) 40%, var(--border)); }\n .seal {\n display: inline-block; font-family: var(--font-mono); font-size: .75rem;\n background: var(--muted); border: 1px solid var(--border);\n color: var(--foreground); border-radius: calc(var(--radius) - 4px); padding: .25rem .625rem;\n font-variant-numeric: tabular-nums; overflow-wrap: anywhere;\n }\n\n /* The workspace shell: sidebar + content, an optional list pane between. */\n /* The rail collapse is instant: a layout dimension is never animated (UI polish 2026-09-13). */\n .app { display: grid; grid-template-columns: 232px minmax(0, 1fr); min-height: 100vh; }\n .side {\n border-right: 1px solid var(--glass-border);\n background: var(--glass-strong);\n padding: 1rem .875rem 1.125rem; display: flex; flex-direction: column; gap: .175rem;\n position: sticky; top: 0; height: 100vh; overflow-y: auto;\n box-shadow: 1px 0 0 var(--glass-highlight) inset;\n }\n .side-head { display: flex; align-items: center; gap: .25rem; min-height: 2.5rem; margin-bottom: .45rem; }\n .side .brand { flex: 1; min-width: 0; padding: .25rem .625rem; font-size: 1rem; height: auto; letter-spacing: -.025em; }\n .brand-short { display: none; font-family: var(--font-mono); letter-spacing: -.06em; }\n .side-toggle {\n display: grid; place-items: center; flex: 0 0 2rem; width: 2rem; min-height: 2rem; padding: 0;\n border-color: transparent; background: transparent; color: var(--muted-foreground); box-shadow: none;\n }\n @media (hover: hover) and (pointer: fine) { .side-toggle:hover { background: var(--glass); color: var(--foreground); transform: none; } }\n .side-toggle svg { width: 1rem; height: 1rem; }\n /* The scope bar: one hairline row, the single scope truth on every\n * screen; its name is the switcher. Magenta never appears here except\n * the needs-you count. */\n .scope-bar {\n display: flex; align-items: baseline; gap: .625rem; flex-wrap: wrap;\n position: sticky; top: 0; z-index: 20;\n padding: .625rem 2rem; border-bottom: 1px solid var(--glass-border);\n background: var(--glass-strong); font-size: .8125rem;\n }\n .scope-bar .eyebrow { font-size: .6875rem; font-weight: 500; color: var(--muted-foreground); }\n .scope-bar .name { font-weight: 600; }\n /* The switcher: a folded menu of enrolled projects under the scope's name. */\n .switcher { position: relative; }\n .switcher > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .25rem; }\n .switcher > summary::-webkit-details-marker { display: none; }\n .switcher .chevron { width: .875rem; height: .875rem; color: var(--muted-foreground); transition: transform .15s; }\n .switcher[open] > summary .chevron { transform: rotate(180deg); }\n .switcher-menu {\n position: absolute; top: calc(100% + .375rem); left: 0; z-index: 40; min-width: 15rem; max-width: 22rem;\n background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);\n box-shadow: var(--shadow-overlay); padding: .375rem; display: flex; flex-direction: column; gap: .125rem;\n }\n .switcher-menu form { margin: 0; }\n .switcher-menu button {\n display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left; margin: 0;\n background: transparent; border: 0; min-height: 2.5rem; padding: 0 .75rem;\n border-radius: calc(var(--radius) - 4px); font: inherit; font-size: .875rem; color: var(--foreground);\n }\n .switcher-menu button.current { font-weight: 600; }\n .switcher-menu button.current::after {\n content: \"\"; margin-left: auto; width: .375rem; height: .625rem; flex: none;\n border-right: 1.75px solid var(--foreground); border-bottom: 1.75px solid var(--foreground);\n transform: rotate(45deg) translateY(-.125rem);\n }\n @media (hover: hover) and (pointer: fine) { .switcher-menu button:hover { background: var(--muted); } }\n .switcher-menu .manage {\n display: block; margin-top: .25rem; padding: .625rem .75rem; border-top: 1px solid var(--border);\n font-size: .75rem; color: var(--muted-foreground); text-decoration: none;\n }\n .scope-status {\n display: flex; gap: .5rem; flex-wrap: wrap;\n color: var(--muted-foreground); font-size: .6875rem; font-variant-numeric: tabular-nums;\n font-family: var(--font-mono);\n }\n .scope-status .hot { color: var(--brand); font-weight: 500; }\n .scope-status a { color: inherit; text-decoration: none; }\n @media (hover: hover) and (pointer: fine) { .scope-status a:hover { text-decoration: underline; } }\n /* A project card's name and counts are forms or links dressed as text and chips. */\n .project-card button.project-name, .project-card a.project-name {\n all: unset; cursor: pointer; font-weight: 600; color: var(--foreground);\n }\n @media (hover: hover) and (pointer: fine) { .project-card a.project-name:hover, .project-card button.project-name:hover { text-decoration: underline; } }\n .project-card button.badge { min-height: auto; box-shadow: none; cursor: pointer; }\n @media (hover: hover) and (pointer: fine) { .project-card button.badge:hover, .project-card a.badge:hover { border-color: var(--input); } }\n /* Adding a project is the page's primary job, not badge-sized metadata.\n * Two roomy action tiles make both roads obvious on a desk and give each\n * one a generous thumb target on a phone. The manual path stays tertiary. */\n .project-add-card { margin-top: 1.25rem; padding: 1.25rem; }\n .project-add-card h2.project-add-title {\n margin: 0; color: var(--foreground); font-size: 1.0625rem; line-height: 1.35;\n letter-spacing: -.015em;\n }\n .project-add-intro { margin: .25rem 0 0; max-width: 36rem; }\n .project-add-actions {\n display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: .625rem; margin-top: 1rem;\n }\n .project-add-action {\n display: grid; grid-template-columns: 2.75rem minmax(0, 1fr) auto;\n align-items: center; gap: .75rem; min-height: 5rem; padding: .75rem .875rem;\n border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 1px);\n background: var(--glass-strong);\n color: var(--foreground); text-decoration: none;\n \n transition: transform .15s ease, background .15s, border-color .15s, box-shadow .15s;\n }\n @media (hover: hover) and (pointer: fine) { .project-add-action:hover {\n border-color: color-mix(in srgb, var(--border) 50%, var(--muted-foreground));\n background: var(--card); text-decoration: none;\n box-shadow: var(--shadow);\n } }\n .project-add-action:active { transform: translateY(0); }\n .project-add-icon {\n display: grid; place-items: center; width: 2.75rem; height: 2.75rem;\n border: 1px solid var(--border); border-radius: .75rem;\n background: var(--muted); color: var(--foreground);\n }\n .project-add-icon svg { width: 1.125rem; height: 1.125rem; }\n .project-add-copy { min-width: 0; }\n .project-add-copy strong { display: block; font-size: .9375rem; line-height: 1.35; }\n .project-add-copy small {\n display: block; margin-top: .175rem; color: var(--muted-foreground);\n font-size: .75rem; font-weight: 400; line-height: 1.4;\n }\n .project-add-arrow { color: var(--muted-foreground); font-size: 1rem; }\n .project-add-more { margin-top: .875rem; border-top: 1px solid var(--border); }\n .project-add-more > summary {\n display: flex; align-items: center; min-height: 2.75rem; width: fit-content;\n color: var(--muted-foreground); cursor: pointer; font-size: .8125rem; font-weight: 500;\n }\n @media (hover: hover) and (pointer: fine) { .project-add-more > summary:hover { color: var(--foreground); } }\n .project-add-more > .card { margin: 0 0 .25rem; }\n .side nav { display: flex; flex-direction: column; gap: .125rem; }\n /* Inline decision options: neutral buttons \u2014 the card's magenta outline is\n * the attention signal; recommendation is a neutral badge, never magenta. */\n .decide-options { margin-top: .5rem; display: flex; flex-direction: column; gap: .375rem; }\n .decide-option { margin: 0; display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }\n .decide-option button { margin: 0; }\n .decide-option .meta { flex: 1 1 12rem; }\n /* The rail's two accordion groups (sidebar rework): collapsed by\n * default, the active page's group open, opening one closes the other\n * (client toggle in chromeScript). Native <details>/<summary> carries\n * the expanded state and keyboard operation for free \u2014 the same pattern\n * the switcher already uses. */\n .side .nav-groups { display: flex; flex-direction: column; gap: .125rem; margin-top: .375rem; }\n .nav-group > summary {\n display: flex; align-items: center; justify-content: space-between; gap: .5rem;\n list-style: none; cursor: pointer; padding: .4375rem .625rem; min-height: 2.125rem;\n border-radius: calc(var(--radius) - 3px); text-decoration: none;\n color: var(--muted-foreground); font-size: .6875rem; font-weight: 500;\n font-family: var(--font-sans);\n }\n .nav-group > summary::-webkit-details-marker { display: none; }\n @media (hover: hover) and (pointer: fine) { .nav-group > summary:hover { background: var(--glass); color: var(--foreground); } }\n .nav-group > summary .chevron { width: .875rem; height: .875rem; flex: none; transition: transform .15s; }\n .nav-group[open] > summary .chevron { transform: rotate(180deg); }\n .nav-group .nav-group-items { display: flex; flex-direction: column; gap: .125rem; margin: .125rem 0 .25rem; }\n .side .nav-settings { margin-top: .375rem; }\n .side nav a {\n position: relative;\n display: flex; align-items: center; gap: .625rem; padding: .4375rem .625rem; min-height: 2.125rem;\n border-radius: calc(var(--radius) - 3px); text-decoration: none;\n color: var(--muted-foreground); font-size: .8125rem; font-weight: 500;\n transition: color .15s, background .15s, transform .15s;\n }\n .side nav a .glyph { display: inline-flex; width: 1rem; height: 1rem; color: var(--muted-foreground); flex: none; }\n .side nav a .glyph svg { width: 1rem; height: 1rem; }\n @media (hover: hover) and (pointer: fine) { .side nav a:hover { background: var(--glass); color: var(--foreground); transform: translateX(2px); } }\n .side nav a:active { background: var(--glass); color: var(--foreground); }\n .app.sidebar-collapsed { grid-template-columns: 64px minmax(0, 1fr); }\n .app.sidebar-collapsed .side { padding-inline: .625rem; }\n .app.sidebar-collapsed .side-head { flex-direction: column; gap: .2rem; margin-bottom: .55rem; }\n .app.sidebar-collapsed .side .brand { flex: none; padding: .2rem 0; font-size: .75rem; }\n .app.sidebar-collapsed .brand-long { display: none; }\n .app.sidebar-collapsed .brand-short { display: block; }\n .app.sidebar-collapsed .side-toggle svg { transform: rotate(180deg); }\n .app.sidebar-collapsed .side nav a {\n justify-content: center; gap: 0; min-height: 2.5rem; padding: .5rem; font-size: 0;\n }\n .app.sidebar-collapsed .side nav a .glyph { width: 1.125rem; height: 1.125rem; }\n .app.sidebar-collapsed .side nav a .glyph svg { width: 1.125rem; height: 1.125rem; }\n .app.sidebar-collapsed .side nav a .count {\n position: absolute; top: .15rem; right: .05rem; min-width: 1rem; padding: 0 .25rem; font-size: .55rem;\n }\n .app.sidebar-collapsed .side .new-task { min-height: 2.5rem; padding: .4rem 0; font-size: 0; }\n .app.sidebar-collapsed .side .new-task::after { content: \"+\"; font-size: 1rem; }\n .app.sidebar-collapsed .side .nav-groups { display: none; }\n .side nav a.active {\n background: var(--card);\n color: var(--foreground); box-shadow: 0 1px 0 var(--glass-highlight) inset;\n }\n .side nav a.active .glyph { color: var(--foreground); }\n .side nav a .count { margin-left: auto; }\n .side .grow { flex: 1; }\n .side .new-task {\n display: block; text-align: center; text-decoration: none; font-weight: 600; font-size: .8125rem;\n background: var(--primary); color: var(--primary-foreground);\n border: 1px solid var(--primary);\n border-radius: calc(var(--radius) - 3px); padding: .525rem; margin: .75rem 0 .125rem;\n box-shadow: 0 10px 28px -18px rgb(255 255 255 / .5);\n }\n @media (hover: hover) and (pointer: fine) { .side .new-task:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); } }\n /* The same action link outside the sidebar reads as a real button. */\n .content .new-task {\n display: inline-block; text-decoration: none; font-weight: 500; font-size: .8125rem;\n background: var(--secondary); color: var(--foreground); border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px); padding: .5rem .875rem;\n }\n @media (hover: hover) and (pointer: fine) { .content .new-task:hover { background: color-mix(in srgb, var(--secondary) 70%, var(--border)); } }\n .content { min-width: 0; }\n .content > main { max-width: 54rem; margin: 0; padding: 2rem 2.5rem 4rem; }\n\n /* Banners: honest labels, quiet strips. */\n .banner {\n border-bottom: 1px solid var(--glass-border); background: var(--glass);\n padding: .375rem .9rem; font-size: .8125rem; color: var(--muted-foreground);\n }\n .banner .badge { margin-right: .5rem; }\n /* Chat in `toolroll demo`: the scripted lead's conversation. */\n .demo-chat { display: flex; flex-direction: column; gap: 1rem; max-width: 46rem; margin: 0 auto; padding-bottom: 1rem; }\n .demo-thread { display: flex; flex-direction: column; gap: 1.25rem; }\n .demo-turn { display: flex; flex-direction: column; gap: .75rem; scroll-margin-top: 1rem; }\n .demo-said p { margin: 0; }\n .demo-you { align-self: flex-end; max-width: min(34rem, 85%); background: var(--so-ink); color: var(--so-paper); border-radius: .875rem .875rem .25rem .875rem; padding: .55rem .85rem; overflow-wrap: anywhere; }\n .demo-lead { max-width: 40rem; }\n .demo-who { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin-bottom: .15rem !important; }\n .demo-chat .card { margin: 0; padding: 1rem 1.125rem; }\n .demo-chat .card h2 { font-size: .9375rem; margin: .1rem 0 .4rem; }\n .demo-chat .card h3 { font-size: .75rem; font-weight: 600; color: var(--so-muted); margin: .75rem 0 .25rem; }\n .demo-chat .card ul { margin: 0; padding-left: 1.1rem; }\n .demo-chat .card li { margin: .15rem 0; }\n .demo-chat .card > p { margin: .25rem 0; }\n .demo-kicker { font-size: .75rem; color: var(--so-muted); margin: 0 !important; }\n .demo-actions { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .5rem; margin-top: .9rem; }\n .demo-actions form { margin: 0; }\n .demo-actions button, .demo-more > summary { min-height: 2.5rem; }\n .demo-chat button.demo-primary { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n .demo-chat details.demo-more { flex: 1 1 12rem; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }\n .demo-chat details.demo-more[open] { padding-bottom: 0; }\n .demo-chat .demo-more > summary { display: inline-flex; align-items: center; color: var(--so-ink); padding: .35rem .85rem; border: 1px solid var(--so-input-line); border-radius: calc(var(--radius) - 3px); background: var(--so-paper); font-weight: 500; font-size: .8125rem; cursor: pointer; list-style: none; }\n .demo-more > summary::-webkit-details-marker { display: none; }\n .demo-more[open] > summary { margin-bottom: .5rem; }\n .demo-more form, .demo-composer { display: flex; flex-direction: column; gap: .4rem; }\n .demo-more label { font-size: .8125rem; font-weight: 500; }\n .demo-more textarea, .demo-composer textarea { width: 100%; font: inherit; font-size: 1rem; resize: vertical; }\n .demo-more form button { align-self: flex-start; }\n .demo-steps { display: flex; flex-wrap: wrap; gap: .4rem 1rem; list-style: none; padding: 0 !important; margin: .4rem 0 !important; }\n .demo-steps li { display: inline-flex; align-items: center; gap: .4rem; color: var(--so-muted); font-size: .8125rem; margin: 0 !important; }\n .demo-steps li::before { content: \"\"; width: .55rem; height: .55rem; border-radius: 50%; border: 1.5px solid currentColor; }\n .demo-steps li.done { color: var(--so-ink); }\n .demo-steps li.done::before { background: var(--so-success); border-color: var(--so-success); }\n .demo-steps li.now { color: var(--so-ink); font-weight: 600; }\n .demo-steps li.now::before { border-color: var(--so-info); background: var(--so-info); animation: demo-pulse 1.2s ease-in-out infinite; }\n @keyframes demo-pulse { 50% { opacity: .35; } }\n @media (prefers-reduced-motion: reduce) { .demo-steps li.now::before { animation: none; } }\n .demo-state { display: flex; align-items: center; gap: .5rem; margin: 0 !important; }\n .demo-chat .demo-handoff { border-color: var(--so-input-line); }\n .demo-command { margin: .4rem 0 .5rem; padding: .6rem .8rem; border-radius: calc(var(--radius) - 3px); background: var(--so-raised); font: 500 .8125rem/1.5 var(--font-mono); overflow-wrap: anywhere; white-space: pre-wrap; }\n .demo-chat .badge.demo-ready { background: var(--so-info-soft); color: var(--so-info); }\n .demo-chat .badge.demo-complete { background: var(--so-success-soft); color: var(--so-success); }\n .demo-pass { color: var(--so-success); font-weight: 500; }\n .demo-fail { color: var(--so-danger); font-weight: 500; }\n .demo-chat details.demo-evidence { margin: .5rem 0 0; padding: 0; border: 0; border-top: 1px solid var(--so-line); border-radius: 0; background: none; box-shadow: none; }\n .demo-evidence > summary { min-height: 2.5rem; display: list-item; padding-block: .55rem; cursor: pointer; font-weight: 500; font-size: .8125rem; }\n .demo-evidence pre { margin: 0 0 .5rem; max-height: 22rem; overflow: auto; padding: .6rem .75rem; border-radius: .5rem; background: var(--so-raised); font: 400 .75rem/1.55 var(--font-mono); white-space: pre; overflow-wrap: normal; }\n .demo-diff span { display: block; min-width: max-content; }\n .demo-diff-add { background: var(--so-success-soft); color: var(--so-success); }\n .demo-diff-del { background: var(--so-danger-soft); color: var(--so-danger); }\n .demo-diff-hunk, .demo-diff-meta { color: var(--so-muted); }\n .demo-diff-file { font-weight: 600; }\n .demo-evidence figure { margin: 0 0 .5rem; }\n .demo-evidence img { display: block; width: 100%; height: auto; border: 1px solid var(--so-line); border-radius: .5rem; }\n .demo-evidence figcaption { margin-top: .35rem; }\n .demo-hint { padding: 1.25rem 0 .25rem; }\n .demo-hint-title { font-size: .9375rem; font-weight: 600; margin: 0 0 .75rem; }\n .demo-suggestions { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; }\n .demo-suggestions button { min-height: 2.5rem; border-radius: 999px; padding: .35rem .95rem; }\n .demo-composer { position: sticky; bottom: 0; padding: .75rem 0 .25rem; background: var(--so-paper); flex-direction: row; align-items: flex-end; }\n .demo-chat .demo-composer textarea { flex: 1 1 auto; width: auto; min-width: 0; min-height: 2.75rem; }\n .demo-chat .demo-composer button { flex: none; width: auto; min-height: 2.75rem; padding-inline: 1.1rem; }\n .banner a { color: var(--muted-foreground); }\n .sign-in-banner form { display: inline; margin: 0 0 0 .5rem; padding: 0; border: 0; background: transparent; }\n\n .split { display: grid; grid-template-columns: minmax(250px, 320px) minmax(0, 1fr); min-height: 100vh; }\n .list-pane {\n border-right: 1px solid var(--border); overflow-y: auto; height: 100vh;\n position: sticky; top: 0; padding: 1rem .75rem;\n }\n .list-pane h2 { margin-top: .25rem; }\n .list-pane a.item {\n display: block; padding: .5rem .625rem; border-radius: calc(var(--radius) - 4px);\n text-decoration: none; font-size: .8125rem; margin-bottom: .125rem;\n }\n @media (hover: hover) and (pointer: fine) { .list-pane a.item:hover { background: var(--card); } }\n .list-pane a.item.current { background: var(--muted); }\n .list-pane a.item .t { display: block; font-weight: 500; color: var(--foreground);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .list-pane a.item .m { display: flex; gap: .375rem; align-items: center; color: var(--muted-foreground);\n font-size: .75rem; margin-top: .125rem; }\n .split > .detail { min-width: 0; }\n .split > .detail > main { max-width: 52rem; padding: 1.5rem 2rem 4rem; }\n .split:has(#wb-rail) { grid-template-columns: minmax(320px, 360px) minmax(0, 1fr); }\n .list-pane:has(#wb-rail) { padding: 0; background: var(--background); }\n #wb-rail { padding: 1rem .75rem 1.5rem; }\n #wb-rail-stamp { padding: 0 .75rem; }\n .workbench-mobile-rail, .workbench-mobile-back { display: none; }\n /* The task page (slice 1c): main column beside a rail; one column narrow. */\n .task-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(16rem, 19rem); gap: 0 2rem; align-items: start; }\n /* The task page (task page pass): title, the acts in one row,\n then folding sections; the rail is the property list. */\n .task-identity { overflow-wrap: anywhere; }\n .task-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }\n .task-title-row .task-main-title { min-width: 0; margin-bottom: .85rem; }\n .task-view-switch {\n display: inline-grid; grid-template-columns: repeat(2, auto); flex: none; padding: .2rem;\n border: 1px solid var(--glass-border); border-radius: .72rem; background: color-mix(in srgb, var(--glass) 82%, transparent);\n \n }\n .task-view-switch a {\n min-width: 4.6rem; padding: .4rem .72rem; border-radius: .52rem; color: var(--muted-foreground);\n font-size: .72rem; font-weight: 550; text-align: center; text-decoration: none;\n }\n @media (hover: hover) and (pointer: fine) { .task-view-switch a:hover { color: var(--foreground); background: color-mix(in srgb, var(--muted) 70%, transparent); } }\n .task-view-switch a.active { color: var(--foreground); background: var(--glass-strong); box-shadow: 0 1px 5px rgb(0 0 0 / .1); }\n .acts-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .75rem 0 .25rem; }\n .acts-bar form.inline { margin: 0; display: inline-flex; align-items: center; gap: .375rem; }\n .acts-bar form.inline button { width: auto; }\n .acts-bar .primary button { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n @media (hover: hover) and (pointer: fine) { .acts-bar .primary button:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); } }\n .acts-bar .act-hold input[type=text] { width: 10rem; min-height: 2.25rem; margin: 0; font-size: .8125rem; }\n .acts-why { margin: 0 0 .5rem; }\n .dispatch-status { padding: .8rem .9rem; border-radius: var(--radius); overflow: hidden; }\n .dispatch-copy { display: flex; align-items: baseline; flex-wrap: wrap; gap: .2rem .35rem; min-width: 0; }\n .dispatch-copy .meta { min-width: 0; }\n .dispatch-action-link { margin-top: .65rem; }\n .dispatch-status[data-dispatch-status=\"proof-refuted\"],\n .dispatch-status[data-dispatch-status=\"needs-verification\"] {\n color: var(--foreground); border-color: var(--glass-border); background: var(--glass);\n }\n .dispatch-status[data-dispatch-status=\"proof-refuted\"] .dispatch-copy > strong::before,\n .dispatch-status[data-dispatch-status=\"needs-verification\"] .dispatch-copy > strong::before {\n content: \"\"; display: inline-block; width: .45rem; height: .45rem; margin-right: .45rem; border-radius: 50%; vertical-align: .08rem;\n background: var(--so-warning, #ab6400);\n }\n .problem.dispatch-status[data-dispatch-status=\"proof-refuted\"] .dispatch-copy > strong::before { background: var(--destructive); }\n .dispatch-status[data-dispatch-status=\"needs-verification\"] .dispatch-copy > strong::before { background: var(--muted-foreground); }\n /* A review in flight (review fixes): the box reads neutral \u2014 neither the\n green of a settled verdict nor the red of a problem \u2014 with the dot in\n the review's own tone. */\n .dispatch-status[data-work-status=\"reviewing\"], .dispatch-status[data-work-status=\"review-pending\"],\n .dispatch-status[data-work-status=\"review-failed\"], .dispatch-status[data-work-status=\"review-exhausted\"] {\n color: var(--foreground); border-color: var(--glass-border); background: var(--glass);\n }\n .dispatch-status[data-work-status=\"reviewing\"] .dispatch-copy > strong::before, .dispatch-status[data-work-status=\"review-pending\"] .dispatch-copy > strong::before,\n .dispatch-status[data-work-status=\"review-failed\"] .dispatch-copy > strong::before, .dispatch-status[data-work-status=\"review-exhausted\"] .dispatch-copy > strong::before {\n content: \"\"; display: inline-block; width: .45rem; height: .45rem; margin-right: .45rem; border-radius: 50%; vertical-align: .08rem; background: var(--warning);\n }\n .dispatch-status[data-work-status=\"reviewing\"] .dispatch-copy > strong::before { background: var(--running); }\n .review-retry { margin-bottom: .65rem; }\n .task-control { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .6rem .9rem; margin: .65rem 0; padding: .8rem .9rem; }\n .task-control-copy { display: flex; flex-direction: column; gap: .15rem; min-width: 0; flex: 1 1 16rem; }\n .task-control-copy strong { display: flex; align-items: center; gap: .4rem; overflow-wrap: anywhere; }\n .task-control-copy .meta { overflow-wrap: anywhere; }\n .task-control-form { margin: 0; flex: none; }\n .task-control-button { width: auto; min-width: 7.5rem; white-space: nowrap; }\n .task-control-button.primary { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n .task-control-button[disabled] { opacity: .65; cursor: not-allowed; }\n .task-control[data-task-control=\"stopping\"] { border-color: color-mix(in srgb, var(--destructive) 45%, var(--glass-border)); }\n .task-control[data-task-control=\"stopping\"] .live-dot { background: var(--destructive); }\n .task-control[data-task-control=\"paused\"] .eyebrow { color: var(--destructive); }\n .resume-ceremony .row { margin: .3rem 0; }\n @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%; } }\n .review-attempts { margin: .55rem 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: .2rem; font-size: .8125rem; }\n .review-attempts li { display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .45rem; min-width: 0; }\n .review-attempts .review-attempt-ordinal { font-weight: 600; }\n .review-attempts .meta { min-width: 0; overflow-wrap: anywhere; }\n .review-retry-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .55rem; margin-top: .65rem; }\n .review-retry-actions form.inline { margin: 0; }\n .review-retry-button { width: auto; max-width: 100%; white-space: normal; }\n .review-retry-form .review-retry-button { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n .review-retry-button[disabled] { opacity: .65; cursor: not-allowed; }\n .proof-review-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .55rem; margin: .65rem 0; }\n .proof-review-actions > .button-link { flex: none; }\n details.proof-exception { margin: 0; }\n details.proof-exception > summary { cursor: pointer; color: var(--muted-foreground); font-size: .78rem; font-weight: 600; }\n details.proof-exception[open] { flex: 1 1 100%; padding: .75rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--glass); }\n details.proof-exception[open] > summary { margin-bottom: .6rem; color: var(--foreground); }\n .proof-exception-form { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; }\n .proof-exception-form input { margin: 0; min-width: 12rem; }\n .proof-exception-form button[type=submit] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n .dispatch-proof-details { margin: .65rem 0 .85rem; border: 1px solid var(--glass-border); border-radius: var(--radius); background: var(--glass); }\n .dispatch-proof-details > summary { padding: .75rem .9rem; cursor: pointer; color: var(--muted-foreground); font-size: .78rem; font-weight: 600; }\n .dispatch-proof-body { padding: 0 .9rem .85rem; border-top: 1px solid var(--glass-border); }\n details.dispatch-recovery { margin-top: .65rem; border: 0; padding: 0; background: transparent; box-shadow: none; }\n details.dispatch-recovery > summary {\n display: inline-flex; align-items: center; min-height: 2.25rem; padding: 0 .875rem; list-style: none;\n border: 1px solid var(--primary); border-radius: calc(var(--radius) - 2px); cursor: pointer;\n background: var(--primary); color: var(--primary-foreground); font-size: .8125rem; font-weight: 600;\n }\n details.dispatch-recovery > summary::-webkit-details-marker { display: none; }\n .dispatch-recovery-body { margin-top: .65rem; padding: .7rem .75rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--glass); color: var(--foreground); }\n .dispatch-recovery-body p { margin: 0; }\n .dispatch-recovery-body p + p { margin-top: .5rem; }\n .dispatch-recovery-command { display: block; margin-top: .45rem; padding: .55rem .65rem; overflow-wrap: anywhere; border-radius: .5rem; background: var(--muted); }\n .dispatch-status[data-dispatch-status=\"no-worker-registered\"],\n .dispatch-status[data-dispatch-status=\"no-worker-online\"] {\n color: var(--foreground); border-color: color-mix(in srgb, var(--warning) 30%, var(--border));\n background: color-mix(in srgb, var(--warning-soft) 48%, var(--glass));\n }\n .dispatch-status[data-dispatch-status=\"no-worker-registered\"] .dispatch-copy > strong,\n .dispatch-status[data-dispatch-status=\"no-worker-online\"] .dispatch-copy > strong { color: color-mix(in srgb, var(--warning) 72%, var(--foreground)); }\n .builder-notice { border-color: color-mix(in srgb, var(--warning) 30%, var(--border)); background: color-mix(in srgb, var(--warning-soft) 48%, var(--glass)); }\n .builder-notice strong { color: color-mix(in srgb, var(--warning) 72%, var(--foreground)); }\n .dispatch-status[data-dispatch-status=\"terminal-dependency\"] {\n color: var(--foreground); border-color: color-mix(in srgb, var(--warning) 42%, var(--border));\n background: color-mix(in srgb, var(--warning-soft) 72%, var(--glass));\n }\n .dispatch-status[data-dispatch-status=\"terminal-dependency\"] .dispatch-copy > strong { color: var(--warning); }\n .dispatch-status[data-dispatch-status=\"waiting-dependency\"] {\n color: var(--foreground); border-color: var(--glass-border); background: var(--glass);\n }\n .dependency-repair-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; margin-top: .7rem; }\n .dependency-repair-help { flex: 1 0 100%; margin: 0 0 .1rem; }\n .dependency-repair-actions form { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; max-width: 100%; margin: 0; }\n .dependency-repair-label { flex: 1 1 16rem; min-width: 0; margin: 0; color: var(--foreground); font-size: .75rem; }\n .dependency-repair-label select { display: block; margin: .3rem 0 0; }\n .dependency-repair-actions select { width: auto; max-width: 16rem; min-height: 2rem; margin: 0; font-size: .75rem; }\n .dependency-repair-actions button { min-height: 2rem; padding: .3rem .65rem; font-size: .75rem; }\n .approve-form { margin: .75rem 0; }\n .approve-form .ceremony-head { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; margin: 0 0 .5rem; }\n .approve-form .ceremony-head a { font-size: .8125rem; color: var(--muted-foreground); white-space: nowrap; }\n .approve-form .recap { margin: .125rem 0 .5rem; }\n .approval-card { padding: 1.2rem 1.3rem; border-radius: calc(var(--radius) + 3px); }\n .approval-card .ceremony-head { align-items: flex-start; padding-bottom: .85rem; border-bottom: 1px solid var(--glass-border); }\n .approval-title { display: grid; gap: .15rem; }\n .approval-kicker { color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }\n .approval-title strong { font-size: 1.05rem; letter-spacing: -.02em; }\n .approval-label { display: block; margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500 12px/1.4 var(--font-sans); }\n .approval-label::first-letter, .planner-plan .eyebrow::first-letter { text-transform: uppercase; }\n .planner-plan .eyebrow { font: 500 12px/1.4 var(--font-sans); letter-spacing: 0; text-transform: none; }\n .approval-goal { margin: 0; font-size: 1rem; line-height: 1.55; white-space: pre-wrap; }\n .approval-boundaries { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-top: .75rem; }\n .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); }\n .approval-boundary .approval-label { margin: 0 0 .2rem; }\n .approval-boundary p { margin: 0; color: var(--muted-foreground); font-size: .8rem; overflow-wrap: anywhere; }\n .approval-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0 .3rem; }\n /* The filed contract (contract handoff, task 1): preserved, amended, or\n drafted from nothing \u2014 the same panel on the task page, in the\n ceremony, and on /next, so an amendment is never a surprise after the\n yes. An amendment is the one state that asks for a second look. */\n .contract-panel { margin-top: .8rem; padding: .7rem .85rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--muted) 45%, transparent); }\n .contract-panel .approval-label { margin: 0 0 .25rem; }\n .contract-panel p { margin: .2rem 0; }\n .contract-amended { border-color: color-mix(in srgb, var(--warning, #c98a1b) 55%, var(--glass-border)); background: color-mix(in srgb, var(--warning, #c98a1b) 9%, transparent); }\n .contract-reason { font-size: .9rem; }\n .contract-changes { margin: .5rem 0 0; padding-left: 1.1rem; display: grid; gap: .45rem; }\n .contract-changes li { overflow-wrap: anywhere; }\n .contract-change-kind { display: inline-block; margin-right: .3rem; padding: .05rem .4rem; border-radius: 999px; font: 600 .62rem/1.5 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; background: color-mix(in srgb, var(--muted) 70%, transparent); }\n .contract-change-added { background: color-mix(in srgb, var(--success, #2f9e5f) 18%, transparent); }\n .contract-change-removed { background: color-mix(in srgb, var(--danger, #c8453d) 18%, transparent); }\n .contract-change-changed { background: color-mix(in srgb, var(--warning, #c98a1b) 22%, transparent); }\n .contract-before, .contract-after { margin-top: .15rem; font-size: .85rem; }\n .contract-before { color: var(--muted-foreground); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--muted-foreground) 55%, transparent); }\n .contract-note { margin-top: .5rem; }\n /* The agents (v47): one compact summary, rendered identically on the task\n page, in the approval ceremony, and in the focused chat; reasons and\n change controls stay closed until asked for. */\n .agents-card { margin-top: .75rem; }\n .agents-head { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .6rem; }\n .agents-head h3 { margin: 0; font-size: .95rem; letter-spacing: -.02em; }\n .agents-badges { display: flex; flex-wrap: wrap; gap: .3rem; }\n .agents-summary { margin: .55rem 0 0; font-size: .86rem; line-height: 1.5; overflow-wrap: anywhere; }\n .agents-availability-label { margin: .6rem 0 .15rem; }\n .agents-availability { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .25rem .9rem; font-size: .8125rem; }\n .agents-availability li { overflow-wrap: anywhere; }\n .agents-availability-unavailable { color: var(--destructive); font-weight: 600; }\n .agents-availability-ready { color: var(--success); }\n .agents-availability-unknown { color: var(--muted-foreground); }\n .agents-halted { margin: .6rem 0 0; padding: .55rem .7rem; border: 1px solid color-mix(in srgb, var(--destructive) 40%, var(--border)); border-radius: calc(var(--radius) - 3px); background: var(--destructive-soft); font-size: .8rem; }\n .agents-problems { margin: .5rem 0 0; padding-left: 1.1rem; color: var(--destructive); font-size: .78rem; }\n .agents-why, .agents-change { margin-top: .65rem; }\n .agents-why > summary, .agents-change > summary { display: flex; align-items: center; min-height: 2.75rem; padding: .35rem .2rem; cursor: pointer; font-size: .8rem; font-weight: 600; }\n .agents-demands { margin: .35rem 0 0; padding-left: 1.1rem; color: var(--muted-foreground); font-size: .76rem; line-height: 1.45; }\n .agents-roles { display: grid; grid-template-columns: 5rem minmax(0, 1fr); gap: .45rem .6rem; margin: .5rem 0 0; }\n .agents-roles dt { color: var(--muted-foreground); font: 500 .66rem/1.8 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }\n .agents-roles dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }\n .agents-roles .agents-reasons { margin: .2rem 0 0; padding-left: 1rem; color: var(--muted-foreground); font-size: .74rem; line-height: 1.45; }\n .agents-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .5rem; align-items: end; margin: 0; }\n .agents-form label, .agents-form-risk label { display: grid; gap: .2rem; margin: 0; min-width: 0; font-size: .74rem; }\n .agents-form input, .agents-form select, .agents-form-risk select { width: 100%; min-width: 0; min-height: 2.75rem; }\n .agents-form button, .agents-form-risk button, .agents-clear button { min-height: 2.75rem; }\n .agents-form-risk { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; margin-top: .5rem; }\n .agents-form-risk label.agents-risky { display: flex; align-items: center; gap: .4rem; min-height: 2.75rem; font-size: .85rem; }\n .agents-overrides { list-style: none; margin: .5rem 0 0; padding: 0; display: grid; gap: .35rem; font-size: .78rem; }\n .agents-overrides li { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .4rem .6rem; }\n .agents-clear { margin: 0; }\n .agents-clear button { padding-inline: .8rem; font-size: .72rem; }\n .agents-ceremony { margin: .5rem 0 0; }\n .agents-ceremony .agents-badges { margin-top: .35rem; }\n .agents-risk-line { margin: .4rem 0 0; }\n .agents-runtime { margin-top: .5rem; }\n .agents-runtime > summary { display: flex; align-items: center; min-height: 2.75rem; padding: .35rem .2rem; cursor: pointer; font-size: .8rem; font-weight: 600; }\n .agents-risk-guide { display: grid; gap: .3rem; margin: .5rem 0 0; font-size: .76rem; line-height: 1.45; }\n .agents-risk-guide div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .5rem; }\n .agents-risk-guide dt { color: var(--muted-foreground); font: 500 .66rem/1.8 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }\n .agents-risk-guide dd { margin: 0; color: var(--muted-foreground); overflow-wrap: anywhere; }\n .agents-role-forms { display: grid; gap: .45rem; margin-top: .6rem; }\n .agents-role-row { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .5rem; align-items: center; }\n .agents-role-name { color: var(--muted-foreground); font: 500 .66rem/1.8 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }\n .task-chat-agents { display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .55rem; margin: .1rem 0 .2rem; padding: .55rem .75rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--muted) 45%, transparent); font-size: .78rem; line-height: 1.45; overflow-wrap: anywhere; }\n .task-chat-agents a { white-space: nowrap; }\n .task-chat-agents-aside { margin-top: .85rem; }\n .task-chat-agents-aside .agents-summary { font-size: .74rem; }\n .task-chat-agents-aside .agents-badges { margin-top: .35rem; }\n .task-chat-agents-aside .agents-roles { grid-template-columns: 1fr; }\n .approval-chip { padding: .25rem .6rem; border: 1px solid var(--glass-border); border-radius: 999px; color: var(--muted-foreground); background: var(--glass); font-size: .7rem; }\n .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); }\n .approval-confirm label { margin: 0; }\n .approval-card .approval-confirm button { min-height: 44px; white-space: nowrap; }\n /* The ceremony's orientation and exact-terms group (UI polish 2026-09-13). */\n /* Neutral by law: magenta belongs to the count and the approve act alone. */\n .approval-orient { margin: .85rem 0 .25rem; }\n .approval-terms { scroll-margin-top: 5rem; }\n .approval-confirm { scroll-margin-top: 5rem; }\n .approval-paths { margin: 0; padding: 0; list-style: none; display: grid; gap: .15rem; }\n .approval-paths li { color: var(--muted-foreground); font-size: .8rem; overflow-wrap: anywhere; }\n /* The approval sheet (approval critique, Oct 2): the plan open in plain\n rows, who builds in one line, one Approve & start beside the password,\n and every other signed term in one Details fold at the end. Sentence\n case, Geist, 12px and up; prose capped at 75ch. */\n .approval-sheet { display: grid; gap: 16px; margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }\n .approval-sheet.approve-form > :not(input) { margin: 0; }\n .approval-sheet-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: -.01em; }\n .approval-revision, .approval-note, .approval-race { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }\n .approval-note { color: var(--muted-foreground); }\n .approval-rows { display: grid; gap: 14px; margin: 0; }\n .approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 16px; }\n .approval-row dt { margin: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }\n .approval-row dd { min-width: 0; max-width: 75ch; margin: 0; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }\n .approval-row dd p, .approval-sheet .approval-goal { margin: 0; font-size: 14px; line-height: 1.6; }\n .approval-row dd :is(ol, ul) { display: grid; gap: 4px; margin: 0; padding-left: 1.25rem; }\n .approval-row dd ol + p { margin-top: 6px; }\n .approval-row dd .approval-paths { margin-top: 2px; padding-left: 0; }\n .approval-row dd .approval-paths li { font-size: 13px; }\n .approval-amendment { max-width: 75ch; padding: 12px 14px; border-radius: 8px; background: var(--muted); font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }\n .approval-amendment p { margin: 0; }\n .approval-amendment ul { display: grid; gap: 4px; margin: 6px 0; padding-left: 1.25rem; }\n .agents-size { margin: 0; font-size: 13px; line-height: 1.5; }\n .approval-who { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }\n .approval-act { display: grid; grid-template-columns: minmax(0, 20rem) auto; justify-content: start; align-items: center; gap: 8px; }\n .approval-password { margin: 0; }\n .approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }\n .approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }\n .approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }\n .approval-allowing, .approval-you-check { max-width: 75ch; font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }\n .approval-sheet .approval-allowing + .approval-you-check { margin-top: -10px; }\n .approval-act > button { grid-column: 2; grid-row: 1; }\n .approval-password-note { grid-column: 1; grid-row: 2; margin: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }\n .approval-secondary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }\n details.approval-edit, details.approval-edit[open] { margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }\n /* Edit plan, in place: the rows become fields where they stood; the\n approval act and Details step aside until it is saved or cancelled. */\n .approval-sheet { position: relative; }\n .approval-edit > summary { list-style: none; cursor: pointer; }\n .approval-edit > summary::-webkit-details-marker { display: none; }\n .approval-edit-close, .approval-edit[open] .approval-edit-open { display: none; }\n .approval-edit[open] .approval-edit-close { display: inline; }\n .approval-edit[open] > summary { position: absolute; top: 14px; right: 20px; }\n .approval-sheet:has(.approval-edit[open]) > :not(.approval-sheet-title, .approval-secondary) { display: none; }\n .approval-secondary:has(> .approval-edit[open]) { display: block; }\n .approval-secondary:has(> .approval-edit[open]) > a { display: none; }\n .approval-editor { display: grid; gap: 14px; }\n .approval-editor .problem { margin: 0; }\n .approval-field { display: grid; gap: 4px; min-width: 0; margin: 0; padding: 0; border: 0; }\n .approval-field-label { padding: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }\n .approval-field-hint { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }\n .approval-field textarea, .approval-field input[type=text] { width: 100%; max-width: 75ch; margin: 0; font-family: var(--font-sans); font-size: 14px; line-height: 1.55; }\n .approval-requirements { display: grid; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }\n .approval-edit-act { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }\n .approval-edit-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }\n .approval-link { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font-size: 13px; font-weight: 500; text-decoration: none; }\n .approval-secondary > .approval-link:not(:first-child) { border-color: transparent; background: none; color: var(--muted-foreground); }\n @media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }\n details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }\n details.approval-details[open] { padding-bottom: 0; }\n .approval-details > summary { display: flex; align-items: center; min-height: 40px; padding: 4px 0 0; font-size: 13px; }\n .approval-detail { max-width: 75ch; margin: 12px 0 0; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }\n .approval-detail h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600; letter-spacing: 0; text-transform: none; }\n .approval-detail p, .approval-detail ul, .approval-detail dl { margin: 4px 0; }\n .approval-detail ul { padding-left: 1.25rem; }\n .approval-detail .revision-card { margin: 0; padding: 0; border: 0; background: none; }\n .approval-roles { display: grid; gap: 8px; }\n .approval-roles > div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: 12px; }\n .approval-roles dt { color: var(--muted-foreground); font-weight: 500; }\n .approval-roles dd { margin: 0; }\n .approval-roles dd ul { margin: 2px 0 0; color: var(--muted-foreground); }\n .chat-plan .approval-sheet { padding: 0; border: 0; background: none; }\n @media (max-width: 760px) {\n .approval-sheet { gap: 12px; padding: 14px; }\n .approval-row { grid-template-columns: 1fr; gap: 2px; }\n .approval-act { grid-template-columns: 1fr; }\n .approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }\n .approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }\n .approval-act button { width: 100%; }\n /* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */\n .approval-sheet[data-sticky] .approval-act {\n position: sticky; bottom: 0; z-index: 20; margin: 0 -14px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));\n border-top: 1px solid var(--border); background: var(--card);\n }\n .approval-link { min-height: 44px; padding: 0 16px; }\n .approval-edit[open] > summary { top: 6px; right: 6px; }\n .approval-field textarea, .approval-field input[type=text] { font-size: 16px; }\n .approval-edit-act button { width: 100%; min-height: 44px; font-size: 16px; }\n .approval-details > summary { min-height: 44px; }\n .approval-roles > div { grid-template-columns: 1fr; gap: 2px; }\n }\n .run-facts-details { margin-top: 1.25rem; }\n .run-facts-details > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }\n .run-facts-details > summary .meta { font-size: .75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .task-main-title, .chat-project-name strong, .proposal h3 { overflow-wrap: anywhere; }\n .planner-plan { margin-top: .75rem; padding: 1.15rem 1.2rem; overflow: hidden; background: color-mix(in srgb, var(--glass) 82%, transparent); }\n .execution-plan-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: .95rem; }\n .execution-plan-head h2 { margin: .15rem 0 0; font-size: 1.05rem; letter-spacing: -.025em; }\n .execution-plan-head p { margin: .25rem 0 0; max-width: 38rem; }\n details.planner-plan-collapsed { padding: 0; }\n .planner-plan-collapsed > summary.execution-plan-head {\n position: relative; list-style: none; align-items: center; min-height: 4.25rem; margin: 0; padding: .9rem 3rem .9rem 1.1rem; cursor: pointer;\n }\n .planner-plan-collapsed > summary::-webkit-details-marker { display: none; }\n .planner-plan-collapsed > summary::after {\n content: \"\"; position: absolute; right: 1.2rem; top: 50%; width: .45rem; height: .45rem;\n border-right: 1.5px solid var(--muted-foreground); border-bottom: 1.5px solid var(--muted-foreground);\n transform: translateY(-65%) rotate(45deg); transition: transform .15s ease;\n }\n .planner-plan-collapsed[open] > summary::after { transform: translateY(-35%) rotate(225deg); }\n .planner-plan-collapsed[open] > summary { border-bottom: 1px solid var(--glass-border); }\n .planner-plan-body { padding: 1rem 1.15rem 1.15rem; }\n .plan-lock { flex: none; padding: .3rem .6rem; border: 1px solid var(--glass-border); border-radius: 999px; color: var(--muted-foreground); background: color-mix(in srgb, var(--muted) 50%, transparent); font-size: .7rem; }\n .execution-plan { display: grid; gap: .75rem; }\n .execution-plan p { margin: .2rem 0 0; line-height: 1.55; white-space: pre-wrap; }\n .execution-plan ol, .execution-plan ul { display: grid; gap: .45rem; margin: .35rem 0 0; padding-left: 1.3rem; }\n .execution-plan li { padding-left: .15rem; line-height: 1.45; overflow-wrap: anywhere; }\n .execution-milestones { padding: .85rem .9rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 2px); background: color-mix(in srgb, var(--muted) 36%, transparent); }\n .execution-milestones li::marker { color: var(--muted-foreground); font: 500 .7rem var(--font-mono); }\n .execution-support { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; }\n .execution-support > div, .execution-proof { padding: .8rem .85rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--glass) 74%, transparent); }\n .execution-proof li::marker { content: \"\u2713 \"; color: var(--running); }\n .execution-plan-compact { gap: .55rem; margin-top: .4rem; }\n .execution-plan-compact .execution-support { grid-template-columns: 1fr; }\n .plan-editor { margin-top: .9rem; padding-top: .75rem; border-top: 1px solid var(--glass-border); }\n .plan-editor > summary { width: fit-content; color: var(--muted-foreground); cursor: pointer; font-size: .8rem; font-weight: 600; }\n .plan-editor form { margin-top: .7rem; }\n .plan-editor textarea { width: 100%; font-family: var(--font-mono); font-size: .75rem; line-height: 1.55; }\n .milestone-progress-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }\n .milestone-progress-head h2 { margin: .15rem 0 0; }\n .milestone-progress-count { flex: none; padding: .28rem .58rem; border: 1px solid var(--glass-border); border-radius: 999px; color: var(--muted-foreground); background: color-mix(in srgb, var(--muted) 45%, transparent); font-size: .7rem; font-weight: 600; }\n .milestone-list { list-style: none; margin: .75rem 0 0; padding: 0; display: grid; gap: .4rem; }\n .milestone { display: flex; align-items: baseline; gap: .5rem; padding: .5rem .65rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--glass) 74%, transparent); overflow-wrap: anywhere; }\n .milestone-badge { display: inline-flex; align-items: center; gap: .35rem; flex: none; padding: .08rem .45rem; border: 1px solid var(--glass-border); border-radius: 999px; color: var(--muted-foreground); background: color-mix(in srgb, var(--muted) 36%, transparent); font-size: .67rem; font-weight: 600; letter-spacing: .01em; }\n .milestone-badge::before { content: \"\"; width: .38rem; height: .38rem; border-radius: 999px; background: color-mix(in srgb, var(--muted-foreground) 55%, transparent); }\n .milestone-current .milestone-badge { color: var(--foreground); }\n .milestone-current .milestone-badge::before { background: var(--running); box-shadow: 0 0 0 2px color-mix(in srgb, var(--running) 13%, transparent); }\n .milestone-completed .milestone-badge::before { background: var(--success, var(--running)); }\n .milestone-blocked .milestone-badge::before { background: var(--warning); }\n .milestone-progress-note { margin: .7rem .1rem 0; }\n .plan-revision-pending { margin-top: .85rem; padding: .85rem .9rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 2px); background: color-mix(in srgb, var(--running-soft) 30%, transparent); }\n .plan-revision-pending h3 { margin: .15rem 0 .35rem; font-size: 1rem; letter-spacing: -.02em; }\n .plan-revision-pending > p { margin: .3rem 0; }\n .plan-revision-pending form { margin-top: .6rem; display: inline-flex; gap: .4rem; align-items: center; }\n .plan-revision-pending form + form { margin-left: .5rem; }\n .plan-revision > h2 { margin: .15rem 0 .35rem; color: var(--foreground); font-size: 1rem; letter-spacing: -.02em; }\n .plan-revision > p.meta { margin-top: 0; }\n .plan-revision-history { margin-top: .85rem; }\n .plan-revision-history > summary { cursor: pointer; color: var(--muted-foreground); font-size: .8rem; font-weight: 600; }\n .planner-status { display: flex; gap: .8rem; align-items: flex-start; }\n .planner-orb { position: relative; flex: 0 0 2.15rem; width: 2.15rem; height: 2.15rem; border-radius: .75rem; background: var(--running-soft); }\n .planner-orb::after { content: \"\"; position: absolute; inset: .65rem; border-radius: 999px; background: var(--running); animation: pulse 1.25s ease-in-out infinite; }\n .planner-status p { margin: 0; }\n .planner-status .meta { display: block; margin-top: .2rem; }\n .ceremony-road { margin: .75rem 0 0; }\n .button-link {\n display: inline-flex; align-items: center; justify-content: center; min-height: 2.25rem; padding: 0 .875rem;\n border-radius: calc(var(--radius) - 2px); background: var(--primary); color: var(--primary-foreground);\n font-weight: 600; font-size: .8125rem; text-decoration: none; border: 1px solid var(--primary);\n }\n .props .row { padding: .4375rem .375rem; align-items: baseline; }\n .props .row .meta { flex: 0 0 6.5rem; }\n .props .row .mono { flex: 1 1 10rem; min-width: 0; overflow-wrap: anywhere; }\n .props .row .seal { margin-right: .375rem; }\n details.section { border: 0; background: transparent; padding: 0; margin: 1.25rem 0 0; border-radius: 0; box-shadow: none; }\n details.section[open] { padding-bottom: 0; }\n details.section > summary { list-style: none; display: flex; align-items: center; padding: .25rem 0; min-height: 2.25rem; }\n details.section > summary::-webkit-details-marker { display: none; }\n details.section > summary h2 { margin: 0; flex: 1; display: flex; align-items: center; gap: .4rem; }\n details.section > summary h2 .lane-count { border: 1px solid var(--border); border-radius: 999px; padding: .04rem .5rem; font-weight: 500; color: var(--muted-foreground); font-size: .6875rem; }\n details.section > summary::after {\n content: \"\"; width: .45rem; height: .45rem; flex: none; margin-right: .375rem;\n border-right: 1.5px solid var(--muted-foreground); border-bottom: 1.5px solid var(--muted-foreground);\n transform: rotate(45deg) translateY(-.125rem); transition: transform .15s;\n }\n details.section[open] > summary::after { transform: rotate(225deg) translateY(-.125rem); }\n @media (hover: hover) and (pointer: fine) { details.section > summary:hover h2 { color: var(--foreground); } }\n details.more-agents { margin: .75rem 0 0; }\n .task-layout > .task-main { min-width: 0; }\n .task-rail { position: sticky; top: 1rem; }\n .task-rail .card { margin-top: .75rem; }\n .task-rail .mono { overflow-wrap: anywhere; }\n main:has(.task-layout) { max-width: 76rem; }\n @media (max-width: 980px) {\n .task-layout { grid-template-columns: 1fr; }\n .cockpit { grid-template-columns: 1fr; }\n .cockpit-queue { position: static; padding-bottom: .75rem; margin-bottom: .5rem; border-bottom: 1px solid var(--border); }\n .cockpit-queue-list { max-height: 15rem; }\n .task-rail { position: static; order: -1; border-bottom: 1px solid var(--border); padding-bottom: .75rem; margin-bottom: .75rem; }\n .acts-bar form.inline, .acts-bar .primary, .acts-bar .primary form { flex: 1 1 auto; }\n .acts-bar .primary { flex-basis: 100%; }\n .acts-bar .primary button { width: 100%; }\n .acts-bar .act-hold { flex-wrap: wrap; }\n .acts-bar .act-hold input[type=text] { flex: 1 1 8rem; width: auto; }\n .dependency-repair-actions { align-items: stretch; }\n .dependency-repair-actions form { flex: 1 1 10rem; }\n .dependency-repair-actions .dependency-repair-replace { flex-basis: 100%; }\n .dependency-repair-actions select { flex: 1 1 auto; min-width: 0; max-width: none; }\n .dependency-repair-actions button { white-space: nowrap; }\n .split { grid-template-columns: 1fr; }\n .list-pane { display: none; }\n .workbench-mobile-rail, .workbench-mobile-back { display: block; }\n .workbench-mobile-rail { margin-top: 1.75rem; border-top: 1px solid var(--border); padding-top: .75rem; }\n .workbench-mobile-back { margin: 0 0 1rem; }\n }\n @media (max-width: 620px) {\n .planner-plan { padding: 1rem; }\n details.planner-plan-collapsed { padding: 0; }\n .planner-plan-collapsed > summary.execution-plan-head { padding: .8rem 2.5rem .8rem .9rem; }\n .planner-plan-collapsed > summary .plan-lock { display: none; }\n .planner-plan-body { padding: .85rem .9rem 1rem; }\n .execution-plan-head { flex-direction: column; align-items: flex-start; gap: .55rem; }\n .execution-support { grid-template-columns: 1fr; }\n .plan-lock { white-space: nowrap; }\n .milestone-progress-head { align-items: center; }\n .milestone-progress-count { padding-inline: .48rem; }\n .milestone { align-items: flex-start; padding: .55rem; }\n .plan-revision-pending form { display: flex; width: 100%; }\n .plan-revision-pending form + form { margin-left: 0; margin-top: .4rem; }\n .plan-revision-pending button { width: 100%; }\n }\n\n /* Work (workspace package 1): a quiet list of rows and dividers, never\n another grid of cards. The status line is the same component every\n surface renders for a run \u2014 a small dot in the tone, neutral words. */\n .work-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }\n .work-head h1 { margin-bottom: .25rem; }\n .work-tools { display: none; position: relative; flex: none; margin: .25rem 0 0; border: 0; padding: 0; background: none; border-radius: 0; }\n .app.sidebar-collapsed .work-tools { display: block; }\n .work-tools > summary {\n list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .3rem;\n min-height: 2.25rem; padding: 0 .75rem; border: 1px solid var(--border); border-radius: 999px;\n font-size: .8125rem; font-weight: 500; color: var(--foreground); background: var(--card);\n }\n .work-tools > summary::-webkit-details-marker { display: none; }\n .work-tools > summary .chevron { width: .875rem; height: .875rem; }\n .work-tools[open] > summary .chevron { transform: rotate(180deg); }\n .work-tools-menu {\n position: absolute; right: 0; top: calc(100% + .375rem); z-index: 20; min-width: 11rem; max-width: calc(100vw - 2rem);\n display: flex; flex-direction: column; padding: .375rem; border: 1px solid var(--border); border-radius: calc(var(--radius) - 2px);\n background: var(--card); box-shadow: var(--shadow);\n }\n .work-tools-menu a { display: block; padding: .5rem .625rem; border-radius: .5rem; text-decoration: none; color: var(--foreground); font-size: .875rem; }\n @media (hover: hover) and (pointer: fine) { .work-tools-menu a:hover { background: var(--muted); } }\n /* The result panel (workspace package 3): one presentation of a finished\n result for the run page, the review cockpit, and the chat's result\n detail \u2014 the deliverable first, problems ahead of readiness words,\n three local views, and Request changes beside it. */\n .result-panel { position: relative; padding: 1.1rem 1.2rem 1rem; min-width: 0; }\n .result-back { margin: 0 0 .4rem; font-size: .8125rem; }\n /* A comfortable target at every width (repair 2026-09-14): 44px tall. */\n .result-back a { display: inline-flex; align-items: center; min-height: 2.75rem; padding: 0 .35rem; margin-left: -.35rem; border-radius: .375rem; text-decoration: none; font-weight: 550; }\n .result-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }\n .result-head h2 { margin: .15rem 0 0; font-size: 1.15rem; }\n .result-head .eyebrow { overflow-wrap: anywhere; }\n .result-head .status-line { flex: none; font-size: .8125rem; }\n .result-summary { max-width: 46rem; margin: .6rem 0 0; font-size: .95rem; line-height: 1.5; }\n .result-action { margin: .7rem 0 0; }\n .result-action form { margin: 0; }\n .result-action .button-link { min-height: 2.5rem; gap: .35rem; }\n .result-action .button-link .meta { color: inherit; opacity: .78; font-weight: 500; }\n .result-notes, .result-details, .result-notes[open], .result-details[open] { margin: .6rem 0 0; padding: 0; border: 0; background: none; box-shadow: none; }\n .result-notes > summary, .result-details > summary {\n list-style: none; cursor: pointer; display: flex; align-items: center; gap: .5rem; min-height: 2.5rem; padding: 0 .25rem; margin: 0 -.25rem;\n border-radius: .375rem; font-size: .8125rem; font-weight: 550; color: var(--muted-foreground);\n }\n .result-notes > summary::-webkit-details-marker, .result-details > summary::-webkit-details-marker { display: none; }\n .result-notes > summary::before, .result-details > summary::before { content: \"\u25B8\"; }\n .result-notes[open] > summary::before, .result-details[open] > summary::before { content: \"\u25BE\"; }\n @media (hover: hover) and (pointer: fine) { .result-notes > summary:hover, .result-details > summary:hover { color: var(--foreground); } }\n .result-details > summary .meta { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: .68rem; font-weight: 400; }\n .result-notes .recap { margin: .25rem 0 .4rem; font-size: .875rem; }\n .result-attention { margin: .75rem 0 .25rem; padding: .7rem .85rem; border-left: 1px solid var(--warning); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: var(--warning-soft); font-size: .8125rem; }\n .result-attention strong { display: block; font-size: .78rem; }\n .result-you-check { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem .75rem; margin: .75rem 0; padding: .65rem .85rem; border-radius: calc(var(--radius) - 3px); background: var(--muted); font-size: .8125rem; }\n .result-you-check ul { flex: 1 1 14rem; min-width: 0; margin: 0; padding: 0; list-style: none; }\n .result-you-check form { margin: 0; }\n .result-attention ul { margin: .3rem 0 0; padding-left: 1.15rem; }\n .result-attention li { margin: .15rem 0; overflow-wrap: anywhere; }\n .result-tabs { display: flex; gap: .25rem; margin: .9rem 0 .75rem; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }\n .result-tabs::-webkit-scrollbar { display: none; }\n .result-tabs a {\n display: inline-flex; align-items: center; gap: .4rem; flex: none; min-height: 2.5rem; padding: 0 .625rem;\n margin-bottom: -1px; border-bottom: 2px solid transparent; text-decoration: none;\n color: var(--muted-foreground); font-size: .875rem; font-weight: 500;\n }\n .result-tabs a .count { font-family: var(--font-mono); font-size: .6875rem; font-variant-numeric: tabular-nums; color: var(--muted-foreground); }\n .result-tabs a[aria-selected=\"true\"] { color: var(--foreground); border-bottom-color: var(--foreground); }\n .result-tabs a[aria-selected=\"true\"] .count { color: var(--foreground); }\n .result-view { min-width: 0; }\n .result-view[hidden] { display: none; }\n .result-visuals { margin-top: .25rem; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); }\n .result-unavailable { margin: .5rem 0 0; padding-left: 1.15rem; font-size: .78rem; }\n .result-changes { margin: .25rem 0 .5rem; padding-left: 1.15rem; font-size: .875rem; }\n .result-changes li { margin: .2rem 0; overflow-wrap: anywhere; }\n .result-files li, .result-files .mono { overflow-wrap: anywhere; white-space: normal; }\n .result-files li { min-width: 0; }\n .result-section[data-cockpit-source=\"reviewer\"] li { overflow-wrap: anywhere; }\n @media (max-width: 760px) { .result-panel .pick-file, .result-panel .pick-line, .diff-modes button { min-height: 44px; min-width: 44px; white-space: nowrap; } }\n .result-files-lead { margin: .25rem 0 .5rem; font-size: .8125rem; overflow-wrap: anywhere; }\n .result-report h3 { margin: .2rem 0 .3rem; font-size: 1rem; }\n .result-report .plan-doc { max-height: 28rem; overflow: auto; }\n .result-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: .5rem; margin: .4rem 0 0; }\n .result-facts > div { min-width: 0; padding: .6rem .7rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--glass-strong) 64%, transparent); }\n .result-facts dt { color: var(--muted-foreground); font: 500 .75rem/1.3 var(--font-sans); }\n .result-facts dd { margin: .2rem 0 0; font-size: .78rem; line-height: 1.4; overflow-wrap: anywhere; }\n .result-facts dd p { margin: 0; }\n .result-facts dd form { margin: .35rem 0 0; }\n .result-stat { margin: .2rem 0 .4rem; }\n .result-files { margin: .4rem 0 .6rem; }\n .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; }\n .result-verdict { align-items: center; }\n .result-verdict > .meta { min-width: 0; overflow-wrap: anywhere; }\n .result-request { margin-top: 1.1rem; padding-top: .9rem; border-top: 1px solid var(--border); }\n .result-request h3 { margin: 0 0 .5rem; font-size: .8rem; letter-spacing: .02em; text-transform: uppercase; color: var(--muted-foreground); }\n .result-request .diff-comment-form { margin: .5rem 0 0; padding: 0; border: 0; background: none; box-shadow: none; }\n .result-request .diff-comment-form textarea { width: 100%; box-sizing: border-box; }\n .result-pin, .result-pin[open] { margin: 0; padding: 0 .2rem; border: 0; background: none; }\n .result-pin > summary { min-height: 2.25rem; display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; font-size: .78rem; font-weight: 550; cursor: pointer; list-style: none; }\n .result-pin > summary::-webkit-details-marker { display: none; }\n .result-pin > summary::before { content: \"\u25B8\"; }\n .result-pin[open] > summary::before { content: \"\u25BE\"; }\n .result-pin > summary .meta { font-weight: 400; }\n .result-pin .diff-comment-target { margin: .25rem 0 .5rem; }\n .revision-card, .revision-lineage, .task-history, .diff-comment p { min-width: 0; overflow-wrap: anywhere; }\n .result-feedback-history > summary, .task-history > summary, .task-history a { display: inline-flex; align-items: center; min-height: 44px; padding: .25rem .5rem; }\n .task-history > summary::before { content: \"\u25B8\"; margin-right: .35rem; }\n .task-history[open] > summary::before { content: \"\u25BE\"; }\n .task-history li { margin: .25rem 0; }\n .result-revision { margin: .6rem 0; padding: .6rem .75rem; border: 1px solid color-mix(in srgb, var(--running) 24%, var(--glass-border)); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--running) 6%, var(--glass-strong)); font-size: .8125rem; overflow-wrap: anywhere; }\n .result-links { display: flex; flex-wrap: wrap; gap: .25rem .5rem; margin: .6rem 0 0; }\n .result-links a { font-weight: 550; }\n .result-links a + a::before { content: \"\u00B7\"; margin-right: .5rem; color: var(--muted-foreground); font-weight: 400; }\n .cockpit-detail .result-panel { margin: .85rem 0; }\n .cockpit-accept { margin-top: .75rem; }\n /* The result beside the conversation (package 3): with room, two\n columns and no second auxiliary panel; without it, the dedicated\n result view with Back to chat and the conversation out of the way. */\n .chat-result { min-width: 0; }\n .task-chat-workspace.result-open { grid-template-columns: minmax(0, 30rem) minmax(0, 1fr); align-items: start; }\n .task-chat-workspace.result-open .chat-main { max-width: none; }\n /* Said once: while the result detail is open beside the conversation,\n the receipt in the thread keeps its heading, status, and roads only. */\n .result-open .completion-receipt > :not(.receipt-head):not(.receipt-actions) { display: none; }\n @media (max-width: 1199px) {\n .task-chat-workspace.result-open { display: block; }\n .task-chat-workspace.result-open .chat-main, .task-chat-workspace.result-open .task-chat-agents { display: none; }\n main:has(.chat-workspace.result-open) { padding-bottom: calc(5rem + env(safe-area-inset-bottom, 0rem)); }\n }\n @media (max-width: 359px) {\n .result-tabs a .count { display: none; }\n .result-panel { padding-inline: .7rem; }\n }\n .work-views { display: flex; gap: .25rem; margin: .75rem 0 .5rem; border-bottom: 1px solid var(--border); overflow-x: auto; scrollbar-width: none; }\n .work-views::-webkit-scrollbar { display: none; }\n .work-views a {\n display: inline-flex; align-items: center; gap: .4rem; flex: none; min-height: 2.5rem; padding: 0 .625rem;\n margin-bottom: -1px; border-bottom: 2px solid transparent; text-decoration: none;\n color: var(--muted-foreground); font-size: .875rem; font-weight: 500;\n }\n .work-views a .count { font-family: var(--font-mono); font-size: .6875rem; font-variant-numeric: tabular-nums; color: var(--muted-foreground); }\n .work-views a.active { color: var(--foreground); border-bottom-color: var(--foreground); }\n .work-views a.active .count { color: var(--foreground); }\n .work-list { display: flex; flex-direction: column; }\n .work-row {\n display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: .5rem 1.5rem; align-items: start;\n padding: .75rem 0; border-bottom: 1px solid var(--border);\n }\n .work-row:last-child { border-bottom: 0; }\n .work-row-main, .work-row-status { min-width: 0; }\n /* Concise rows (2026-09-13): the status, the next act, and the Details\n toggle share one wrapping line; an opened disclosure is wider than\n its summary, so it wraps onto its own full-width line by itself. */\n .work-row-status { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .875rem; }\n .work-details, .work-details[open] { max-width: 100%; margin: 0; padding: 0; border: 0; background: none; border-radius: 0; }\n .work-details > summary {\n list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .25rem;\n min-height: 2.25rem; padding: 0 .25rem; margin: 0 -.25rem; border-radius: .375rem;\n font-size: .8125rem; font-weight: 500; color: var(--muted-foreground);\n }\n .work-details > summary::-webkit-details-marker { display: none; }\n .work-details > summary::before { content: \"\u25B8\"; }\n .work-details[open] > summary::before { content: \"\u25BE\"; }\n @media (hover: hover) and (pointer: fine) { .work-details > summary:hover { color: var(--foreground); } }\n .work-title { display: block; font-weight: 600; font-size: .9375rem; line-height: 1.35; color: var(--foreground); text-decoration: none; overflow-wrap: anywhere; }\n @media (hover: hover) and (pointer: fine) { .work-title:hover { text-decoration: underline; } }\n .work-meta { display: flex; flex-wrap: wrap; gap: .25rem .625rem; margin: .25rem 0 0; font-size: .75rem; color: var(--muted-foreground); }\n .work-meta .mono { overflow-wrap: anywhere; }\n .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); }\n .work-detail { margin: 0 0 .25rem; font-size: .8125rem; line-height: 1.45; color: var(--muted-foreground); overflow-wrap: anywhere; }\n .work-details .work-id { margin: 0 0 .25rem; }\n .work-action { display: inline-flex; align-items: center; min-height: 44px; font-size: .8125rem; font-weight: 500; }\n .work-empty { padding: 2rem 0 1rem; max-width: 34rem; }\n .work-empty p { margin: 0 0 .75rem; color: var(--muted-foreground); line-height: 1.5; }\n .work-empty .row { display: flex; align-items: center; gap: 1rem; }\n .work-empty .row > a { display: inline-flex; align-items: center; min-height: 44px; }\n .work-bound { margin-top: .75rem; }\n .status-line { display: inline-flex; align-items: center; gap: .45rem; min-width: 0; font-size: .875rem; font-weight: 600; color: var(--foreground); }\n .status-line .status-label { min-width: 0; overflow-wrap: anywhere; }\n .status-line .status-dot { flex: none; width: .5rem; height: .5rem; border-radius: 50%; background: var(--muted-foreground); }\n .status-line[data-tone=\"attention\"] .status-dot { background: var(--warning); }\n .status-line[data-tone=\"problem\"] .status-dot { background: var(--destructive); }\n .status-line[data-tone=\"live\"] .status-dot { background: var(--running); box-shadow: 0 0 0 3px var(--running-soft); }\n .status-line[data-tone=\"ready\"] .status-dot, .status-line[data-tone=\"done\"] .status-dot { background: var(--success); }\n .status-line[data-tone=\"muted\"] { color: var(--muted-foreground); font-weight: 500; }\n .status-line[data-tone=\"muted\"] .status-dot { background: var(--border); }\n .pull-request { display: grid; gap: .35rem; }\n .pull-request p { margin: 0; overflow-wrap: anywhere; }\n .pull-request-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .75rem; }\n .pull-request-head a, .pull-request .meta a { display: inline-flex; align-items: center; min-height: 44px; }\n .pull-request-head strong { font-size: 1.0625rem; font-weight: 600; color: var(--foreground); display: inline-flex; align-items: center; gap: .5rem; }\n .pull-request-head strong::before { content: \"\"; width: .5rem; height: .5rem; border-radius: 999px; background: var(--muted-foreground); }\n .pull-request--ok .pull-request-head strong::before { background: var(--success, #15803d); }\n .pull-request--problem .pull-request-head strong::before { background: var(--so-warning, #ab6400); }\n .pull-request-act { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .75rem; margin: .25rem 0 0; }\n .pull-request-act .meta { flex: 1 1 100%; }\n .pull-request-act label { display: grid; gap: .25rem; flex: 1 1 12rem; margin: 0; }\n .pull-request-act label input { margin: 0; min-height: 44px; box-sizing: border-box; width: 100%; }\n .pull-request-act button { min-height: 44px; white-space: nowrap; }\n .result-complete-actions { display: flex; flex-wrap: wrap; gap: .5rem; }\n .result-complete-actions button { white-space: nowrap; }\n .result-complete-actions button[name=publish], .pull-request-act button { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n @media (max-width: 480px) { .pull-request-act button, .result-complete-actions button { flex: 1 1 100%; } }\n .receipt-publication { position: relative; z-index: 1; margin: .35rem 0 0; font-size: .8125rem; color: var(--muted-foreground); }\n /* A raw repository path in ordinary copy wraps at any point rather than\n widening the page (the phone task-list overflow, package 1). */\n .path-words { overflow-wrap: anywhere; word-break: break-word; }\n @media (max-width: 760px) {\n .work-row { grid-template-columns: minmax(0, 1fr); gap: .125rem; padding: .625rem 0; }\n .work-head { align-items: center; }\n .work-head h1 { margin-bottom: 0; }\n .work-details > summary, .work-action { min-height: 44px; }\n /* The tools control sits at the head's right edge on every width, so\n its menu keeps the desk's right-aligned anchor here too. A phone\n override once re-anchored it at left: 0, which pushed the opened\n menu past the viewport (right edge 441.5px at 390px) and widened\n the document \u2014 found by the independent concise-UI review. */\n /* All four filters share the row (review fixes, finding 3): each tab\n takes an equal share, no tab scrolls out of view, the count sits on\n the label, and every target stays \u2265 40px tall. */\n .work-views { gap: 0; margin: .5rem 0 .25rem; overflow: visible; }\n .work-views a { flex: 1 1 0; min-width: 0; justify-content: center; padding: 0 .25rem; font-size: .8125rem; gap: .3rem; white-space: nowrap; }\n }\n @media (max-width: 400px) {\n .work-views a { font-size: .75rem; gap: .2rem; padding: 0 .125rem; }\n .work-views a .count { font-size: .625rem; }\n }\n /* The narrowest phones (\u2264 360px): four labels with counts cannot share\n 288px at legible type, so the strip becomes two rows of two \u2014 every\n filter wholly visible, unclipped, 40px tall, never scrolled away. */\n @media (max-width: 360px) {\n .work-views { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .work-views a { font-size: .8125rem; gap: .3rem; padding: 0 .25rem; }\n .work-views a .count { font-size: .6875rem; }\n }\n\n /* The phone shell: the sidebar disappears; a top bar carries the project\n and quick capture; a bottom tab bar carries the destinations a thumb\n visits. Desktop is untouched. */\n .mobile-top, .tabbar { display: none; }\n @media (max-width: 760px) {\n .app { display: block; }\n .side { display: none; }\n .mobile-top {\n display: flex; align-items: center; gap: .5rem; position: sticky; top: 0; z-index: 30;\n background: var(--glass-strong); border-bottom: 1px solid var(--glass-border);\n padding: calc(.375rem + env(safe-area-inset-top, 0rem)) .75rem .375rem;\n }\n /* One header row: the pill carries scope, counts, and the switch. */\n .scope-bar { display: none; }\n .mobile-top .brand-mini { font-weight: 600; font-size: .9375rem; text-decoration: none; color: var(--foreground); font-family: var(--font-mono); }\n .mobile-top .project-pill { flex: 1; min-width: 0; position: static; margin: 0; padding: 0; border: 0; background: none; box-shadow: none; }\n .mobile-top a.project-pill, .mobile-top .project-pill > summary {\n /* align-items: stretch, not the switcher's center: a centered column\n item takes its content width, and a long project name then widens\n the document (the 320px overflow, workspace package 1). */\n min-width: 0; display: flex; flex-direction: column; justify-content: center; align-items: stretch; gap: .0625rem;\n border: 1px solid var(--border); border-radius: 1.375rem; background: var(--card);\n min-height: 2.75rem; padding: .25rem .875rem; text-decoration: none; color: var(--foreground);\n font-size: .875rem; font-weight: 500; line-height: 1.25; cursor: pointer;\n }\n .mobile-top .project-pill > summary .name { display: inline-flex; align-items: center; gap: .25rem; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .mobile-top .project-pill > summary .name .chevron { flex: none; }\n .mobile-top a.project-pill:active, .mobile-top .project-pill > summary:active { background: var(--muted); }\n /* On a phone the menu drops from the header it belongs to (UI polish\n 2026-09-13). It was a fixed sheet, but the header's backdrop-filter\n makes the header the containing block for fixed descendants, so the\n sheet landed above the header, off-screen. Absolute to the sticky\n header is exact, and never taller than the screen. */\n .mobile-top .switcher-menu {\n position: absolute; left: .5rem; right: .5rem; top: calc(100% - .25rem); bottom: auto; max-width: none;\n max-height: calc(100dvh - 8rem); overflow-y: auto; z-index: 45;\n }\n .mobile-top .switcher-menu button { min-height: 2.75rem; }\n .mobile-top .pill-status {\n display: flex; flex-wrap: wrap; gap: .125rem .5rem;\n font-family: var(--font-mono); font-size: .6875rem; font-weight: 500;\n color: var(--muted-foreground); font-variant-numeric: tabular-nums;\n }\n .mobile-top .pill-status > span { white-space: nowrap; }\n .mobile-top .pill-status .hot { color: var(--brand); }\n .mobile-top .project-pill .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .mobile-top .mobile-new {\n flex: 0 0 auto; display: flex; align-items: center; justify-content: center;\n min-height: 2.75rem; padding: 0 .875rem; border-radius: 999px;\n background: var(--secondary); border: 1px solid var(--border); color: var(--foreground);\n font-weight: 500; font-size: .875rem; text-decoration: none;\n }\n /* Tools and settings: a header action, never a fourth tab (workspace\n package 1). A 44px target, the same quiet pill as quick capture. */\n .mobile-top .mobile-more {\n flex: 0 0 auto; display: flex; align-items: center; justify-content: center;\n width: 2.75rem; min-height: 2.75rem; border-radius: 999px;\n border: 1px solid var(--border); background: var(--card); color: var(--foreground);\n }\n .mobile-top .mobile-more svg { width: 1.125rem; height: 1.125rem; }\n @media (max-width: 360px) { .mobile-top .mobile-new { padding: 0 .625rem; } }\n .tabbar {\n display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;\n background: var(--glass-strong); border-top: 1px solid var(--glass-border);\n padding: .25rem max(.25rem, env(safe-area-inset-right, 0rem)) calc(.25rem + env(safe-area-inset-bottom, 0rem)) max(.25rem, env(safe-area-inset-left, 0rem));\n }\n .tabbar a {\n flex: 1; display: flex; flex-direction: column; align-items: center; gap: .125rem;\n padding: .375rem 0 .25rem; min-height: 3rem; text-decoration: none;\n color: var(--muted-foreground); font-size: .6875rem; font-weight: 500;\n font-family: var(--font-sans);\n }\n .tabbar a .glyph { display: flex; align-items: center; justify-content: center; height: 1.125rem; }\n .tabbar a .glyph svg { width: 1rem; height: 1rem; }\n .tabbar a.active { color: var(--foreground); }\n .tabbar a.active .glyph { background: var(--muted); border-radius: 999px; box-shadow: 0 0 0 .35rem var(--muted); }\n .tabbar a { position: relative; }\n .tabbar a .dot-badge {\n position: absolute; top: .3125rem; left: calc(50% + .375rem);\n width: .375rem; height: .375rem; border-radius: 9999px; background: var(--brand);\n }\n .content > main { padding: 1rem 1rem calc(4.5rem + env(safe-area-inset-bottom, 0rem)); }\n .project-add-card { padding: 1rem; }\n .project-add-actions { grid-template-columns: minmax(0, 1fr); }\n .project-add-action { min-height: 5.25rem; padding: .75rem; }\n .project-add-more > summary { width: 100%; }\n }\n\n /* /menu mirrors the rail's workflows/admin grouping as plain headed\n * sections \u2014 no collapse: it is already one tap behind the tab bar. */\n .menu-group-label {\n margin: 1.5rem 0 .25rem; font-size: .75rem; font-weight: 600;\n color: var(--muted-foreground); font-family: var(--font-sans);\n }\n .menu-group-label:first-of-type { margin-top: .75rem; }\n .menu-list { display: flex; flex-direction: column; gap: .375rem; margin-top: .75rem; }\n .menu-row {\n display: flex; flex-direction: column; gap: .125rem; text-decoration: none;\n border: 1px solid var(--border); border-radius: var(--radius); background: var(--card);\n padding: .75rem .875rem; color: var(--foreground); min-height: 44px; justify-content: center;\n }\n @media (hover: hover) and (pointer: fine) { .menu-row:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--muted-foreground)); } }\n\n .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr)); gap: .625rem; margin: .5rem 0; }\n .stat-card {\n border: 1px solid var(--border); border-radius: var(--radius); background: var(--card);\n padding: .75rem .875rem; min-width: 0;\n }\n .stat-card .k { font-weight: 600; font-size: .875rem; display: flex; align-items: center; gap: .4rem;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .stat-card .v { font-size: .75rem; color: var(--muted-foreground); margin-top: .25rem;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 9999px; flex: none; }\n .dot-ok { background: var(--success); }\n /* Caution without a claim on the operator: quiet, not magenta. */\n .dot-warn { background: var(--muted-foreground); }\n .dot-off { background: var(--muted-foreground); opacity: .5; }\n .dot-bad { background: var(--destructive); }\n .pulse { animation: pulse 2s ease-in-out infinite; }\n .dot-ok.pulse { background: var(--running); }\n @keyframes pulse { 50% { opacity: .35; } }\n\n .login-viewport { min-height: 100dvh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem 1.25rem; }\n .login-shell { width: 100%; max-width: 23rem; }\n .login-shell h1 { text-align: center; margin: 0 0 .375rem; font-size: 1.5rem; letter-spacing: -0.02em; }\n .login-shell > .hint { text-align: center; margin: 0 0 2rem; font-size: 0.875rem; }\n .login-card {\n background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);\n padding: 1.5rem 1.5rem 1.625rem;\n }\n .login-card label:first-child { margin-top: 0; }\n .login-card .problem { margin: 0 0 1rem; }\n .login-shell button {\n width: 100%; margin-top: 1.25rem;\n background: var(--so-accent); color: var(--so-on-accent); border-color: var(--so-accent); font-weight: 600;\n }\n @media (hover: hover) and (pointer: fine) { .login-shell button:hover { background: var(--so-accent-hover); border-color: var(--so-accent-hover); } }\n .login-foot { text-align: center; margin: 1.5rem 0 0; font-size: 0.75rem; color: var(--muted-foreground); line-height: 1.9; }\n .login-foot code { background: none; padding: 0; color: var(--muted-foreground); white-space: nowrap; }\n .so-wordmark { display: flex; align-items: center; gap: 9px; font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap; letter-spacing: -.02em; color: var(--foreground); }\n .so-brand-mark { width: 18px; height: 22px; display: flex; align-items: center; gap: 3px; transform: skewY(-10deg); }\n .so-brand-mark i { display: block; width: 4px; height: 15px; background: var(--so-signal); border-radius: 1px; }\n .so-brand-mark i:nth-child(2) { height: 22px; }\n .login-shell .login-brand { justify-content: center; font-size: 1.125rem; margin: 0 0 1.5rem; }\n .sso-step-up { display: inline-flex; align-items: center; min-height: 36px; font-size: .9rem; }\n .sso-step-up[data-sso-step-up=\"confirmed\"] { color: var(--so-success); }\n .sso-step-up[data-sso-step-up=\"confirmed\"]::before { content: \"\u2713\"; margin-right: 6px; }\n .login-shell .login-sso { display: flex; justify-content: center; align-items: center; min-height: 44px; border-radius: 8px; background: var(--so-accent); color: var(--so-on-accent); font-weight: 600; text-decoration: none; margin: 0 0 1rem; }\n @media (hover: hover) and (pointer: fine) { .login-shell .login-sso:hover { background: var(--so-accent-hover); } }\n .login-shell .login-password > summary { text-align: center; font-size: .875rem; color: var(--muted-foreground); cursor: pointer; margin: 0 0 .75rem; }\n .focus-page { max-width: 40rem; margin: 0 auto; padding: 2rem 1.25rem 3rem; }\n .focus-page .focus-brand { display: inline-flex; margin: 0 0 2.5rem; }\n .focus-page h1 { margin: 0 0 1rem; }\n .secret-value { overflow-wrap: anywhere; font-size: .95rem; padding: .75rem .875rem; margin: .75rem 0; border: 1px solid var(--border); border-radius: 8px; background: var(--background); user-select: all; }\n\n @media (max-width: 640px) {\n button, form.option button { min-height: 2.75rem; }\n .topbar nav a { padding: 0 .5rem; }\n }\n\n /* The board: lanes as columns, the pipeline left to right. */\n .content > main:has(.board) { max-width: none; }\n .board {\n display: grid; grid-template-columns: repeat(5, minmax(12.5rem, 1fr));\n gap: .625rem; overflow-x: auto; padding-bottom: .75rem; align-items: start;\n }\n /* The phone board (board pass): lanes stack as sections that fold, the\n counts readable before a single card \u2014 what needs you, then what is\n building, then the rest. A header is a 2.75rem tap; a drawn chevron\n says which way it folds. */\n @media (max-width: 760px) {\n .board { display: flex; flex-direction: column; gap: .5rem; padding-bottom: 0; }\n .board .lane { min-height: 0; padding: 0 .625rem .125rem; }\n .board .lane > summary {\n cursor: pointer; display: flex; align-items: center; min-height: 2.75rem; margin: 0;\n -webkit-tap-highlight-color: transparent;\n }\n .board .lane > summary h2 { flex: 1; font-size: .75rem; }\n .board .lane > summary .lane-count { border: 1px solid var(--border); border-radius: 999px; padding: .04rem .5rem; margin-left: .25rem; }\n .board .lane > summary::after {\n content: \"\"; width: .5rem; height: .5rem; flex: none; margin-right: .25rem;\n border-right: 1.5px solid var(--muted-foreground); border-bottom: 1.5px solid var(--muted-foreground);\n transform: rotate(45deg) translateY(-.125rem); transition: transform .15s;\n }\n .board .lane[open] > summary::after { transform: rotate(225deg) translateY(-.125rem); }\n .board .lane .hint { margin-bottom: .25rem; }\n .board .lane .lane-card:last-of-type { margin-bottom: .5rem; }\n .board .lane-attention { order: 0; }\n .board .lane-building { order: 1; }\n .board .lane-queued { order: 2; }\n .board .lane-waiting { order: 3; }\n .board .lane-done { order: 4; }\n }\n .lane {\n background: color-mix(in srgb, var(--card) 45%, var(--background)); border: 1px solid var(--border);\n border-radius: var(--radius); padding: .625rem; min-height: 12rem;\n }\n .lane > summary { list-style: none; cursor: default; margin: 0 0 .125rem; }\n .lane > summary::-webkit-details-marker { display: none; }\n .lane h2 { display: flex; align-items: center; gap: .4rem; margin: 0; font-size: .6875rem; }\n .lane h2 a { color: inherit; text-decoration: none; }\n @media (hover: hover) and (pointer: fine) { .lane h2 a:hover { text-decoration: underline; } }\n .lane .hint { margin-top: 0; }\n .lane-count { color: var(--muted-foreground); font-weight: 400; font-variant-numeric: tabular-nums; }\n .lane h2::before {\n content: \"\"; width: .375rem; height: .375rem; border-radius: 9999px; flex: none;\n background: var(--muted-foreground); opacity: .55;\n }\n .lane-attention h2::before { background: var(--brand); opacity: 1; }\n .lane-building h2::before { background: var(--running); opacity: 1; }\n .lane-done h2::before { background: var(--success); opacity: 1; }\n .lane-card {\n display: block; text-decoration: none; color: inherit;\n background: var(--card); border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px); padding: .5rem .625rem; margin-top: .5rem;\n }\n @media (hover: hover) and (pointer: fine) { .lane-card:hover { border-color: color-mix(in srgb, var(--border) 55%, var(--muted-foreground)); } }\n .lane-card .id { display: block; font-family: var(--font-mono); font-size: .6875rem; color: var(--muted-foreground); margin-bottom: .125rem; overflow-wrap: anywhere; }\n .lane-card .t { display: block; font-size: .8125rem; font-weight: 500; line-height: 1.35; }\n .lane-card .dot { margin-right: .4rem; }\n .lane-card .meta, .lane-card .mono { display: block; margin-top: .125rem; font-size: .75rem; }\n .lane-card .why { display: block; margin-top: .125rem; font-size: .75rem; color: var(--muted-foreground); }\n /* The facts: mono key\u2013value pairs, keys dim, values ink \u2014 one grammar on every lane. */\n .lane-card .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: .0625rem .625rem; margin-top: .4rem; font-size: .6875rem; line-height: 1.5; }\n .lane-card .fact { display: contents; }\n .lane-card .fact .k { color: var(--muted-foreground); font-family: var(--font-mono); }\n .lane-card .fact .v { font-family: var(--font-mono); color: var(--foreground); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }\n .lane-card .chips { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .4rem; }\n .lane-card .chips .badge { margin: 0; }\n /* The live strip on a building card: stage and clock in an inset well \u2014\n the run's own facts, never a percent. */\n .lane-card .live-line {\n display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: .375rem;\n padding: .3rem .5rem; border-radius: calc(var(--radius) - 4px); background: var(--muted);\n font-family: var(--font-mono); font-size: .6875rem; color: var(--running); font-variant-numeric: tabular-nums;\n }\n .lane-card .live-line .clock { color: var(--muted-foreground); }\n .lane-empty { margin: .75rem 0 .25rem; }\n .plan-doc { white-space: pre-wrap; overflow-wrap: anywhere; font-size: .8125rem; max-height: 24rem; overflow-y: auto; }\n .lane-more { display: block; margin-top: .5rem; font-size: .75rem; }\n\n /* The attended control room: one cross-workspace pulse, then a dense\n master rail. */\n .control-room-head {\n display: flex; align-items: flex-start; justify-content: space-between;\n gap: 1rem; margin-bottom: 1.1rem;\n }\n .control-room-head h1 { font-size: 1.375rem; margin-top: .08rem; }\n .control-room-head .actions { display: flex; gap: .45rem; flex-wrap: wrap; justify-content: flex-end; }\n .control-room-head .actions a { text-decoration: none; }\n .command-metrics {\n display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: .625rem; margin: .85rem 0 1.5rem;\n }\n .command-metric {\n border: 1px solid var(--border); border-radius: calc(var(--radius) - 2px); background: var(--card);\n padding: .8rem .9rem; min-width: 0;\n }\n .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); }\n .command-metric .label { display: flex; align-items: center; gap: .4rem; font-weight: 500; font-size: .8125rem; }\n .command-metric .label::before {\n content: \"\"; width: .375rem; height: .375rem; border-radius: 9999px;\n background: var(--muted-foreground); flex: none;\n }\n .command-metric .detail { display: block; color: var(--muted-foreground); font-size: .6875rem; margin-top: .18rem; }\n .command-metric.attention .label::before { background: var(--brand); }\n .command-metric.live .label::before { background: var(--running); }\n .workspace-pulse { margin: .4rem 0 1.5rem; display: grid; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); gap: .625rem; }\n /* The workspace card: name and status word, four counts, the same counts\n as a bar, and the one tap to its board. Neutral border always; the\n needs-you count and the bar's segment carry the accent. */\n .workspace-card {\n padding: .75rem .9rem; border: 1px solid var(--border);\n border-radius: calc(var(--radius) - 2px); background: var(--card); font-size: .8125rem; min-width: 0;\n }\n /* The mate's unified workspace: a glass project navigator beside one\n long-lived conversation. The content stays calm and legible; the\n atmosphere belongs to the shell and edges, never behind the prose. */\n main:has(.chat-workspace) { max-width: 86rem; padding-top: 1.5rem; }\n .chat-workspace {\n display: grid; grid-template-columns: minmax(16rem, 18.5rem) minmax(0, 56rem);\n align-items: start; gap: clamp(1.25rem, 3vw, 2.75rem);\n }\n .chat-workspace.projects-hidden { grid-template-columns: minmax(0, 56rem); }\n .chat-workspace.projects-hidden .chat-projects { display: none; }\n .chat-main { min-width: 0; max-width: 56rem; }\n .task-chat-workspace { grid-template-columns: minmax(15rem, 18rem) minmax(0, 56rem); }\n .task-chat-context {\n position: sticky; top: 6rem; align-self: start; padding: 1rem;\n border: 1px solid var(--glass-border); border-radius: calc(var(--radius) + 3px);\n background: var(--glass); box-shadow: var(--shadow);\n }\n .task-chat-context-head { display: flex; align-items: center; justify-content: space-between; gap: .65rem; }\n .task-chat-context h2 { margin: .7rem 0 .25rem; color: var(--foreground); font-size: 1rem; line-height: 1.35; letter-spacing: -.025em; }\n .task-chat-context > .meta { margin: 0; overflow-wrap: anywhere; font-size: .65rem; }\n .task-chat-status { display: grid; gap: .2rem; margin-top: .85rem; padding: .7rem; border-radius: calc(var(--radius) - 3px); background: var(--warning-soft); }\n .task-chat-status.ready { background: var(--running-soft); }\n .task-chat-status strong { font-size: .75rem; }\n .task-chat-status span { color: var(--muted-foreground); font-size: .68rem; line-height: 1.45; }\n .task-chat-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .4rem; margin: .65rem 0; }\n .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); }\n .task-chat-facts dt { color: var(--muted-foreground); font: 400 .58rem/1.2 var(--font-mono); text-transform: uppercase; letter-spacing: .04em; }\n .task-chat-facts dd { margin: .15rem 0 0; font-size: .68rem; overflow-wrap: anywhere; }\n .task-chat-overview-actions { display: grid; justify-items: start; gap: .55rem; margin-top: .75rem; }\n .task-chat-overview-link { display: block; font-size: .72rem; text-decoration: none; }\n .task-chat-recovery-link { min-height: 2rem; padding-inline: .7rem; font-size: .7rem; }\n #task-chat-live { display: grid; gap: 1rem; margin-bottom: 1.1rem; }\n .task-journey {\n position: relative; overflow: hidden; padding: 1rem 1.05rem;\n border-color: color-mix(in srgb, var(--accent) 16%, var(--glass-border));\n background: var(--card);\n }\n .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; }\n .task-journey-head { position: relative; z-index: 1; display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }\n .task-journey h2 { margin: .2rem 0 0; font-size: 1rem; letter-spacing: -.025em; }\n .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; }\n .task-journey > ol::before { content: \"\"; position: absolute; top: .7rem; left: 10%; right: 10%; height: 1px; background: var(--border); }\n .task-journey li { position: relative; z-index: 1; display: grid; justify-items: center; gap: .35rem; color: var(--muted-foreground); font-size: .625rem; text-align: center; }\n .task-journey li i { display: grid; place-items: center; width: 1.4rem; height: 1.4rem; border: 1px solid var(--border); border-radius: 50%; background: var(--card); font: 600 .58rem/1 var(--font-mono); font-style: normal; }\n .task-journey li.complete i { color: var(--success); border-color: color-mix(in srgb,var(--success) 32%,var(--border)); background: color-mix(in srgb,var(--success) 9%,var(--card)); }\n .task-journey li.complete span { color: var(--foreground); }\n .task-journey li.active i { color: white; border-color: var(--accent); background: var(--accent); box-shadow: 0 0 0 4px color-mix(in srgb,var(--accent) 12%,transparent); }\n .task-journey li.active span { color: var(--foreground); font-weight: 650; }\n .task-journey > .meta { margin: 0; line-height: 1.45; }\n .task-journey-action { margin-top: .75rem; min-height: 44px; }\n .check-progress {\n margin: .5rem 0 0; color: var(--running); font-size: clamp(.625rem,2.2vw,.75rem);\n line-height: 1.5; font-variant-numeric: tabular-nums; white-space: nowrap;\n overflow-x: auto; scrollbar-width: thin;\n }\n .check-progress[data-final=\"passed\"] { color: var(--success); }\n .check-progress[data-final=\"failed\"] { color: var(--destructive); }\n .check-progress[data-final=\"unknown\"] { color: var(--warning); }\n .receipt-actions [data-primary-action] { min-height: 44px; }\n .task-status-reason { margin-top: .75rem; }\n .task-status-reason summary, .task-status-details > summary { color: var(--muted-foreground); }\n .task-status-details > summary { min-height: 44px; }\n .task-status-details, #task-control-details { margin: 1rem 0; }\n .task-control-copy { min-width: 0; overflow-wrap: anywhere; }\n [data-primary-action] { max-width: 100%; }\n\n .task-live-build { display: flex; align-items: center; flex-wrap: wrap; gap: .3rem; margin: .75rem 0 0; font-size: .72rem; }\n .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); }\n .chat-action-card { padding: 0; overflow: hidden; }\n .chat-action-card:not(details) { padding: 1rem 1.05rem; }\n .chat-action-card > summary { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.05rem; cursor: pointer; list-style: none; }\n .chat-action-card > summary::-webkit-details-marker { display: none; }\n .chat-action-card > summary > span:first-child { display: grid; gap: .18rem; }\n .chat-action-card > summary strong { font-size: .92rem; }\n .chat-action-card > summary small { color: var(--muted-foreground); font-size: .68rem; font-weight: 400; }\n .chat-action-card[open] > summary { border-bottom: 1px solid var(--border); }\n .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); }\n .chat-run-details > summary { color: var(--muted-foreground); cursor: pointer; font-size: .68rem; }\n .chat-run-details > .meta { margin-bottom: 0; }\n .chat-decisions { display: grid; gap: .7rem; }\n .chat-section-head { margin: .15rem .15rem 0; }\n .chat-section-head h2 { margin: .2rem 0 0; font-size: 1rem; }\n .chat-decisions .decide-card { margin: 0; background: var(--glass); box-shadow: var(--shadow); }\n .chat-publication { margin: -.25rem .25rem .25rem; }\n .chat-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding: .5rem .25rem 0; }\n .chat-head h1 { margin-bottom: .2rem; font-size: 1.65rem; letter-spacing: -.04em; }\n .chat-head .badge-running { margin-top: .2rem; background: color-mix(in srgb, var(--success) 11%, var(--glass)); color: var(--success); }\n .chat-head-actions { display: flex; align-items: center; justify-content: flex-end; gap: .45rem; flex-wrap: wrap; }\n .task-chat-head > div { min-width: 0; flex: 1; }\n .chat-task-back { margin: 0 0 .55rem; font-size: .6875rem; }\n .chat-task-back a { text-decoration: none; }\n .task-chat-title-line { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }\n .task-chat-title-line > div { min-width: 0; }\n .chat-project-toggle {\n display: inline-flex; align-items: center; gap: .4rem; min-height: 2rem; padding: .25rem .55rem;\n color: var(--muted-foreground); font-size: .6875rem; box-shadow: none;\n }\n .chat-project-toggle svg { width: .9rem; height: .9rem; }\n .chat-project-toggle .badge { padding-inline: .38rem; font-size: .6rem; }\n .chat-budget {\n display: flex; flex-wrap: wrap; gap: .4rem; margin: 1rem 0 1.25rem;\n color: var(--muted-foreground); font-size: .6875rem; font-variant-numeric: tabular-nums;\n }\n .chat-budget > span {\n white-space: nowrap; padding: .32rem .58rem; border: 1px solid var(--glass-border);\n border-radius: 999px; background: color-mix(in srgb, var(--glass) 74%, transparent);\n }\n /* Provider, model, and limits behind one quiet disclosure (UI polish 2026-09-13). */\n .chat-limits { margin: .5rem 0 .75rem; padding: 0 .9rem; border-color: var(--glass-border); background: color-mix(in srgb, var(--glass) 70%, transparent); }\n .chat-limits > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; min-height: 2.5rem; padding: .45rem 0; font-size: .75rem; }\n .chat-limits > summary .meta { font-size: .75rem; }\n .chat-limits[open] { padding-bottom: .6rem; }\n .chat-limits .chat-budget { margin: .2rem 0 .1rem; }\n .chat-main .chat-limits + .chat-fleet-context, .chat-main .chat-fleet-context + .chat-limits { margin-top: .5rem; }\n .chat-overview {\n margin: 0 0 1.35rem; padding: 1rem; border-radius: calc(var(--radius) + 2px);\n background: var(--glass);\n box-shadow: var(--shadow);\n }\n .chat-overview-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }\n .chat-overview-head h2 { margin: .1rem 0 0; color: var(--foreground); font-size: .95rem; letter-spacing: -.02em; }\n .chat-overview-head form { margin: 0; }\n .chat-overview-head button, .chat-overview-link {\n min-height: 2rem; padding: .25rem .7rem; font-size: .6875rem; text-decoration: none;\n }\n .chat-overview-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .45rem; margin-top: .85rem; }\n .chat-overview-stat {\n display: flex; flex-direction: column; min-width: 0; padding: .65rem .7rem;\n border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px);\n background: color-mix(in srgb, var(--glass-strong) 64%, transparent); text-decoration: none;\n }\n @media (hover: hover) and (pointer: fine) { .chat-overview-stat:hover { background: var(--glass-strong); } }\n .chat-overview-stat b { color: var(--foreground); font: 600 1.2rem/1.2 var(--font-mono); font-variant-numeric: tabular-nums; }\n .chat-overview-stat span { overflow: hidden; color: var(--muted-foreground); font-size: .65rem; text-overflow: ellipsis; white-space: nowrap; }\n .chat-overview-stat.attention b { color: var(--warning); }\n .chat-overview-stat.live b { color: var(--running); }\n .chat-overview-items { display: grid; gap: .35rem; margin-top: .7rem; }\n .chat-overview-item {\n display: grid; grid-template-columns: 1.75rem minmax(0, 1fr) auto; align-items: center; gap: .6rem;\n padding: .48rem .55rem; border-radius: calc(var(--radius) - 4px); color: inherit; text-decoration: none;\n }\n @media (hover: hover) and (pointer: fine) { .chat-overview-item:hover { background: color-mix(in srgb, var(--muted) 72%, transparent); } }\n .chat-overview-icon { display: grid; place-items: center; width: 1.75rem; height: 1.75rem; border-radius: .55rem; background: var(--muted); color: var(--muted-foreground); }\n .chat-overview-icon svg { width: .9rem; height: .9rem; }\n .chat-overview-item.decision .chat-overview-icon { color: var(--warning); background: var(--warning-soft); }\n .chat-overview-item.failed .chat-overview-icon { color: var(--destructive); background: var(--destructive-soft); }\n .chat-overview-item.running .chat-overview-icon { color: var(--running); background: var(--running-soft); }\n .chat-overview-copy { min-width: 0; }\n .chat-overview-copy strong, .chat-overview-copy span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .chat-overview-copy strong { font-size: .75rem; font-weight: 500; }\n .chat-overview-copy span { color: var(--muted-foreground); font-size: .65rem; margin-top: .05rem; }\n .chat-overview-arrow { color: var(--muted-foreground); }\n .chat-overview-clear { display: flex; align-items: center; gap: .45rem; margin: .8rem .2rem 0; color: var(--muted-foreground); font-size: .75rem; }\n .chat-overview-note { margin: .65rem .2rem 0; font-size: .6875rem; }\n .chat-readonly { margin-top: 1rem; padding: 1rem; }\n .chat-projects {\n position: sticky; top: 4rem; align-self: start; max-height: calc(100vh - 5rem); overflow-y: auto;\n padding: 1rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) + 3px);\n background: var(--glass); box-shadow: var(--shadow);\n }\n .chat-projects-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-bottom: .75rem; padding: 0 .15rem; }\n .chat-projects-head h2 { margin: 0; color: var(--foreground); font-size: .8125rem; letter-spacing: -.01em; }\n .chat-project-close { display: none; margin-left: auto; width: 2rem; min-height: 2rem; padding: 0; font-size: 1rem; box-shadow: none; }\n .chat-project-card {\n padding: .8rem; margin-top: .5rem; border: 1px solid transparent; border-radius: var(--radius);\n background: color-mix(in srgb, var(--muted) 58%, transparent);\n transition: transform .16s ease, background .16s, border-color .16s, box-shadow .16s;\n }\n @media (hover: hover) and (pointer: fine) { .chat-project-card:hover {\n border-color: var(--glass-border); background: color-mix(in srgb, var(--muted) 82%, transparent);\n box-shadow: 0 12px 28px -24px rgb(0 0 0 / .8);\n } }\n .chat-project-name { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: .45rem; }\n .chat-project-name strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .8125rem; }\n .chat-project-name .mono { color: var(--running); font-size: .6875rem; }\n .chat-project-name .badge { background: var(--glass); font-size: .625rem; }\n .chat-project-stats { display: grid; grid-template-columns: 1fr 1fr; gap: .3rem .55rem; margin-top: .65rem; }\n .chat-project-stats span { color: var(--muted-foreground); font-size: .6875rem; white-space: nowrap; }\n .chat-project-stats b { color: var(--foreground); font-family: var(--font-mono); font-weight: 600; font-variant-numeric: tabular-nums; }\n .chat-project-stats span.hot, .chat-project-stats span.hot b { color: var(--brand); }\n .chat-project-actions { display: flex; gap: .35rem; margin-top: .65rem; }\n .chat-project-actions form { margin-bottom: 0; }\n .chat-project-actions button { min-height: 1.8rem; padding: .2rem .6rem; font-size: .6875rem; box-shadow: none; background: transparent; }\n .thread {\n display: flex; flex-direction: column; gap: 1rem; margin: 1rem 0 1.25rem;\n min-height: min(32rem, 48vh); padding: .25rem;\n }\n .thread .msg { max-width: 48rem; line-height: 1.65; overflow-wrap: anywhere; }\n .chat-previous-divider { display: flex; align-items: center; gap: .6rem; margin: 1rem 0; color: var(--so-muted); font-size: .8rem; }\n .chat-previous-divider::before, .chat-previous-divider::after { content: \"\"; flex: 1; border-top: 1px solid var(--so-line); }\n .thread .msg p { margin: .3rem 0; }\n .thread .msg.op {\n align-self: flex-end; max-width: min(82%, 40rem); padding: .75rem 1rem;\n border: 1px solid var(--glass-border); border-radius: 1.2rem 1.2rem .35rem 1.2rem;\n background: var(--user-message); color: var(--so-ink);\n box-shadow: 0 10px 30px -24px rgb(0 0 0 / .9), 0 1px 0 rgb(255 255 255 / .07) inset;\n }\n .thread .msg.mate {\n position: relative; align-self: stretch; padding: .75rem .5rem .75rem 3.45rem;\n border: 0; background: transparent;\n }\n .thread .msg.mate::before {\n content: \"T\"; position: absolute; top: .7rem; left: .1rem; width: 2.35rem; height: 2.35rem;\n display: grid; place-items: center; border: 1px solid var(--glass-border); border-radius: .8rem;\n background: var(--glass-strong);\n color: var(--foreground); font: 600 .6875rem/1 var(--font-mono); letter-spacing: -.04em;\n box-shadow: var(--so-pill-shadow);\n }\n .chat-copy > :first-child { margin-top: 0; }\n .chat-copy > :last-child { margin-bottom: 0; }\n .chat-copy p { margin: .35rem 0 .7rem; }\n .chat-copy h3 { margin: 1rem 0 .35rem; color: var(--foreground); font-size: .8125rem; }\n .chat-copy ul, .chat-copy ol { margin: .4rem 0 .75rem; padding-left: 1.3rem; }\n .chat-copy li { margin: .24rem 0; padding-left: .15rem; }\n .chat-message-foot { display: flex; align-items: center; justify-content: space-between; gap: .75rem; margin-top: .75rem; }\n .chat-message-foot time { color: var(--muted-foreground); font: 400 .625rem/1 var(--font-mono); white-space: nowrap; }\n .chat-activity { display: flex; flex-wrap: wrap; gap: .3rem; }\n .chat-activity span { padding: .15rem .42rem; border: 1px solid var(--glass-border); border-radius: 999px; color: var(--muted-foreground); font: 400 .625rem/1.25 var(--font-mono); }\n .proposal { margin: .9rem 0 0; padding: 0; overflow: hidden; background: var(--glass); }\n .proposal-head { display: grid; grid-template-columns: 2rem minmax(0, 1fr) auto; align-items: center; gap: .65rem; padding: .75rem .85rem; border-bottom: 1px solid var(--glass-border); }\n .proposal-head > span:nth-child(2) { min-width: 0; }\n .proposal-head strong, .proposal-head small { display: block; }\n .proposal-head strong { font-size: .75rem; }\n .proposal-head small { margin-top: .05rem; color: var(--muted-foreground); font-size: .625rem; }\n .proposal-icon { display: grid; place-items: center; width: 2rem; height: 2rem; border-radius: .6rem; color: var(--running); background: var(--running-soft); }\n .proposal-icon svg { width: 1rem; height: 1rem; }\n .proposal-cancel .proposal-icon { color: var(--destructive); background: var(--destructive-soft); }\n .proposal-answer .proposal-icon { color: var(--warning); background: var(--warning-soft); }\n .proposal-repair .proposal-icon { color: var(--warning); background: var(--warning-soft); }\n .proposal-body { padding: .85rem; }\n .proposal-body h3 { margin: 0; color: var(--foreground); font-size: .9rem; letter-spacing: -.015em; }\n .proposal-summary { margin: .45rem 0 0; color: var(--foreground); white-space: pre-wrap; }\n .proposal-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .35rem; margin: .75rem 0 0; }\n .proposal-facts div { min-width: 0; padding: .45rem .55rem; border-radius: calc(var(--radius) - 5px); background: color-mix(in srgb, var(--muted) 65%, transparent); }\n .proposal-facts dt { color: var(--muted-foreground); font: 400 .6rem/1.25 var(--font-mono); text-transform: uppercase; letter-spacing: .04em; }\n .proposal-facts dd { margin: .18rem 0 0; overflow-wrap: anywhere; font-size: .72rem; }\n .proposal-rationale { margin-top: .75rem; padding: .65rem .75rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--muted); }\n .proposal-rationale strong { display: block; margin-top: .2rem; }\n .proposal-rationale p { margin: .2rem 0 0; color: var(--muted-foreground); }\n .proposal-disclosure { margin-top: .7rem !important; font-size: .6875rem; }\n .proposal-actions { padding: 0 .85rem .85rem; }\n .proposal-actions .acts { display: flex; align-items: center; gap: .5rem; }\n .proposal-actions form { margin: 0; }\n /* The confirm is the card's one primary; dismiss stays quiet (UI polish 2026-09-13). */\n .proposal-actions .acts form:first-child button[type=submit] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n @media (hover: hover) and (pointer: fine) { .proposal-actions .acts form:first-child button[type=submit]:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background)); } }\n .proposal-actions .done, .proposal-actions .refused, .proposal-wait { margin: 0; padding: .55rem .65rem; border-radius: calc(var(--radius) - 5px); font-size: .72rem; }\n .proposal-actions .done { color: var(--success); background: var(--success-soft); }\n .proposal-actions .refused { color: var(--destructive); background: var(--destructive-soft); }\n .proposal.confirmed { border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); }\n .proposal.refused { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); }\n .proposal .done { color: var(--ok); }\n .proposal .refused { color: var(--danger); }\n .chat-empty { margin: auto; padding: clamp(3rem, 9vh, 6rem) 1rem 3rem; text-align: center; }\n .chat-empty::before {\n content: \"T\"; display: grid; place-items: center; width: 3.5rem; height: 3.5rem; margin: 0 auto 1.1rem;\n border: 1px solid var(--glass-border); border-radius: 1.15rem;\n background: var(--glass-strong);\n box-shadow: var(--so-pill-shadow);\n font: 600 .8rem/1 var(--font-mono); letter-spacing: -.05em;\n }\n .chat-empty > strong { display: block; font-size: 1.2rem; letter-spacing: -.025em; }\n .chat-empty > .meta { margin-top: .4rem; }\n .chat-prompts { display: flex; justify-content: center; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; }\n /* The composer's status line takes no room until it has a state to report. */\n .composer-hint:empty { display: none; }\n .chat-prompts form { margin: 0; }\n .chat-prompts button { min-height: 2.35rem; box-shadow: none; background: var(--glass); padding-inline: .9rem; }\n /* The New update action (package 2): sticks above the composer, takes no\n room while hidden, and is the only thing that moves a reader who sat\n above the latest message when a live update landed. */\n .chat-new-update-holder { position: sticky; bottom: 1rem; z-index: 28; display: flex; justify-content: center; height: 0; margin: 0; pointer-events: none; }\n .chat-new-update { pointer-events: auto; transform: translateY(-100%); min-height: 2.35rem; padding-inline: .9rem; border-radius: 999px; box-shadow: var(--shadow-overlay); }\n .chat-new-update[hidden] { display: none; }\n /* The concise plan (package 2): a title, the outcome, one line of\n counts and limits, then one Review plan disclosure over the unchanged\n exact-terms form. */\n .chat-approval-stale { margin: .75rem 0 0; }\n .chat-approval-stale a { font-weight: 600; }\n form.approve-form[data-stale=\"1\"] .approval-confirm, form.approve-form[data-stale=\"1\"] .approval-act { opacity: .55; }\n .proposal-filed { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; margin: .5rem 0 0; }\n .chat-thinking { display: flex; align-items: center; gap: .75rem; padding: .75rem .85rem; }\n .chat-thinking p { flex: 1; margin: 0; }\n .chat-thinking p strong, .chat-thinking p span { display: block; }\n .chat-thinking p span { margin-top: .08rem; }\n .chat-thinking form { margin: 0; }\n /* On a desk the overview is its own card \u2014 no second frame around it. */\n .chat-fleet-context, .chat-fleet-context[open] { margin: 0; padding: 0; border: 0; background: transparent; }\n .chat-fleet-context > summary { display: none; }\n /* The summary's needs-you count is magenta wherever the summary shows (a count, by the law). */\n .chat-fleet-context > summary .hot { color: var(--brand); font-weight: 600; }\n @media (min-width: 761px) {\n /* A fresh conversation on a desk (annotation on build 1540): the\n overview folds behind one summary row that still carries its two\n counts, and the empty state gives up its centering slack, so the whole\n composer and its send control sit inside the first 1280\u00D7800 viewport. */\n .chat-main:has(.chat-empty) .chat-fleet-context { margin: 0 0 .25rem; border: 1px solid var(--glass-border); border-radius: 1rem; background: color-mix(in srgb, var(--glass) 70%, transparent); }\n .chat-main:has(.chat-empty) .chat-fleet-context[open] { padding-bottom: .5rem; }\n .chat-main:has(.chat-empty) .chat-fleet-context > summary { display: flex; align-items: center; gap: .6rem; min-height: 2.5rem; padding: .5rem .9rem; color: var(--muted-foreground); font-size: .78rem; cursor: pointer; }\n .chat-main:has(.chat-empty) .chat-fleet-context > summary::before { content: \"\u25B8\"; flex: none; font-size: .7rem; }\n .chat-main:has(.chat-empty) .chat-fleet-context[open] > summary::before { content: \"\u25BE\"; }\n .chat-main:has(.chat-empty) .chat-fleet-context > summary .meta { margin-left: auto; font-size: .68rem; white-space: nowrap; }\n .chat-main:has(.chat-empty) .chat-fleet-context .chat-overview { margin: 0 .5rem; box-shadow: none; }\n .chat-main:has(.chat-empty) .thread { min-height: 0; margin: .75rem 0 .75rem; }\n .chat-main:has(.chat-empty) .chat-empty { padding: clamp(1.25rem, 4vh, 2.25rem) 1rem 1rem; }\n }\n .thinking-orb { position: relative; width: 2rem; height: 2rem; flex: none; border-radius: 999px; background: var(--running-soft); }\n .thinking-orb::after { content: \"\"; position: absolute; inset: .55rem; border-radius: inherit; background: var(--running); animation: pulse 1.25s ease-in-out infinite; }\n .composer {\n display: flex; align-items: flex-end; gap: .75rem; padding: .7rem; margin-top: .5rem;\n border-radius: 1.35rem; background: var(--glass-strong);\n }\n .composer label { flex: 1; min-width: 0; margin: 0; font-size: 0; }\n .composer textarea {\n width: 100%; min-height: 3.5rem; max-height: 13rem; margin: 0; padding: .75rem .85rem;\n resize: vertical; border: 0; background: transparent; box-shadow: none; font-size: 1rem;\n }\n .composer textarea:focus-visible { border: 0; box-shadow: none; }\n @media (hover: hover) and (pointer: fine) { .composer textarea:hover { border: 0; box-shadow: none; } }\n .composer button {\n flex: 0 0 2.75rem; width: 2.75rem; min-height: 2.75rem; padding: 0; border-radius: 999px;\n font-size: 0; box-shadow: 0 10px 24px -16px rgb(255 255 255 / .65);\n }\n .composer button::after { content: \"\u2191\"; font: 600 1.15rem/1 var(--font-sans); }\n .chat-main > details { margin-top: 1rem; background: color-mix(in srgb, var(--glass) 70%, transparent); }\n @media (min-width: 761px) {\n /* Keep the desktop composer in the document flow. A sticky bottom\n * constraint pulled it over proposal cards on long conversations (and\n * into the middle of full-page captures), hiding the very acts it asks\n * the operator to confirm. The #latest anchor still brings this form\n * into view after every turn without making it an overlay. */\n .chat-workspace .composer { position: static; width: 100%; box-shadow: var(--shadow); }\n /* The agents summary is said ONCE per screen (v48): the context panel\n * carries it on desktop, so the compact strip \u2014 the phone's copy \u2014\n * steps aside wherever that panel is visible. */\n .task-chat-workspace .task-chat-agents { display: none; }\n }\n @media (min-width: 761px) and (max-width: 1199px) {\n .chat-workspace { grid-template-columns: minmax(0, 1fr); gap: 1rem; }\n .chat-main { max-width: none; }\n .chat-projects { display: none; }\n .chat-workspace.projects-open::before {\n content: \"\"; position: fixed; inset: 0 0 0 232px; z-index: 23; background: rgb(0 0 0 / .18);\n }\n .chat-workspace.projects-open .chat-projects {\n display: block; position: fixed; top: 4rem; left: calc(232px + 1.25rem); z-index: 25;\n width: min(18.5rem, calc(100vw - 232px - 2.5rem)); max-height: calc(100vh - 5rem);\n }\n .app.sidebar-collapsed .chat-workspace.projects-open::before { inset: 0 0 0 64px; }\n .app.sidebar-collapsed .chat-workspace.projects-open .chat-projects { left: calc(64px + 1.25rem); }\n .chat-project-close { display: grid; place-items: center; }\n }\n @media (min-width: 900px) and (max-width: 1199px) {\n .task-chat-workspace { grid-template-columns: minmax(14rem, 16rem) minmax(0, 1fr); gap: 1.25rem; }\n }\n @media (max-width: 760px) {\n main:has(.chat-workspace) { padding: 1rem max(1rem, env(safe-area-inset-right, 0rem)) calc(var(--composer-height, 4rem) + 5rem + env(safe-area-inset-bottom, 0rem)) max(1rem, env(safe-area-inset-left, 0rem)); }\n /* Above the fixed composer and the tab bar, never beneath them. */\n .chat-new-update-holder { bottom: calc(var(--composer-height, 4rem) + 4.5rem + env(safe-area-inset-bottom, 0rem)); }\n .chat-main:has(.proposal.pending) .chat-new-update-holder, .chat-main:has(.chat-empty) .chat-new-update-holder { bottom: calc(4.5rem + env(safe-area-inset-bottom, 0rem)); }\n .chat-workspace, .chat-workspace.projects-hidden { display: block; }\n .chat-workspace.projects-hidden .chat-projects, .chat-projects { display: none; }\n .chat-project-toggle { display: inline-flex; min-height: 2.75rem; }\n .chat-project-close { display: grid; place-items: center; min-height: 2.75rem; min-width: 2.75rem; }\n .chat-workspace.projects-open::before { content: \"\"; position: fixed; inset: 0; z-index: 30; background: rgb(0 0 0 / .18); }\n .chat-workspace.projects-open .chat-projects { display: block; position: fixed; top: 5rem; left: 1rem; right: 1rem; width: auto; max-height: calc(100dvh - 10rem); overflow-y: auto; overscroll-behavior: contain; z-index: 31; background: var(--card); }\n .chat-project-list { display: grid; gap: 0; padding: 0; }\n .chat-project-card {\n margin: 0; padding: .85rem .4rem; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: transparent;\n }\n .chat-projects { position: static; max-height: none; overflow: hidden; padding: .65rem; margin-bottom: .85rem; }\n .chat-projects-head { margin-bottom: .35rem; }\n .chat-project-stats { display: flex; flex-wrap: wrap; gap: .15rem .7rem; margin-top: .35rem; }\n .chat-project-stats span { font-size: .625rem; }\n .chat-project-stats span:last-child { display: none; }\n .chat-project-actions { margin-top: .5rem; }\n .chat-project-actions button { min-height: 2.75rem; padding-inline: .85rem; font-size: .8125rem; }\n .chat-head { padding-inline: 0; }\n .chat-head h1 { font-size: 1.4rem; }\n .chat-head-actions { align-items: flex-start; }\n .task-chat-workspace .task-chat-context { display: none; }\n .result-panel { padding: .25rem 0 1rem; border: 0; border-radius: 0; box-shadow: none; background: transparent; }\n .result-head { display: grid; gap: .45rem; }\n .result-head .status-line { justify-self: start; }\n .result-tabs { gap: 0; }\n .result-tabs a { flex: 1 1 auto; min-width: 0; justify-content: center; padding: 0 .25rem; font-size: .8125rem; gap: .3rem; white-space: nowrap; min-height: 2.75rem; overflow: hidden; }\n .result-facts { grid-template-columns: 1fr; }\n .result-request .diff-comment-form button, .result-request .revision-from-comments button { width: 100%; }\n .result-action .button-link, .result-action form button { width: 100%; box-sizing: border-box; min-height: 2.75rem; }\n .result-request .revision-from-comments { display: grid; }\n .result-links { display: grid; gap: 0; }\n .result-links a { min-height: 2.75rem; display: inline-flex; align-items: center; }\n .result-links a + a::before { content: none; }\n .task-chat-head { display: block; }\n .task-chat-head > .badge { display: none; }\n .task-chat-title-line { display: grid; gap: .65rem; }\n .task-chat-title-line .task-view-switch { justify-self: start; }\n #task-chat-live { gap: .75rem; }\n .task-journey { padding: .85rem; }\n .task-journey > ol { margin: .85rem -.2rem .65rem; }\n .task-journey > ol::before { left: 9%; right: 9%; }\n .task-journey li { font-size: .56rem; }\n .task-journey li i { width: 1.25rem; height: 1.25rem; }\n .chat-action-card > summary { align-items: flex-start; padding: .85rem; }\n .chat-action-card > summary .button-link { min-height: 2.1rem; padding-inline: .6rem; font-size: .65rem; white-space: nowrap; }\n .chat-action-card > summary small { max-width: 14rem; }\n .chat-budget { gap: .3rem; margin: .65rem 0 .9rem; }\n .chat-budget > span { padding: .25rem .48rem; }\n .chat-budget > span:first-child { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }\n .chat-overview { padding: .8rem; }\n .chat-fleet-context { margin: .5rem 0 .75rem; border: 1px solid var(--glass-border); border-radius: 1rem; background: var(--glass); }\n .chat-fleet-context[open] { padding-bottom: .25rem; }\n .chat-fleet-context > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; min-height: 2.75rem; padding: .75rem .9rem; color: var(--muted-foreground); font-size: .8rem; cursor: pointer; }\n .chat-fleet-context > summary::before { content: \"\u25B8\"; flex: none; margin-right: .35rem; font-size: .7rem; }\n .chat-fleet-context[open] > summary::before { content: \"\u25BE\"; }\n .chat-fleet-context > summary .meta { margin-left: auto; font-size: .68rem; white-space: nowrap; }\n .chat-fleet-context .chat-overview { border: 0; box-shadow: none; margin: 0; }\n .chat-overview-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .chat-overview-head { align-items: flex-start; }\n .chat-overview-copy strong, .chat-overview-copy span { white-space: normal; }\n .thread { min-height: 18rem; padding: 0; }\n .thread .msg.op { max-width: 90%; }\n .thread .msg.mate { padding-left: 2.8rem; padding-right: 0; }\n .thread .msg.mate::before { width: 2.15rem; height: 2.15rem; border-radius: .7rem; }\n .proposal-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .proposal-actions .acts { align-items: stretch; flex-direction: row; flex-wrap: wrap; }\n .proposal-actions .acts form { flex: 1 1 8rem; width: auto; }\n .proposal-actions .acts form:has(.arm) { flex-basis: 100%; }\n .proposal-actions .acts button { width: 100%; }\n .chat-prompts {\n display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: .5rem; width: 100%; max-width: 100%; overflow: visible; padding: 0;\n }\n .chat-prompts form, .chat-prompts button { min-width: 0; width: 100%; }\n .chat-prompts form:last-child:nth-child(odd) { grid-column: 1 / -1; }\n .chat-main { padding-bottom: 0; }\n .chat-main #latest { scroll-margin-top: 7rem; }\n .chat-workspace .composer {\n position: fixed; left: 1rem; right: 1rem; bottom: calc(3.75rem + env(safe-area-inset-bottom, 0rem));\n z-index: 29; margin: 0; padding: .45rem; border-radius: 1.1rem; box-shadow: var(--shadow-overlay);\n }\n /* A confirmation is the primary act. Let the composer return to the\n document flow while one is pending so it can never cover the card's\n explanation or buttons on a short phone viewport. */\n .chat-main:has(.proposal.pending) { padding-bottom: 0; }\n .chat-main:has(.proposal.pending) .composer { position: static; width: 100%; margin-top: .5rem; }\n /* \u2026and the suggestion chips step aside for the same reason: on a phone\n the card, its buttons, and the composer then share one screen. */\n .chat-main:has(.proposal.pending) .chat-prompts { display: none; }\n /* First use is one cohesive intake card: prompt, suggestions, then the\n box. A fixed box belongs to an established thread; here it would sit\n above the very question it is asking the person to answer. */\n .chat-main:has(.chat-empty) { padding-bottom: 0; }\n .chat-main:has(.chat-empty) .thread { min-height: 0; margin-bottom: .5rem; }\n .chat-main:has(.chat-empty) .chat-empty {\n width: 100%; min-width: 0; max-width: 100%; margin: 0; padding: 2rem 0 .75rem;\n }\n .chat-main:has(.chat-empty) .chat-empty > strong,\n .chat-main:has(.chat-empty) .chat-empty > .meta { max-width: 22rem; margin-inline: auto; }\n .chat-main:has(.chat-empty) .chat-empty > .meta { margin-top: .45rem; }\n .chat-main:has(.chat-empty) .composer { position: static; width: 100%; margin-top: .5rem; }\n .composer textarea { min-height: 2.75rem; padding: .55rem .65rem; font-size: 1rem; }\n /* Hide bottom navigation only when viewport shrink indicates a soft\n keyboard; hardware-keyboard focus and pinch zoom keep it available. */\n html[data-mobile-keyboard] .tabbar { display: none; }\n html[data-mobile-keyboard] .chat-workspace .composer { bottom: calc(var(--keyboard-inset, 0px) + .5rem); }\n /* The raised composer needs the same room beneath the thread as the\n resting one, or the newest message hides behind it while typing. */\n html[data-mobile-keyboard] main:has(.chat-workspace) { padding-bottom: calc(var(--keyboard-inset, 0px) + var(--composer-height, 4rem) + 1.5rem); }\n html[data-mobile-keyboard] main:has(.chat-workspace) :is(input, textarea, button, summary, a) { scroll-margin-block: 6rem calc(var(--keyboard-inset, 0px) + var(--composer-height, 4rem) + 1.5rem); }\n html[data-mobile-keyboard] .chat-new-update-holder { bottom: calc(var(--keyboard-inset, 0px) + var(--composer-height, 4rem) + 1rem); }\n html[data-mobile-keyboard] .sticky-actions { bottom: .5rem; }\n .work-tools { display: block; }\n .work-tools > summary, .work-tools-menu a, .work-views a { min-height: 2.75rem; }\n .work-tools-menu a { display: flex; align-items: center; }\n main :is(input, textarea, button, summary, a) { scroll-margin-block: 6rem calc(var(--composer-height, 4rem) + 5rem); }\n }\n main:has(.mate-mint) { max-width: 68rem; }\n main:has(.mate-mint) > h1 { margin-top: .5rem; font-size: 1.7rem; letter-spacing: -.04em; }\n .mate-mint { position: relative; max-width: 46rem; margin-top: 1.5rem; padding: 1.5rem; border-radius: calc(var(--radius) + 4px); }\n .mate-mint::before {\n content: \"T\"; position: absolute; top: 1.4rem; left: 1.4rem; width: 3rem; height: 3rem;\n display: grid; place-items: center; border: 1px solid var(--glass-border); border-radius: 1rem;\n background: var(--glass-strong);\n box-shadow: var(--so-pill-shadow);\n font: 600 .75rem/1 var(--font-mono); letter-spacing: -.05em;\n }\n .mate-mint > p:first-child { min-height: 3rem; margin: 0; padding: .15rem 0 1.25rem 4rem; font-size: .95rem; }\n .mate-mint > p:first-child strong { display: block; margin-bottom: .2rem; font-size: 1.1rem; letter-spacing: -.02em; }\n .mate-mint form { border-top: 1px solid var(--glass-border); padding-top: .5rem; }\n /* The start act is the page's one primary (UI polish 2026-09-13). */\n .mate-mint form > button[type=submit] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }\n @media (hover: hover) and (pointer: fine) { .mate-mint form > button[type=submit]:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background)); } }\n .mate-terms { display: flex; flex-wrap: wrap; gap: 1rem; align-items: baseline; padding: .35rem 0; }\n .mate-terms .inline-field { white-space: nowrap; }\n button.quiet { background: transparent; color: var(--fg-muted); border-color: var(--border); }\n .answer-options { list-style: none; padding: 0; margin: 0.4rem 0; }\n .answer-options li { padding: 0.35rem 0.6rem; border-left: 1px solid var(--border); margin: 0.25rem 0; }\n .answer-options li.picked { border-left-color: var(--foreground); }\n .shared-action { max-width: 49rem; overflow-wrap: anywhere; }\n .shared-action form { margin-top: 1.5rem; }\n .shared-action label.arm { display: flex; align-items: center; gap: .6rem; min-height: 44px; padding-block: .4rem; }\n .shared-action label.arm input { flex: none; width: 20px; height: 20px; }\n .shared-action button { white-space: nowrap; }\n .proposal label.arm { display: inline-flex; gap: 0.35rem; align-items: center; margin-right: 0.5rem; font-size: 0.85rem; }\n .coordinator-proposals .card { margin: 0.5rem 0; }\n .workspace-head { display: flex; align-items: center; gap: .5rem; min-width: 0; }\n .workspace-head .workspace-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; font-family: var(--font-mono); font-size: .8125rem; }\n .workspace-head .badge { flex: none; }\n .workspace-head form { margin: 0 0 0 auto; }\n .workspace-head form button { min-height: 2rem; padding: 0 .625rem; font-size: .75rem; width: auto; }\n .workspace-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .375rem; margin-top: .625rem; }\n .workspace-stats .pulse-stat {\n color: var(--muted-foreground); font-size: .6875rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n background: var(--muted); border-radius: calc(var(--radius) - 4px); padding: .375rem .5rem; text-align: center;\n }\n .workspace-stats .pulse-stat b { display: block; color: var(--foreground); font-weight: 600; font-family: var(--font-mono); font-size: 1.125rem; line-height: 1.2; font-variant-numeric: tabular-nums; }\n .workspace-stats .pulse-stat.hot b { color: var(--brand); }\n .workspace-bar { display: flex; gap: 2px; height: .375rem; margin-top: .625rem; border-radius: 9999px; overflow: hidden; background: var(--muted); }\n .workspace-bar .seg { flex: 1 1 0; }\n .workspace-bar .seg.attention { background: var(--brand); }\n .workspace-bar .seg.building { background: var(--running); }\n .workspace-bar .seg.waiting { background: var(--muted-foreground); }\n .workspace-bar .seg.queued { background: color-mix(in srgb, var(--muted-foreground) 45%, transparent); }\n .attention-stack { display: grid; gap: .55rem; }\n .attention-stack .decide-card { margin: 0; }\n .attention-stack .q { display: flex; gap: .5rem; align-items: center; justify-content: space-between; }\n .wb-rail-head {\n display: flex; align-items: flex-end; justify-content: space-between; gap: .75rem;\n padding: 0 .15rem .6rem; border-bottom: 1px solid var(--border);\n }\n .wb-rail-head h2 { margin: .1rem 0 0; color: var(--foreground); font-size: .875rem; letter-spacing: -.01em; text-transform: none; font-family: var(--font-sans); }\n .wb-rail-head a { font-size: .75rem; color: var(--muted-foreground); }\n .wb-group { margin-top: 1.05rem; }\n .wb-group > h2 {\n display: flex; align-items: center; justify-content: space-between; margin: 0 .2rem .35rem;\n font-size: .65rem;\n }\n .wb-group > h2 .lane-count { border: 1px solid var(--border); border-radius: 999px; padding: .04rem .42rem; }\n .wb-row {\n display: block; padding: .58rem .65rem; margin: .16rem 0; border: 1px solid transparent;\n border-radius: calc(var(--radius) - 4px); color: inherit; text-decoration: none;\n }\n @media (hover: hover) and (pointer: fine) { .wb-row:hover { background: var(--card); border-color: var(--border); } }\n .wb-row.wb-selected { background: var(--muted); border-color: var(--border); }\n .wb-row .wb-title { display: block; font-size: .8125rem; font-weight: 500; line-height: 1.35; }\n .wb-row .wb-meta { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; margin-top: .22rem; }\n .wb-row .badge { padding: .04rem .42rem; font-size: .65rem; }\n .wb-row .wb-reason { display: block; color: var(--muted-foreground); font-size: .72rem; margin-top: .22rem; line-height: 1.35; }\n @media (max-width: 720px) {\n .control-room-head { display: block; }\n .control-room-head .actions { justify-content: flex-start; margin-top: .75rem; }\n .command-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .workspace-pulse { grid-template-columns: 1fr; }\n .workspace-stats .pulse-stat { padding: .375rem .125rem; font-size: .625rem; letter-spacing: -.01em; }\n .permission-toggle { grid-template-columns: 1fr; }\n .task-intake { margin-top: .25rem; }\n .task-intake-hero { text-align: left; margin-bottom: .9rem; }\n .task-intake-mark { display: none; }\n .task-composer { margin-inline: 0; padding: .55rem; border-radius: 1.15rem; }\n .task-prompt textarea { min-height: 7.25rem; padding: .75rem; font-size: 1rem; }\n .task-composer-footer { flex-wrap: wrap; }\n .task-context { order: 1; flex-basis: calc(100% - 7rem); }\n .task-context-chip:first-child { max-width: 9.5rem; }\n .task-context-chip:nth-child(2) { display: none; }\n .task-quality { order: 2; }\n .task-submit { order: 3; width: 100%; }\n .task-options { order: 4; }\n .task-options-grid { grid-template-columns: 1fr; }\n .task-options-grid .wide, .task-options-grid .permission-field { grid-column: auto; }\n .agents-form { grid-template-columns: 1fr; }\n .agents-roles { grid-template-columns: 1fr; }\n .agents-risk-guide div, .agents-role-row { grid-template-columns: 1fr; }\n .approval-card { padding: 1rem; }\n /* The orientation block says the wait on a phone; the kicker would say it twice. */\n .approval-card .approval-kicker { display: none; }\n .approval-boundaries, .approval-confirm { grid-template-columns: 1fr; }\n /* The approval stays IN FLOW on phones: the password field comes\n first and the button follows it \u2014 never a sticky control floating\n over the field it confirms. */\n .approval-confirm .sticky-actions { position: static; margin-top: 0; padding: 0; border-top: 0; }\n }\n\n /* Runner lanes (queue + fleet): one column per worker. */\n .content > main:has(.lanes) { max-width: none; }\n .lanes {\n display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));\n gap: .625rem; padding-bottom: .75rem; align-items: start;\n }\n @media (max-width: 40rem) {\n .lanes { display: flex; flex-direction: column; }\n .lanes .lane { min-height: 0; }\n }\n .runner-note { width: 100%; }\n .queue-handle {\n display: inline-flex; align-items: center; justify-content: center; flex: none;\n width: 2rem; height: 2rem; margin: -.375rem .125rem -.375rem -.5rem; vertical-align: middle;\n color: var(--muted-foreground); cursor: grab; user-select: none; touch-action: none; border-radius: calc(var(--radius) - 4px);\n }\n .queue-handle svg { width: 1.125rem; height: 1.125rem; }\n .queue-handle:active { cursor: grabbing; background: var(--muted); }\n .icon-button, .inline button.icon-button { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 2.75rem; min-width: 2.75rem; padding: 0; }\n .icon-button svg { width: 1rem; height: 1rem; }\n .queue-card p { margin: 0; }\n .queue-card a { text-decoration: none; }\n @media (hover: hover) and (pointer: fine) { .queue-card a:hover { text-decoration: underline; } }\n .queue-card .row + .row { margin-top: .125rem; border-bottom: none; }\n .queue-card p.row { padding: 0; border-bottom: none; }\n .tracks { margin-top: 1.5rem; }\n .tracks > .hint { margin-bottom: .75rem; }\n .track-row { margin-bottom: .6rem; }\n .track-row p { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }\n .track-row p .right { margin-left: auto; }\n .track-row a { text-decoration: none; }\n @media (hover: hover) and (pointer: fine) { .track-row a:hover { text-decoration: underline; } }\n .track-strip { display: inline-flex; gap: .3rem; align-items: center; vertical-align: middle; }\n .fire {\n display: inline-block; width: .65rem; height: .65rem; border-radius: 50%;\n background: var(--muted);\n }\n .fire-ok { background: var(--success); }\n .fire-bad { background: var(--destructive); }\n .fire-live { background: var(--running); animation: pulse 1.6s ease-in-out infinite; }\n .fire-skip { background: transparent; border: 1.5px solid var(--muted); }\n\n/* the phone: fingers, not cursors \u2014 tested at 320/390px */\ninput, select, textarea { font-size: 16px; }\nbutton { min-height: 44px; }\n@media (max-width: 40rem) {\n form.card button[type=submit], form > button[type=submit] { width: 100%; }\n input[type=text], input[type=password] { width: 100%; max-width: 100%; box-sizing: border-box; }\n main { padding-bottom: calc(1rem + env(safe-area-inset-bottom)); }\n /* Task actions are composed for a thumb, not allowed to wrap according\n to their intrinsic text widths. Every row owns the available width. */\n .task-title-row { display: grid; gap: .6rem; margin-bottom: .8rem; }\n .task-title-row .task-main-title { margin-bottom: 0; }\n .task-title-row .task-view-switch { justify-self: start; }\n .task-main-title { display: flex; align-items: center; flex-wrap: wrap; gap: .3rem .4rem; }\n .dispatch-copy { display: grid; gap: .2rem; }\n .dispatch-copy > strong { line-height: 1.35; }\n .dispatch-action-link, details.dispatch-recovery > summary { width: 100%; box-sizing: border-box; justify-content: center; }\n .proof-review-actions { display: grid; }\n .proof-review-actions > .button-link { width: 100%; box-sizing: border-box; text-align: center; }\n details.proof-exception[open] { width: 100%; }\n .dispatch-recovery-body { padding: .7rem; }\n .dependency-repair-actions { display: grid; grid-template-columns: 1fr; gap: .5rem; margin-top: .75rem; }\n .dependency-repair-actions form { display: flex; width: 100%; max-width: none; margin: 0; }\n .dependency-repair-actions form > button[type=submit] { width: 100%; }\n .dependency-repair-actions .dependency-repair-replace {\n display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .5rem;\n }\n .dependency-repair-actions .dependency-repair-label { width: 100%; min-width: 0; }\n .dependency-repair-actions .dependency-repair-label > select { width: 100%; min-width: 0; max-width: none; }\n .dependency-repair-actions .dependency-repair-replace > button[type=submit] { width: auto; }\n .acts-bar { display: grid; grid-template-columns: minmax(0, 1fr); gap: .5rem; margin: .75rem 0 .35rem; }\n .acts-bar > *, .acts-bar form.inline { min-width: 0; margin: 0; }\n .acts-bar .primary { display: block; width: 100%; }\n .acts-bar .primary form { width: 100%; margin: 0; }\n .acts-bar .primary form.inline > button[type=submit] { width: 100%; margin: 0; }\n .acts-bar .act-hold {\n display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;\n gap: .5rem; width: 100%;\n }\n .acts-bar .act-hold input[type=text] { width: 100%; min-width: 0; margin: 0; }\n .acts-bar .act-hold > button[type=submit] { width: auto; white-space: nowrap; }\n .acts-why { margin: .25rem 0 .85rem; font-size: .75rem; line-height: 1.5; }\n .acts-why-plan { display: none; }\n .task-scope-needed { padding: .85rem 1rem; }\n .task-scope-needed p { margin: .2rem 0; }\n .receipt-head { display: grid; gap: .45rem; }\n .receipt-head .status-line { justify-self: start; }\n .receipt-facts { grid-template-columns: 1fr; }\n .receipt-actions { display: grid; grid-template-columns: 1fr; }\n .receipt-actions .button-link { width: 100%; box-sizing: border-box; text-align: center; }\n /* The cockpit already stacks at 980px (above); only the phone-width\n act card and section padding are decided here. */\n .cockpit-detail { grid-row: 1; }\n .cockpit-queue { grid-row: 2; margin-top: .85rem; padding-top: .85rem; border-top: 1px solid var(--border); border-bottom: 0; }\n .cockpit-next { display: grid; }\n .cockpit-next form, .cockpit-next .button-link { width: 100%; box-sizing: border-box; }\n .cockpit-next form input[type=text] { flex: 1 1 100%; }\n .cockpit-next form button[type=submit] { width: 100%; }\n .cockpit-section { padding: .85rem .8rem; }\n .cockpit-disclosure { padding: 0; }\n .cockpit-disclosure > summary { padding: .8rem; }\n .cockpit-disclosure-body { padding: 0 .8rem .85rem; }\n .cockpit-accept-form { display: grid; }\n .cockpit-accept-form button { width: 100%; }\n .diff-review { margin-inline: -.1rem; }\n .diff-review-bar { padding-left: .7rem; }\n .diff-file > summary { padding-inline: .7rem; }\n .diff-line { grid-template-columns: 2.75rem 2.65rem 2.65rem minmax(max-content, 1fr); min-height: 2.75rem; }\n .diff-annotate, .diff-annotate-space { width: 2.75rem; min-width: 2.75rem; min-height: 2.75rem; }\n .diff-review[data-mode=\"view\"] .diff-line { grid-template-columns: 0 2.65rem 2.65rem minmax(max-content, 1fr); }\n .diff-review[data-mode=\"view\"] .diff-annotate,\n .diff-review[data-mode=\"view\"] .diff-annotate-space { width: 0; min-width: 0; }\n .diff-line code, .diff-gutter { padding-top: .72rem; padding-bottom: .72rem; }\n .diff-comment-target { grid-template-columns: minmax(0, 1fr) 4.75rem; }\n .diff-comment-form button { width: 100%; }\n .revision-from-comments { display: grid; }\n .revision-from-comments button { width: 100%; }\n /* A decision option on a phone: the answer is the full-width thumb\n target; its recommendation and consequence share the line beneath. */\n .decide-option > button { flex: 0 0 100%; width: 100%; }\n .decide-option .badge { flex: none; }\n .decide-option .meta { flex: 1 1 10rem; }\n /* A queue card's controls read as one row under the id: to-front \u00B7 reserve (stretching) \u00B7 move. */\n .queue-card p.row.meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }\n .queue-card p.row.meta > .mono { flex: 0 0 100%; }\n .queue-card .inline { margin: 0; display: inline-flex; align-items: center; gap: .375rem; }\n .queue-card .inline:last-child { flex: 1 1 auto; min-width: 0; }\n .queue-card .inline > button[type=submit]:not(.icon-button) { width: auto; }\n .queue-card .inline select { flex: 1 1 6rem; min-width: 0; width: auto; min-height: 2.75rem; margin: 0; }\n .queue-card .mono { overflow-wrap: anywhere; }\n .queue-card .row + .row { margin-top: .5rem; }\n /* The title sits beside the grip and wraps within its own box; the\n chips flow after it, never above the name. */\n .queue-card p.row > a:first-of-type { flex: 1 1 12rem; min-width: 0; }\n}\n/* A phone reads a long changed line wrapped, never scrolled sideways: the\n line keeps its numbers and marker, the code column takes what is left. */\n@media (max-width: 760px) {\n .diff-lines { overflow-x: visible; }\n .diff-line, .diff-review .diff-line { min-width: 0; grid-template-columns: 2.75rem 2.65rem 2.65rem minmax(0, 1fr); }\n .diff-review[data-mode=\"view\"] .diff-line { grid-template-columns: 0 2.65rem 2.65rem minmax(0, 1fr); }\n .diff-line code { white-space: pre-wrap; overflow-wrap: anywhere; }\n .diff-hunk-head { overflow-x: visible; white-space: pre-wrap; overflow-wrap: anywhere; }\n}\n@media (max-width: 30rem) {\n .dependency-repair-actions .dependency-repair-replace { grid-template-columns: minmax(0, 1fr); }\n .dependency-repair-actions .dependency-repair-replace > button[type=submit] { width: 100%; }\n}\n@media (max-width: 26rem) {\n .acts-bar .act-hold { grid-template-columns: minmax(0, 1fr); }\n .acts-bar .act-hold > button[type=submit] { width: 100%; }\n}\n.next-pager { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .75rem; margin: 0 0 .75rem; }\n.next-pager .skip {\n margin-left: auto; display: inline-flex; align-items: center; justify-content: center; min-height: 2.75rem;\n padding: 0 .875rem; border: 1px solid var(--border); border-radius: 999px;\n text-decoration: none; color: var(--foreground); background: var(--card);\n}\n@media (hover: hover) and (pointer: fine) { .next-pager .skip:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--muted-foreground)); } }\n\n/* Motion: only where a human caused the change \u2014 navigation, presses,\n overlays. Liveness swaps stay instant; the pulse dot is the one \"alive\"\n signal. Everything dies under prefers-reduced-motion; auto-refresh pages\n opt out of the navigation cross-fade separately (see shell()). */\n@view-transition { navigation: auto; }\n/* A page change fades THROUGH, not across: the old page leaves before the new\n one arrives, so two pages' text is never on screen at once. The sidebar,\n the same on both, swaps in place instead of fading (each shell names its\n own, so a page can never carry the name twice). */\n::view-transition-old(root) { animation: so-page-leave 80ms ease-in both; }\n::view-transition-new(root) { animation: so-page-arrive 140ms ease-out 60ms both; }\n@keyframes so-page-leave { to { opacity: 0; } }\n@keyframes so-page-arrive { from { opacity: 0; } }\n.side { view-transition-name: so-nav-page; }\n.so-sidebar { view-transition-name: so-nav-app; }\n::view-transition-group(so-nav-page), ::view-transition-group(so-nav-app),\n::view-transition-old(so-nav-page), ::view-transition-new(so-nav-page),\n::view-transition-old(so-nav-app), ::view-transition-new(so-nav-app) { animation: none; }\n/* UI polish 2026-09-13 \u2014 the motion contract. Feedback transitions run\n 140\u2013200 ms on color, border, shadow, opacity, and transform only; an\n overlay's entrance is at most 220 ms of opacity + transform. Nothing\n animates a width, height, margin, grid track, or blur, and nothing\n pulses for decoration. The reduce block below is universal: every\n animation and transition dies, and no page reads differently for it. */\n@media (prefers-reduced-motion: no-preference) {\n .tabbar a, .side nav a { transition: color .15s, background .15s; }\n button:active { transform: scale(.985); }\n /* Anchor buttons press like buttons; nav links fill instead. The palette\n and shortcuts overlay are keyboard-summoned, so they appear at once. */\n .button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip { transition: transform 160ms var(--so-ease-out); }\n :is(.button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip):active { transform: scale(.985); }\n .chat-workspace.projects-open .chat-projects { animation: rise 200ms ease-out; }\n .chat-workspace.projects-open::before { animation: fade 180ms ease-out; }\n .switcher[open] .switcher-menu { animation: rise 160ms ease-out; }\n @media (hover: hover) and (pointer: fine) {\n .lane-card, .decide-card, .menu-row, .chat-overview-item, .proposal { transition: border-color .15s, transform .15s, box-shadow .15s; }\n .lane-card:hover, .decide-card:hover, .menu-row:hover {\n \n box-shadow: 0 2px 8px -2px rgb(0 0 0 / .35);\n }\n }\n}\n@keyframes rise { from { opacity: 0; transform: translateY(4px); } }\n@keyframes fade { from { opacity: 0; } }\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after { animation: none !important; transition: none !important; }\n ::view-transition-group(*), ::view-transition-old(root), ::view-transition-new(root) { animation: none; }\n button:active, :is(.button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip):active { transform: none; }\n @media (hover: hover) and (pointer: fine) { button:hover, .side nav a:hover, .chat-project-card:hover, .lane-card:hover, .decide-card:hover, .menu-row:hover { transform: none; } }\n}\n\n/* The shortcuts overlay: display-only, toggled by the chrome layer, absent\n from sensitive pages. */\n.kbd-help {\n position: fixed; top: 18vh; left: 50%; transform: translateX(-50%); width: min(26rem, 92vw);\n background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);\n box-shadow: var(--shadow-overlay);\n padding: 1rem 1.25rem; z-index: 50;\n}\n.kbd-help h2 { margin: 0 0 .5rem; font-size: .8125rem; }\n.kbd-help table { width: 100%; border-collapse: collapse; font-size: .8125rem; }\n.kbd-help td { padding: .25rem 0; vertical-align: top; }\n.kbd-help td:first-child { width: 7.5rem; color: var(--muted-foreground); white-space: nowrap; }\n.kbd-help kbd {\n font-family: var(--font-mono); font-size: .75rem; background: var(--muted);\n border: 1px solid var(--border); border-radius: .3rem; padding: .05rem .35rem;\n}\n@media (max-width: 760px) {\n .kbd-help {\n top: auto; bottom: 0; left: 0; right: 0; transform: none; width: auto;\n border-radius: var(--radius) var(--radius) 0 0;\n padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0rem));\n }\n}\n\n/* The tournament comparison: an at-a-glance table and side-by-side cards. */\n.scroll-x { overflow-x: auto; max-width: 100%; }\n.contest-glance { border-collapse: collapse; font-size: .8125rem; min-width: 34rem; margin: .75rem 0; }\n.contest-glance th, .contest-glance td { text-align: left; padding: .375rem .75rem .375rem 0; vertical-align: top; border-bottom: 1px solid var(--border); }\n.contest-glance th { font-weight: 600; }\n.contest-glance tr:last-child td { border-bottom: none; }\n.contest-compare { display: grid; grid-template-columns: 1fr; gap: .75rem; align-items: start; }\n@media (min-width: 1100px) {\n .content > main:has(.contest-compare) { max-width: none; }\n .contest-compare { grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr)); }\n}\n.contest-compare .card { margin: 0; }\n\n/* The per-file comment button: a small real button beside a diff row. */\nbutton.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }\n\n/* Sticky ceremony actions: single-primary-action forms keep their submit\n within thumb reach on phones. Desktop: plain flow. */\n@media (max-width: 760px) {\n .sticky-actions {\n position: sticky; bottom: calc(3.5rem + env(safe-area-inset-bottom, 0rem)); z-index: 20;\n background: var(--background); border-top: 1px solid var(--border);\n padding: .625rem 0; margin-top: .75rem;\n }\n .sticky-actions button { margin: 0; }\n}\n\n/* A time a person reads (when-html.ts): the full stamp on a desk, the short\n one on a phone; the full stamp stays in the title. */\n.so-when-short { display: none; }\n@media (max-width: 760px) {\n .so-when-full { display: none; }\n .so-when-short { display: inline; }\n /* Server pages outside the workspace keep the same phone rhythm. */\n main { padding: var(--so-phone-gap) var(--so-phone-gutter) 3rem; }\n h2 { margin: 1.25rem 0 .375rem; }\n .card { padding: var(--so-phone-card); margin: var(--so-phone-row) 0; }\n .problem { padding: .5rem .625rem; margin: var(--so-phone-row) 0; }\n .meta, .hint { line-height: 1.35; }\n details { margin: var(--so-phone-row) 0; }\n}\n";
|
|
745
|
+
/** Appearance: a three-way segmented switch, one tap per choice. */
|
|
746
|
+
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}";
|
|
747
|
+
/** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
|
|
748
|
+
export declare const PAGE_CSS: string;
|
|
749
|
+
/** The Inbox tabs (console v2): a segmented track of real links; the dot marks a tab with something new, on phones only. */
|
|
750
|
+
export declare const INBOX_TABS_CSS: string;
|
|
751
|
+
export declare const WORKSPACE_STYLE: {
|
|
752
|
+
path: string;
|
|
753
|
+
serve(request: IncomingMessage, response: ServerResponse): void;
|
|
754
|
+
};
|
|
755
|
+
/** Everything the sidebar needs to draw itself for one request. */
|
|
756
|
+
export type Chrome = {
|
|
757
|
+
projectScoped?: boolean;
|
|
758
|
+
active: "code" | "inbox" | "board" | "queue" | "fleet" | "workbench" | "work" | "done" | "activity" | "review" | "system" | "tasks" | "runs" | "caps" | "routines" | "recipes" | "projects" | "flows" | "settings" | "chat" | "people" | "ledger" | "spend" | "mode" | "menu" | "none";
|
|
759
|
+
project: string | null;
|
|
760
|
+
/** The surface's scope for the scope bar — which rows this screen can
|
|
761
|
+
* show. Derived from the ROUTE, not the session: portfolio and fleet are
|
|
762
|
+
* all-project even while a project is open; the board says which of its
|
|
763
|
+
* two modes it is in. Absent = the session default (open project, else
|
|
764
|
+
* all projects). Display only — switching stays POST + CSRF. */
|
|
765
|
+
scope?: "all" | "project" | "board-all";
|
|
766
|
+
/** The saturated inbox count — never a sum of unbounded list reads. */
|
|
767
|
+
inboxCount: number;
|
|
768
|
+
/** The project the Tasks count covers: the request's own project view
|
|
769
|
+
* (the open project, or /work's ?project=), null for every admitted project. */
|
|
770
|
+
inboxProject?: string | null;
|
|
771
|
+
/** The Tasks badge's words, naming what it covers: "3 need you in shop". */
|
|
772
|
+
inboxLabel?: string;
|
|
773
|
+
inboxSaturated: boolean;
|
|
774
|
+
settings: boolean;
|
|
775
|
+
/** This database is a demo sandbox: banner every page, spend fenced. */
|
|
776
|
+
demo?: boolean;
|
|
777
|
+
/** The active operating mode's banner (M1): rides every page scoped to
|
|
778
|
+
* a repo with a live mode — a signed posture is never invisible. */
|
|
779
|
+
modeBanner?: {
|
|
780
|
+
words: string;
|
|
781
|
+
name: string;
|
|
782
|
+
};
|
|
783
|
+
/** Providers whose sign-in stopped working: every page says so, once. */
|
|
784
|
+
signIn?: BrowserSignIn[];
|
|
785
|
+
/** A newer Toolroll: a quiet notice for an operator until they dismiss this version. */
|
|
786
|
+
update?: BrowserUpdateNotice;
|
|
787
|
+
/** An update waiting on something, what, and the action that clears it: every page, for an operator. */
|
|
788
|
+
updateWaiting?: {
|
|
789
|
+
words: string;
|
|
790
|
+
};
|
|
791
|
+
/** The chat tab renders only where chat could ever be allowed. */
|
|
792
|
+
chat?: boolean;
|
|
793
|
+
code?: boolean;
|
|
794
|
+
/** A rendered list pane makes the page master-detail. */
|
|
795
|
+
listPane?: string;
|
|
796
|
+
/** A compact pulse for the currently open workspace. Null in the
|
|
797
|
+
* cross-workspace view; absent only when the store could not answer. */
|
|
798
|
+
projectPeek?: ProjectPeek | null;
|
|
799
|
+
/** The switcher (board pass): every enrolled project inside the ceiling,
|
|
800
|
+
* most recently opened first — the one-tap switch on any screen. */
|
|
801
|
+
projects?: {
|
|
802
|
+
path: string;
|
|
803
|
+
name: string;
|
|
804
|
+
}[];
|
|
805
|
+
/** The session's csrf token, for the switcher's forms; "" when the
|
|
806
|
+
* request has no cookie session (bearer), which renders the switcher
|
|
807
|
+
* inert. */
|
|
808
|
+
csrf?: string;
|
|
809
|
+
/** Where a switch made on this screen returns to. */
|
|
810
|
+
returnTo?: string;
|
|
811
|
+
};
|
|
812
|
+
/**
|
|
813
|
+
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
814
|
+
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
815
|
+
* and correct by cadence rather than by trusting the scheduler's wake
|
|
816
|
+
* sequence to narrate every UI-visible change (Codex board review, finding
|
|
817
|
+
* 3 chose this over SSE). A redirect or auth failure navigates to /login
|
|
818
|
+
* instead of ever inserting the login page into the region (finding 4).
|
|
819
|
+
* The swapped markup is this server's own rendering of the same route —
|
|
820
|
+
* escaped at the sink like every page, fetched same-origin — and the
|
|
821
|
+
* nonce'd CSP refuses to execute anything the region could smuggle.
|
|
822
|
+
*/
|
|
823
|
+
export declare function regionScript(regionId: string, fragmentName: string, everySeconds: number, path?: string): string;
|
|
824
|
+
/**
|
|
825
|
+
* The live transcript's DEDICATED poller (arc 1 §4): a JSON byte-offset
|
|
826
|
+
* protocol, not an HTML fragment — the response is raw sanitized text and
|
|
827
|
+
* the ONLY sink is textContent, so nothing here can become markup. One
|
|
828
|
+
* request in flight, visibility-paused, stops on `final`, and a `replaced`
|
|
829
|
+
* answer restarts from zero with a visible line — never a silent re-read.
|
|
830
|
+
*/
|
|
831
|
+
export declare function transcriptScript(path?: string, elementId?: string): string;
|
|
832
|
+
/**
|
|
833
|
+
* The attended chrome layer: the jump palette and elapsed tickers. Pure
|
|
834
|
+
* navigation — no key ever posts, so the palette cannot approve anything;
|
|
835
|
+
* ceremonies stay POST + password + CSRF, untouched. Reads its index from
|
|
836
|
+
* a non-executable JSON script tag rendered by the same authorized page.
|
|
837
|
+
*/
|
|
838
|
+
/** The attended beat (v28): parameterless, cookie + same-origin proven
|
|
839
|
+
* server-side, renewal-only — any console page keeps the signed-in
|
|
840
|
+
* approver's own sessions live; a hidden tab pauses honestly. Cheap
|
|
841
|
+
* no-op when nothing is open. Shipped ALONE on sensitive pages. */
|
|
842
|
+
export declare function beatScript(enabled?: boolean): string;
|
|
843
|
+
/** The rail's presentation-only state is safe beside a password ceremony:
|
|
844
|
+
* it never reads a field or sends a request, and keeps the chat collapsible
|
|
845
|
+
* from its very first screen while the palette and global keys remain absent. */
|
|
846
|
+
export declare function sidebarScript(): string;
|
|
847
|
+
export declare function chromeScript(beats?: boolean): string;
|
|
848
|
+
/**
|
|
849
|
+
* A page described, not yet rendered (arc 4): every chromed HTML route
|
|
850
|
+
* returns one of these and ONE helper (sendScreen, inside the server)
|
|
851
|
+
* owns the nonce, the palette index, the shortcuts overlay, script
|
|
852
|
+
* composition, and the CSP. Renderers stopped calling shell() themselves
|
|
853
|
+
* so those five things cannot drift apart per route.
|
|
854
|
+
*/
|
|
855
|
+
export type Screen = {
|
|
856
|
+
title: string;
|
|
857
|
+
body: string;
|
|
858
|
+
chrome?: Chrome;
|
|
859
|
+
/** The page's own executable behavior (a region poller, the push
|
|
860
|
+
* enrollment script). fetches: true when it calls fetch — connect-src
|
|
861
|
+
* is granted only then. */
|
|
862
|
+
functional?: {
|
|
863
|
+
script: string;
|
|
864
|
+
fetches?: boolean;
|
|
865
|
+
};
|
|
866
|
+
refreshSeconds?: number;
|
|
867
|
+
/** Render sensitive even when no password field is visible — one-time
|
|
868
|
+
* secrets and judgment calls the classifier cannot see. */
|
|
869
|
+
forceSensitive?: boolean;
|
|
870
|
+
/** Structured conversation; complex guarded forms stay native islands. */
|
|
871
|
+
workspace?: Partial<Pick<BrowserWorkspace, 'conversation' | 'team' | 'focus' | 'result' | 'catchUpHtml' | 'controlsHtml' | 'notices' | 'pageHtml' | 'view' | 'firstRun' | 'phone' | 'home'>>;
|
|
872
|
+
};
|
|
873
|
+
export declare const ROLE_TITLES: Record<"plan" | "build" | "review" | "repair", string>;
|
|
874
|
+
export declare function screen(title: string, body: string, options?: Omit<Screen, "title" | "body">): Screen;
|
|
875
|
+
/**
|
|
876
|
+
* The sensitivity classifier (arc 4, findings 3/17): a body showing a
|
|
877
|
+
* password input renders WITHOUT the chrome additions (palette, overlay,
|
|
878
|
+
* global keys) — the page's own functional script still ships. Tolerant
|
|
879
|
+
* of quoting, casing, and whitespace; `data-type="password"` and prose
|
|
880
|
+
* mentioning passwords do not match. This is defense-in-depth over a
|
|
881
|
+
* file whose only HTML producer is its own double-quoted template
|
|
882
|
+
* convention — forceSensitive is the escape hatch for what a regex
|
|
883
|
+
* cannot judge.
|
|
884
|
+
*/
|
|
885
|
+
export declare const SENSITIVE_INPUT: RegExp;
|
|
886
|
+
/** The design contract every page carries (impeccable direction, 2026-09-27). */
|
|
887
|
+
export declare const DESIGN_CONTRACT = "<!-- THESIS: a control plane that stays quiet until a person is needed; it refuses the dashboard default of coloured status everywhere and an accent on every button.\nOWN-WORLD: a neutral grey frame with paper sheets inset into it (Arc, Linear), Geist for words and Geist Mono for machine facts, ink for every act a person can take, one chart magenta only for what waits on a person plus focus and selection; hairlines and one soft sheet shadow, no glass, no gradients.\nSTORY: glance, see the accent count, open the one thing that needs you, act with the one accent verb, leave.\nFIRST VIEWPORT: sidebar on the frame (accent mark, ink New task, the current page as a raised pill, the accent needs-you count); the main sheet with a 52px header over compact 13px rows; the Crew sheet beside it.\nFORM: the Raycast and Arc canon, user-pinned; seed 9c849086.\nFINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md -->";
|
|
888
|
+
/** The shortcuts overlay: display-only, toggled by the chrome layer,
|
|
889
|
+
* absent from sensitive pages. Navigation help in plain words — no key
|
|
890
|
+
* ever posts. */
|
|
891
|
+
export declare const KBD_HELP: string;
|
|
892
|
+
export declare function shell(title: string, body: string, options?: {
|
|
893
|
+
nav?: boolean;
|
|
894
|
+
chrome?: Chrome;
|
|
895
|
+
/** A password ceremony is on this page: the chrome gains no forms, so
|
|
896
|
+
* the switcher renders inert — the name, and the one /projects link. */
|
|
897
|
+
sensitive?: boolean;
|
|
898
|
+
/** Whether the presentation-only desktop rail control has a nonce'd
|
|
899
|
+
* handler. Sensitive pages may opt in; one-time-secret pages do not. */
|
|
900
|
+
sidebarToggle?: boolean;
|
|
901
|
+
refreshSeconds?: number;
|
|
902
|
+
/** The page's one nonce'd script: region pollers + the chrome layer,
|
|
903
|
+
* composed by sendScreen. Read-only regions only; one nonce per
|
|
904
|
+
* response. fallbackRefresh marks a page whose script POLLS — only
|
|
905
|
+
* those earn the noscript meta-refresh (a chrome-layer-only page with
|
|
906
|
+
* forms must never re-render what someone was typing). */
|
|
907
|
+
live?: {
|
|
908
|
+
nonce: string;
|
|
909
|
+
script: string;
|
|
910
|
+
fallbackRefresh?: boolean;
|
|
911
|
+
};
|
|
912
|
+
}): string;
|
|
913
|
+
/** ONE way a person is named on any surface (U3): the same chip everywhere. */
|
|
914
|
+
export declare function personChip(name: string): string;
|
|
915
|
+
/** The invite's front door: cookie-free, script-free, sensitive by shape.
|
|
916
|
+
* Rendered only for a LIVE token — everything dead gets joinDeadPage. */
|
|
917
|
+
export declare function joinFormPage(token: string, problem: string | null, name: string): string;
|
|
918
|
+
/** Unknown, expired, revoked, consumed, attempts spent: ONE page (D6). */
|
|
919
|
+
export declare function joinDeadPage(): string;
|
|
920
|
+
/**
|
|
921
|
+
* v100: for someone signed in with the identity provider, every step-up
|
|
922
|
+
* password field (autocomplete="current-password"; a secret like a bot token
|
|
923
|
+
* is never one) becomes that sign-in: confirmed, when the provider checked
|
|
924
|
+
* them in the last ten minutes, or a link to be checked again, then back here.
|
|
925
|
+
*/
|
|
926
|
+
export declare function ssoStepUps(html: string, sso: {
|
|
927
|
+
label: string;
|
|
928
|
+
fresh: boolean;
|
|
929
|
+
}, returnTo: string): string;
|
|
930
|
+
/** The brand, as the workspace sidebar shows it. */
|
|
931
|
+
export declare const BRAND_HTML = "<span class=\"so-brand-mark\" aria-hidden=\"true\"><i></i><i></i><i></i></span>Toolroll";
|
|
932
|
+
/** A page on its own, in the workspace's look but with no script: the brand, then the page. */
|
|
933
|
+
export declare function focusDocument(title: string, body: string): string;
|
|
934
|
+
export declare function loginPage(problem: string | null, returnTo?: string, sso?: {
|
|
935
|
+
label: string;
|
|
936
|
+
operatorsOnly: boolean;
|
|
937
|
+
} | null): string;
|
|
938
|
+
/**
|
|
939
|
+
* The page for an address the console doesn't answer to (onboarding): what was opened, where it answers, and the exact
|
|
940
|
+
* command that admits this address. It stands alone (styles inline, no script, no font), because every other asset
|
|
941
|
+
* would be refused at this address too; and it holds nothing a stranger's page doesn't already know.
|
|
942
|
+
*/
|
|
943
|
+
export declare function wrongHostPage(facts: {
|
|
944
|
+
opened: string | null;
|
|
945
|
+
served: string;
|
|
946
|
+
command: string | null;
|
|
947
|
+
}): string;
|
|
948
|
+
/** The first-account page (setup review): shown only while no approver exists. */
|
|
949
|
+
export declare function signupPage(problem: string | null, attemptsLeft: number): string;
|
|
950
|
+
/**
|
|
951
|
+
* The inbox (v3): only things stalling progress without a person, one card
|
|
952
|
+
* per underlying stall, each with its verb inline or one step away. No
|
|
953
|
+
* auto-refresh — approval links lead to the step-up screen, and a page
|
|
954
|
+
* that might hold typed input never re-renders itself.
|
|
955
|
+
*/
|
|
956
|
+
export type InboxTab = "needs-you" | "ready" | "running" | "all";
|
|
957
|
+
export declare const INBOX_TABS: readonly {
|
|
958
|
+
id: InboxTab;
|
|
959
|
+
label: string;
|
|
960
|
+
}[];
|
|
961
|
+
export declare function parseInboxTab(raw: string | null): InboxTab | undefined;
|
|
962
|
+
/** What each tab holds, as a short fingerprint: a tab is unread when its
|
|
963
|
+
* fingerprint differs from the one this browser saw there last. */
|
|
964
|
+
export declare function inboxFingerprints(data: Pick<Parameters<typeof inboxPage>[1], "decisions" | "approvals" | "requeueables" | "cancelledBlockers" | "gaps" | "needsVerification" | "ready" | "running">): Record<InboxTab, string>;
|
|
965
|
+
export declare function inboxPage(chrome: Chrome, data: {
|
|
966
|
+
csrf: string;
|
|
967
|
+
revision: number;
|
|
968
|
+
/** No project open in scoped mode: every admitted project at once,
|
|
969
|
+
* chips on every row, links only — acting means opening the project. */
|
|
970
|
+
rollup: boolean;
|
|
971
|
+
/** A project is OPEN: only then do decisions answer on the card. The
|
|
972
|
+
* legacy unscoped projectless inbox stays links-only too (commit-1
|
|
973
|
+
* review, finding 4) — the partial belongs to a chosen project. */
|
|
974
|
+
interactive: boolean;
|
|
975
|
+
decisions: (Decision & {
|
|
976
|
+
taskId: string;
|
|
977
|
+
repo?: string | null;
|
|
978
|
+
})[];
|
|
979
|
+
approvals: {
|
|
980
|
+
taskId: string;
|
|
981
|
+
title: string;
|
|
982
|
+
goal: string;
|
|
983
|
+
proposedAt: string;
|
|
984
|
+
repo?: string | null; /** v48 authority repair: the closed consent door's title, or null when a yes could bind. */
|
|
985
|
+
closed?: string | null;
|
|
986
|
+
}[];
|
|
987
|
+
requeueables: {
|
|
988
|
+
taskId: string;
|
|
989
|
+
title: string;
|
|
990
|
+
state: TaskState;
|
|
991
|
+
strikes: number;
|
|
992
|
+
incidentCount: number;
|
|
993
|
+
repo?: string | null;
|
|
994
|
+
}[];
|
|
995
|
+
cancelledBlockers: {
|
|
996
|
+
blockerId: string;
|
|
997
|
+
dependentCount: number;
|
|
998
|
+
exampleDependent: string;
|
|
999
|
+
repo?: string | null;
|
|
1000
|
+
blockerRepo?: string | null;
|
|
1001
|
+
}[];
|
|
1002
|
+
gaps: Gap[];
|
|
1003
|
+
/** A completed task whose proof is short or refuted and not yet accepted
|
|
1004
|
+
* (Priority 2) — reads "needs verification" here too, never silently
|
|
1005
|
+
* "done" just because the inbox does not otherwise look at finished work. */
|
|
1006
|
+
needsVerification: {
|
|
1007
|
+
taskId: string;
|
|
1008
|
+
title: string;
|
|
1009
|
+
verdict: "short" | "refuted";
|
|
1010
|
+
repo?: string | null;
|
|
1011
|
+
matrix?: CriterionMatrixRow[];
|
|
1012
|
+
repairChain?: RepairChainRow | null;
|
|
1013
|
+
}[];
|
|
1014
|
+
/** The first-run steps; null once the installation has had its first Ready result. */
|
|
1015
|
+
wizard: FirstRunStep[] | null;
|
|
1016
|
+
/** Whether any worker is answering right now — said at the top when none is. */
|
|
1017
|
+
worker: {
|
|
1018
|
+
answering: number;
|
|
1019
|
+
registered: number;
|
|
1020
|
+
lastHeard: string | null;
|
|
1021
|
+
};
|
|
1022
|
+
now: Date;
|
|
1023
|
+
/** Console v2: the tab shown, results ready to review, work running now,
|
|
1024
|
+
* and the tabs holding something this browser hasn't seen (dots on a phone). */
|
|
1025
|
+
tab?: InboxTab;
|
|
1026
|
+
ready?: {
|
|
1027
|
+
taskId: string;
|
|
1028
|
+
title: string;
|
|
1029
|
+
detail: string;
|
|
1030
|
+
repo: string | null;
|
|
1031
|
+
}[];
|
|
1032
|
+
running?: {
|
|
1033
|
+
taskId: string;
|
|
1034
|
+
title: string;
|
|
1035
|
+
detail: string;
|
|
1036
|
+
repo: string | null;
|
|
1037
|
+
}[];
|
|
1038
|
+
unread?: readonly InboxTab[];
|
|
1039
|
+
}): Screen;
|
|
1040
|
+
/** System: the machinery — workers, background service, workspaces. */
|
|
1041
|
+
export declare function systemPage(chrome: Chrome, data: {
|
|
1042
|
+
agents: {
|
|
1043
|
+
phase: "plan" | "build" | "repair" | "review";
|
|
1044
|
+
provider?: string;
|
|
1045
|
+
model?: string | null;
|
|
1046
|
+
source?: "pinned" | "flag" | "project" | "installation" | "default";
|
|
1047
|
+
problem?: string;
|
|
1048
|
+
setBy: string | null;
|
|
1049
|
+
}[];
|
|
1050
|
+
building: {
|
|
1051
|
+
taskId: string;
|
|
1052
|
+
runner: string;
|
|
1053
|
+
claimedAt: string;
|
|
1054
|
+
expiresAt: string;
|
|
1055
|
+
model: string | null;
|
|
1056
|
+
}[];
|
|
1057
|
+
runners: Runner[];
|
|
1058
|
+
/** v28: open attended sessions per runner. */
|
|
1059
|
+
heldSessions?: Map<string, number>;
|
|
1060
|
+
worktrees: WorktreeRow[];
|
|
1061
|
+
episode: {
|
|
1062
|
+
id: number;
|
|
1063
|
+
startedAt: string;
|
|
1064
|
+
endedAt: string | null;
|
|
1065
|
+
ticks: number;
|
|
1066
|
+
built: number;
|
|
1067
|
+
broke: number;
|
|
1068
|
+
} | null;
|
|
1069
|
+
outboxPending: number;
|
|
1070
|
+
externalWork?: {
|
|
1071
|
+
remoteRepo: string;
|
|
1072
|
+
blocked: string | null;
|
|
1073
|
+
openEpisode: string | null;
|
|
1074
|
+
}[];
|
|
1075
|
+
now: Date;
|
|
1076
|
+
}): Screen;
|
|
1077
|
+
/**
|
|
1078
|
+
* The board: the pipeline as lanes, position as meaning — attention, then
|
|
1079
|
+
* queued, then waiting, then building, then recently done. Read-only by
|
|
1080
|
+
* construction (every card is a link, no forms, no nonces), which is what
|
|
1081
|
+
* makes it safe to re-render itself while somebody watches.
|
|
1082
|
+
*/
|
|
1083
|
+
export declare function boardBody(data: {
|
|
1084
|
+
cards: BoardCard[];
|
|
1085
|
+
tracks: Track[];
|
|
1086
|
+
done: ReturnType<Store["listCompletedWorkScoped"]>;
|
|
1087
|
+
saturated: boolean;
|
|
1088
|
+
now: Date;
|
|
1089
|
+
/** The rolled-up view: every project inside the ceiling at once. */
|
|
1090
|
+
all: boolean;
|
|
1091
|
+
project: string | null;
|
|
1092
|
+
delta: {
|
|
1093
|
+
agoMinutes: number;
|
|
1094
|
+
built: number;
|
|
1095
|
+
failed: number;
|
|
1096
|
+
questions: number;
|
|
1097
|
+
} | null;
|
|
1098
|
+
}, ciRed: (pr: number) => boolean): string;
|
|
1099
|
+
/** Completed work: one row per done task, its final run and PR attached. */
|
|
1100
|
+
export declare function donePage(chrome: Chrome, rows: ReturnType<Store["listCompletedWorkScoped"]>, ciRed: (pr: number) => boolean): Screen;
|
|
1101
|
+
/** One routine's board-facing snapshot — the store's routineTracks row. */
|
|
1102
|
+
export type Track = {
|
|
1103
|
+
routine: Routine;
|
|
1104
|
+
fires: ReturnType<Store["routineFires"]>;
|
|
1105
|
+
spend: {
|
|
1106
|
+
costUsd: number;
|
|
1107
|
+
unmeasuredRuns: number;
|
|
1108
|
+
totalRuns: number;
|
|
1109
|
+
};
|
|
1110
|
+
blocker: {
|
|
1111
|
+
taskId: string;
|
|
1112
|
+
state: string;
|
|
1113
|
+
} | null;
|
|
1114
|
+
};
|
|
1115
|
+
export declare const routineHref: (id: number) => string;
|
|
1116
|
+
export declare function routineStatus(routine: Routine): {
|
|
1117
|
+
text: string;
|
|
1118
|
+
badge: string;
|
|
1119
|
+
};
|
|
1120
|
+
/**
|
|
1121
|
+
* The run-history strip: the last firings as dots, oldest on the left like
|
|
1122
|
+
* a CI history. Fired slots wear their instance's fate; skipped slots are
|
|
1123
|
+
* hollow — recorded absence, not silence.
|
|
1124
|
+
*/
|
|
1125
|
+
export declare function trackStrip(fires: Track["fires"]): string;
|
|
1126
|
+
/** One track row — shared by the board's tracks section and /routines. */
|
|
1127
|
+
export declare function trackRow(track: Track, all: boolean): string;
|
|
1128
|
+
export declare function chatMoney(microusd: number | null): string;
|
|
1129
|
+
export type ChatProjectPulse = {
|
|
1130
|
+
id: string;
|
|
1131
|
+
label: string;
|
|
1132
|
+
path: string;
|
|
1133
|
+
peek: ProjectPeek | null;
|
|
1134
|
+
};
|
|
1135
|
+
/** The browser's one-click approval binds not only the signed scope but the
|
|
1136
|
+
* exact advisory plan revision and any race terms shown beside it. The scope
|
|
1137
|
+
* digest remains the durable authority; this composite makes a stale open
|
|
1138
|
+
* tab fail when somebody edits the plan before approval. */
|
|
1139
|
+
/** The approve POST's refusal, in words a person can act on. */
|
|
1140
|
+
export declare function approveRefusalWords(reason: string): string;
|
|
1141
|
+
export declare function approvalFormDigest(scopeDigest: string, raceDigest: string | null, planSha: string | null): string;
|
|
1142
|
+
/** A task attached by the server to one chat turn. The thread remains the
|
|
1143
|
+
* unified conversation; this is a focused lens, not a second chat silo. */
|
|
1144
|
+
export type TaskChatFocus = {
|
|
1145
|
+
executionId: string;
|
|
1146
|
+
/** A failed task: what its latest attempt missed and what to change (the task page's own words). */
|
|
1147
|
+
failure?: FailureExplanation | null;
|
|
1148
|
+
/** Until this installation's first Ready result, the task shows where it stands on Plan → You approve → Build → Checks → Ready. */
|
|
1149
|
+
guide?: boolean;
|
|
1150
|
+
family: TaskFamily;
|
|
1151
|
+
history: string;
|
|
1152
|
+
status: DisplayStatus;
|
|
1153
|
+
assignment: AssignmentSnapshot | null;
|
|
1154
|
+
id: string;
|
|
1155
|
+
title: string;
|
|
1156
|
+
state: TaskState;
|
|
1157
|
+
project: string | null;
|
|
1158
|
+
now: Date;
|
|
1159
|
+
dispatch: DispatchDiagnosis | null;
|
|
1160
|
+
scope: "none" | "needs approval" | "approved";
|
|
1161
|
+
plan: "requested" | "drafted" | null;
|
|
1162
|
+
/** Adaptive execution plans (v44): the identical ledger/progress
|
|
1163
|
+
* projection the task page renders (c2). */
|
|
1164
|
+
planRevisions: PlanRevisionLedgerView | null;
|
|
1165
|
+
milestoneProgress: MilestoneProgressView[] | null;
|
|
1166
|
+
claimed: boolean;
|
|
1167
|
+
liveRun: {
|
|
1168
|
+
id: number;
|
|
1169
|
+
runner: string;
|
|
1170
|
+
startedAt: string;
|
|
1171
|
+
phase: string | null;
|
|
1172
|
+
} | null;
|
|
1173
|
+
checkProgress: CheckProgress | null;
|
|
1174
|
+
/** v52: the exact-run control — the task page's own projection. */
|
|
1175
|
+
control: TaskControlView;
|
|
1176
|
+
/** The phase route (v47): the task page's exact projection, so chat and
|
|
1177
|
+
* page can never disagree about which agent runs which phase. */
|
|
1178
|
+
route: RouteView | null;
|
|
1179
|
+
/** Approval uses the task page's exact nonce and joint digest. */
|
|
1180
|
+
approval: {
|
|
1181
|
+
scope: Scope;
|
|
1182
|
+
nonce: string;
|
|
1183
|
+
digest: string;
|
|
1184
|
+
planDocument: string | null;
|
|
1185
|
+
/** The drafted plan's contract record (contract handoff, task 1): the
|
|
1186
|
+
* same panel the task page and /next show inside the ceremony. */
|
|
1187
|
+
planContract: PlanContractView | null;
|
|
1188
|
+
deliverable: "branch" | "report";
|
|
1189
|
+
raceTerms: TournamentTerms | null;
|
|
1190
|
+
revision: RevisionView | null;
|
|
1191
|
+
coordinator: {
|
|
1192
|
+
label: string;
|
|
1193
|
+
filedAgo: string | null;
|
|
1194
|
+
} | null;
|
|
1195
|
+
/** The repair chain row when this task is a machine-drafted repair. */
|
|
1196
|
+
repairChain: RepairChainRow | null;
|
|
1197
|
+
} | null;
|
|
1198
|
+
decisions: (Decision & {
|
|
1199
|
+
taskId: string;
|
|
1200
|
+
repo: string | null;
|
|
1201
|
+
})[];
|
|
1202
|
+
publication: Publication | null;
|
|
1203
|
+
/** The same compact, evidence-backed receipt shown on the task page.
|
|
1204
|
+
* Chat is a lens over durable workflow state, never a second copy. */
|
|
1205
|
+
result: CompletionReceiptView | null;
|
|
1206
|
+
};
|
|
1207
|
+
export declare const taskChatHref: (taskId: string) => string;
|
|
1208
|
+
/** The task composer's modes (console v2), as the words the lead reads for that one turn. */
|
|
1209
|
+
export type TaskComposerMode = "build" | "plan" | "answer";
|
|
1210
|
+
export declare const TASK_COMPOSER_MODES: Record<TaskComposerMode, string>;
|
|
1211
|
+
/** A project's own lead thread (v77). */
|
|
1212
|
+
export declare const projectChatHref: (repo: string) => string;
|
|
1213
|
+
/** The one task-level road from a truthful dispatch diagnosis to its nearest
|
|
1214
|
+
* existing repair. This is navigation, never authority: every destination
|
|
1215
|
+
* still owns its original confirmation, password, CSRF, and transactional
|
|
1216
|
+
* checks. Keeping the map here also means the focused chat and task overview
|
|
1217
|
+
* cannot send a person to different fixes for the same gate. */
|
|
1218
|
+
export declare function taskRecoveryHref(taskId: string, diagnosis: DispatchDiagnosis | null): string | null;
|
|
1219
|
+
/** The resume ceremony's digest (v52): the exact run, its settlement, and
|
|
1220
|
+
* the scope approval as it stands — a nonce minted over one set of facts
|
|
1221
|
+
* cannot confirm a resume under another. */
|
|
1222
|
+
export declare function resumeDigestOf(taskId: string, runId: number, settledAt: string, scope: Scope | null): string;
|
|
1223
|
+
export declare const stopWhen: (iso: string) => string;
|
|
1224
|
+
/**
|
|
1225
|
+
* The exact-run control (v52): ONE component on the task page, the
|
|
1226
|
+
* focused chat, and the status fragment, so every surface names the same
|
|
1227
|
+
* run and shows the same state. Stop is one guarded post naming the run;
|
|
1228
|
+
* Stopping… is a disabled control with who asked and when; Resume opens
|
|
1229
|
+
* the password ceremony; a stopped review points at Review again. Forms
|
|
1230
|
+
* carry the CSRF token and the exact run id, nothing else.
|
|
1231
|
+
*/
|
|
1232
|
+
/** Presentation only: the same projected words and action on all task
|
|
1233
|
+
* surfaces. Approval keeps its action on the exact-terms disclosure. */
|
|
1234
|
+
export declare function taskStatusCard(status: DisplayStatus & {
|
|
1235
|
+
diagnostics?: WorkStatus["diagnostics"];
|
|
1236
|
+
}, taskId: string, dispatch: DispatchDiagnosis | null, runId: number | null, approvalAction?: boolean): string;
|
|
1237
|
+
export declare function checkProgressHtml(progress: CheckProgress | null, id?: string): string;
|
|
1238
|
+
export declare function taskControlDetailsHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean): string;
|
|
1239
|
+
/** `stopElsewhere`: the Building card already carries this exact run's Stop, so the control card keeps only its details. */
|
|
1240
|
+
export declare function taskControlHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean): string;
|
|
1241
|
+
/** The resume confirmation (v52): the exact run restated, what resuming
|
|
1242
|
+
* does and does not do, the gate that would still keep work from
|
|
1243
|
+
* starting, and the password typed again. */
|
|
1244
|
+
export declare function resumeCeremonyPage(chrome: Chrome, data: {
|
|
1245
|
+
taskId: string;
|
|
1246
|
+
taskTitle: string;
|
|
1247
|
+
control: TaskControlView & {
|
|
1248
|
+
kind: "paused";
|
|
1249
|
+
};
|
|
1250
|
+
nonceValue: string;
|
|
1251
|
+
csrf: string;
|
|
1252
|
+
returnTo: "task" | "chat";
|
|
1253
|
+
gate: DispatchDiagnosis | null;
|
|
1254
|
+
approved: boolean;
|
|
1255
|
+
}): Screen;
|
|
1256
|
+
export declare function taskViewSwitch(taskId: string, active: "overview" | "ask"): string;
|
|
1257
|
+
export declare function taskChatContext(focus: TaskChatFocus): string;
|
|
1258
|
+
export declare function taskChatApproval(focus: TaskChatFocus, csrf: string): string;
|
|
1259
|
+
/** The chat's status sentence, worded as the task page words it: what a failed attempt missed, or which step a live
|
|
1260
|
+
* build is on (and the step it is stuck on). Undefined keeps the shared status sentence. */
|
|
1261
|
+
export declare function chatStatusSentence(focus: TaskChatFocus): string | undefined;
|
|
1262
|
+
/** One live, server-derived journey from request to proof. The fragment is
|
|
1263
|
+
* safe to refresh independently, so an in-progress message is never lost. */
|
|
1264
|
+
export declare function taskChatLiveRegion(focus: TaskChatFocus, csrf: string, fragment?: boolean, inert?: boolean, approvalHref?: string): string;
|
|
1265
|
+
/** The first task's way to Ready, filled in as it moves (onboarding): the same stage every surface reads. */
|
|
1266
|
+
export declare function firstTaskJourneyHtml(focus: TaskChatFocus): string;
|
|
1267
|
+
export declare function taskChatHeading(focus: TaskChatFocus): string;
|
|
1268
|
+
export declare function chatWorkspace(content: string, projects: readonly ChatProjectPulse[], csrf: string, inert: boolean, focus: TaskChatFocus | null, resultPanel?: string | null): string;
|
|
1269
|
+
/** The folded overview's one-line summary (UI polish 2026-09-13): the
|
|
1270
|
+
* two counts a phone reader scans before deciding to open it. */
|
|
1271
|
+
export declare function chatOverviewSummaryHtml(projects: readonly ChatProjectPulse[], attention?: {
|
|
1272
|
+
count: number;
|
|
1273
|
+
saturated: boolean;
|
|
1274
|
+
}): string;
|
|
1275
|
+
/** A live, server-derived portfolio card. It is deliberately independent
|
|
1276
|
+
* of the model's prose: the numbers and links always reflect the current
|
|
1277
|
+
* control plane, while chat remains the place to ask what they mean. */
|
|
1278
|
+
export type AssignmentChatSnapshot = ChatSnapshot & {
|
|
1279
|
+
assignmentStates?: Record<string, {
|
|
1280
|
+
state: AssignmentSnapshot['state'];
|
|
1281
|
+
label: string;
|
|
1282
|
+
detail: string;
|
|
1283
|
+
}>;
|
|
1284
|
+
attentionCount?: {
|
|
1285
|
+
count: number;
|
|
1286
|
+
saturated: boolean;
|
|
1287
|
+
};
|
|
1288
|
+
};
|
|
1289
|
+
export declare function chatFleetOverview(snapshot: AssignmentChatSnapshot | null, projects: readonly ChatProjectPulse[], csrf: string, interactive: boolean): string;
|
|
1290
|
+
/**
|
|
1291
|
+
* The mate's project rail: one bounded pulse per admitted project, plus
|
|
1292
|
+
* two roads that preserve the plane's contracts. "ask" sends the stable
|
|
1293
|
+
* rN alias the model already sees; "board" uses the existing POST switch
|
|
1294
|
+
* instead of smuggling a project change through a GET parameter.
|
|
1295
|
+
*/
|
|
1296
|
+
export declare function chatProjectRail(projects: readonly ChatProjectPulse[], csrf: string, inert: boolean): string;
|
|
1297
|
+
/** Spend-authorized one-click questions: ordinary /chat posts, not a new door. */
|
|
1298
|
+
export declare function matePromptStarters(csrf: string, focus?: TaskChatFocus | null): string;
|
|
1299
|
+
export declare function chatHeading(copy: string, projectCount: number, showProjectToggle?: boolean): string;
|
|
1300
|
+
/** A deliberately small rich-text grammar for model copy. The reply is shaped first (the lead's voice, enforced:
|
|
1301
|
+
* no headers, at most three bold anchors, labelled links, no internal ids unless `asked` wanted them); input is
|
|
1302
|
+
* escaped before tags are introduced: bullets, numbered steps, bold, inline code and labelled http(s) links are
|
|
1303
|
+
* presentation only—never executable HTML. */
|
|
1304
|
+
export declare function renderChatText(raw: string, asked?: string): string;
|
|
1305
|
+
export declare function chatActivity(activity: string | null): string;
|
|
1306
|
+
export declare const CHAT_UI_SCRIPT: string;
|
|
1307
|
+
/** Provider, model, and limits under ONE disclosure (UI polish
|
|
1308
|
+
* 2026-09-13): the facts stay one tap away on every chat surface, and a
|
|
1309
|
+
* membership never shows a dollar figure as if it were a charge. */
|
|
1310
|
+
export declare function chatLimitsHtml(facts: {
|
|
1311
|
+
provider: string;
|
|
1312
|
+
model: string;
|
|
1313
|
+
turnsToday: number;
|
|
1314
|
+
dailyTurns: number;
|
|
1315
|
+
subscription: boolean;
|
|
1316
|
+
weekly: {
|
|
1317
|
+
spent: number;
|
|
1318
|
+
ceiling: number;
|
|
1319
|
+
} | null;
|
|
1320
|
+
}): string;
|
|
1321
|
+
/** What the lead's settings form shows beside the saved configuration: where keys come from (never the keys), the
|
|
1322
|
+
* live model lists, and where saving goes back to. */
|
|
1323
|
+
export type LeadFormFacts = {
|
|
1324
|
+
keyFacts: {
|
|
1325
|
+
provider: string;
|
|
1326
|
+
state: "environment" | "stored" | "none";
|
|
1327
|
+
tail: string | null;
|
|
1328
|
+
}[];
|
|
1329
|
+
openrouterModels: string[] | null;
|
|
1330
|
+
liveModels?: {
|
|
1331
|
+
value: string;
|
|
1332
|
+
label: string;
|
|
1333
|
+
}[];
|
|
1334
|
+
csrf: string;
|
|
1335
|
+
returnTo: string;
|
|
1336
|
+
};
|
|
1337
|
+
/** Settings → Lead: one line for what runs the lead and one action; the full form, turning it off and stored keys under Advanced. */
|
|
1338
|
+
export declare function leadSettingsHtml(data: {
|
|
1339
|
+
config: import("../store.js").ChatConfig | null;
|
|
1340
|
+
facts: LeadFormFacts;
|
|
1341
|
+
words: string | null;
|
|
1342
|
+
signedIn: string | null;
|
|
1343
|
+
command: string;
|
|
1344
|
+
said: string | null;
|
|
1345
|
+
saved?: boolean;
|
|
1346
|
+
identity?: LeadIdentity;
|
|
1347
|
+
about?: string[];
|
|
1348
|
+
aboutSaved?: boolean;
|
|
1349
|
+
promises?: {
|
|
1350
|
+
id: number;
|
|
1351
|
+
what: string;
|
|
1352
|
+
when: string;
|
|
1353
|
+
until: string;
|
|
1354
|
+
}[];
|
|
1355
|
+
}): string;
|
|
1356
|
+
/** The lead's full settings (Settings → Lead → Advanced): provider, model, limits and a direct API key. */
|
|
1357
|
+
export declare function leadConfigForm(current: import("../store.js").ChatConfig | null, data: LeadFormFacts): string;
|
|
1358
|
+
export declare function chatPage(chrome: Chrome, data: {
|
|
1359
|
+
enabled: ({
|
|
1360
|
+
ok: true;
|
|
1361
|
+
} & Record<string, unknown>) | {
|
|
1362
|
+
ok: false;
|
|
1363
|
+
why: string;
|
|
1364
|
+
};
|
|
1365
|
+
pending: ChatTurn | null;
|
|
1366
|
+
latched: ChatTurn[];
|
|
1367
|
+
chat: {
|
|
1368
|
+
candidates: Map<string, {
|
|
1369
|
+
key: string;
|
|
1370
|
+
draft: ChatDraft;
|
|
1371
|
+
repoPath: string;
|
|
1372
|
+
}>;
|
|
1373
|
+
lastTurn: {
|
|
1374
|
+
id: number;
|
|
1375
|
+
reply: string | null;
|
|
1376
|
+
staticError: string | null;
|
|
1377
|
+
proposalsDiscarded: boolean;
|
|
1378
|
+
} | null;
|
|
1379
|
+
} | null;
|
|
1380
|
+
recent: ChatTurn[];
|
|
1381
|
+
turnsToday: number;
|
|
1382
|
+
weeklySpent: number;
|
|
1383
|
+
projects: ChatProjectPulse[];
|
|
1384
|
+
fleetSnapshot: AssignmentChatSnapshot | null;
|
|
1385
|
+
catchUp?: string;
|
|
1386
|
+
/** Optional task lens into the same unified conversation. */
|
|
1387
|
+
focusTask: TaskChatFocus | null;
|
|
1388
|
+
canManage: boolean;
|
|
1389
|
+
config: import("../store.js").ChatConfig | null;
|
|
1390
|
+
/** Where each provider's key comes from — never the key itself. */
|
|
1391
|
+
keyFacts: {
|
|
1392
|
+
provider: string;
|
|
1393
|
+
state: "environment" | "stored" | "none";
|
|
1394
|
+
tail: string | null;
|
|
1395
|
+
}[];
|
|
1396
|
+
/** OpenRouter's live catalog when the key is present and reachable. */
|
|
1397
|
+
openrouterModels: string[] | null;
|
|
1398
|
+
/** Claude and Codex models from the saved live catalog, labelled with prices. */
|
|
1399
|
+
liveModels?: {
|
|
1400
|
+
value: string;
|
|
1401
|
+
label: string;
|
|
1402
|
+
}[];
|
|
1403
|
+
csrf: string;
|
|
1404
|
+
problem: string | null;
|
|
1405
|
+
/** The card that mints a mate session (mate arc §5), approvers only. */
|
|
1406
|
+
mateMint?: string;
|
|
1407
|
+
/** Pending coordinator proposals as cards (mate arc v3), approvers only. */
|
|
1408
|
+
coordinatorProposals?: string;
|
|
1409
|
+
/** The task's result detail (package 3), when the URL opened one. */
|
|
1410
|
+
resultPanel?: string | null;
|
|
1411
|
+
/** Opened from "Chat settings": show them expanded. */
|
|
1412
|
+
settingsOpen?: boolean;
|
|
1413
|
+
/** Chat's first run is on the page: it already says what to do while there is no lead. */
|
|
1414
|
+
firstRunShown?: boolean;
|
|
1415
|
+
}): Screen;
|
|
1416
|
+
export declare function chatAckPage(chrome: Chrome, turn: ChatTurn, nonce: string, csrf: string): Screen;
|
|
1417
|
+
/** The card that starts a conversation: the one password ceremony (mate arc §1). */
|
|
1418
|
+
export declare function mateMintCard(csrf: string, enabled: {
|
|
1419
|
+
billing: "metered" | "subscription";
|
|
1420
|
+
config: {
|
|
1421
|
+
provider: ChatProviderId;
|
|
1422
|
+
weeklyCeilingMicrousd: number;
|
|
1423
|
+
};
|
|
1424
|
+
}, returnTo?: string): string;
|
|
1425
|
+
/** The decisions the cards on a page name, for the answer card's consequences and the builder's recommendation. */
|
|
1426
|
+
export declare function decisionsFor(store: Store, proposals: readonly {
|
|
1427
|
+
kind: string;
|
|
1428
|
+
payload: Record<string, unknown>;
|
|
1429
|
+
}[]): Map<number, Decision>;
|
|
1430
|
+
export type ProposalCardView = {
|
|
1431
|
+
id: number;
|
|
1432
|
+
kind: MateProposal["kind"];
|
|
1433
|
+
payload: Record<string, unknown>;
|
|
1434
|
+
state: string;
|
|
1435
|
+
outcome: Record<string, unknown> | null;
|
|
1436
|
+
/** Who proposed: the mate, or a coordinator by name. */
|
|
1437
|
+
by: {
|
|
1438
|
+
mate: true;
|
|
1439
|
+
} | {
|
|
1440
|
+
mate: false;
|
|
1441
|
+
name: string;
|
|
1442
|
+
ago: string;
|
|
1443
|
+
};
|
|
1444
|
+
/** Where confirm/dismiss post: `/chat/proposal` for the mate's, `/proposals` for a coordinator's. */
|
|
1445
|
+
actionBase: string;
|
|
1446
|
+
};
|
|
1447
|
+
/**
|
|
1448
|
+
* A proposal card: what, then confirm/dismiss, or the door's answer. An
|
|
1449
|
+
* `answer` card shows the question, every option WITH its consequence,
|
|
1450
|
+
* the builder's recommendation beside the proposer's pick, and — for an
|
|
1451
|
+
* irreversible option — the explicit confirmation field the decision
|
|
1452
|
+
* page itself uses (ruling 12).
|
|
1453
|
+
*/
|
|
1454
|
+
export declare function proposalCard(view: ProposalCardView, csrf: string, inert: boolean, decision: Decision | null, returnTo?: string | null): string;
|
|
1455
|
+
/** The card's HTML and, for the chat's confirm-in-place cards, the same
|
|
1456
|
+
* card as data: the body is the HTML's own, the act is the same door. */
|
|
1457
|
+
export declare function proposalCardParts(view: ProposalCardView, csrf: string, inert: boolean, decision: Decision | null, returnTo?: string | null): {
|
|
1458
|
+
html: string;
|
|
1459
|
+
card: BrowserActionCard;
|
|
1460
|
+
};
|
|
1461
|
+
export declare function mateProposalCard(proposal: MateProposal, csrf: string, inert: boolean, decision: Decision | null, returnTo?: string | null): string;
|
|
1462
|
+
export declare function mateProposalCardParts(proposal: MateProposal, csrf: string, inert: boolean, decision: Decision | null, returnTo?: string | null): {
|
|
1463
|
+
html: string;
|
|
1464
|
+
card: BrowserActionCard;
|
|
1465
|
+
};
|
|
1466
|
+
/** Review and inline team cards share the same server-decided controls and reasons. */
|
|
1467
|
+
export declare function teamProposalCardParts(store: Store, actor: {
|
|
1468
|
+
name: string;
|
|
1469
|
+
generation: number;
|
|
1470
|
+
}, proposal: MateProposal, snapshot: TeamSnapshot, csrf: string, decision: Decision | null, now: Date, provider: TeamChatProviderResolver): {
|
|
1471
|
+
html: string;
|
|
1472
|
+
card: BrowserActionCard;
|
|
1473
|
+
};
|
|
1474
|
+
export declare function coordinatorProposalCard(proposal: CoordinatorProposal, csrf: string, now: Date, decision: Decision | null, returnTo?: string | null): string;
|
|
1475
|
+
export declare function relativeAge(iso: string, now: Date): string;
|
|
1476
|
+
/** The section shared by /chat (both modes) and the task page: pending coordinator proposals as cards. */
|
|
1477
|
+
export declare function coordinatorProposalsSection(proposals: readonly CoordinatorProposal[], decisions: Map<number, Decision>, csrf: string, now: Date, heading?: boolean, returnTo?: string | null): string;
|
|
1478
|
+
/** The lead thread a change in reachable projects closed (mate arc ruling 9): its saved words and card outcomes, never continued. */
|
|
1479
|
+
export type ReplacedThread = {
|
|
1480
|
+
messages: MateMessage[];
|
|
1481
|
+
proposals: MateProposal[];
|
|
1482
|
+
decisions: Map<number, Decision>;
|
|
1483
|
+
};
|
|
1484
|
+
export declare const REPLACED_THREAD_DIVIDER = "New conversation \u2014 the projects I can reach changed";
|
|
1485
|
+
/** The replaced thread as the React conversation renders it: words and card outcomes, no buttons. */
|
|
1486
|
+
export declare function replacedBrowserMessages(previous: ReplacedThread, csrf: string): import("../browser-workspace.js").BrowserMessage[];
|
|
1487
|
+
/** The replaced thread for the server-rendered page: read only, then the divider. */
|
|
1488
|
+
export declare function replacedThreadHtml(previous: ReplacedThread | null | undefined): string;
|
|
1489
|
+
export type MateThreadRows = {
|
|
1490
|
+
/** The lead thread a change in reachable projects replaced; shown only. */
|
|
1491
|
+
previous?: ReplacedThread | null;
|
|
1492
|
+
messages: MateMessage[];
|
|
1493
|
+
proposals: MateProposal[];
|
|
1494
|
+
/** The decisions the answer cards name. */
|
|
1495
|
+
decisions: Map<number, Decision>;
|
|
1496
|
+
coordinatorProposals: CoordinatorProposal[];
|
|
1497
|
+
pending: MateTurn | null;
|
|
1498
|
+
recent: MateTurn[];
|
|
1499
|
+
/** Optional task lens into the same unified thread. */
|
|
1500
|
+
focusTask: TaskChatFocus | null;
|
|
1501
|
+
/** The lead's open question to this reader, drawn as buttons under its reply. */
|
|
1502
|
+
ask?: MateAsk | null;
|
|
1503
|
+
/** Every question the lead asked in these messages, by turn: answered ones show their words without buttons. */
|
|
1504
|
+
asks?: Map<number, MateAsk>;
|
|
1505
|
+
};
|
|
1506
|
+
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
1507
|
+
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
1508
|
+
export declare function mateAskHtml(ask: MateAsk, csrf: string, target: {
|
|
1509
|
+
task: string | null;
|
|
1510
|
+
project: string | null;
|
|
1511
|
+
}, composer: string, open?: boolean): string;
|
|
1512
|
+
/** The thread's messages as the React conversation renders them, each
|
|
1513
|
+
* with the proposal cards its turn produced; `back` is where a card's
|
|
1514
|
+
* confirm or dismiss returns. */
|
|
1515
|
+
export declare function mateBrowserMessages(rows: Pick<MateThreadRows, "messages" | "proposals" | "decisions" | "pending" | "ask" | "asks">, csrf: string, back: string | null, target?: {
|
|
1516
|
+
task: string | null;
|
|
1517
|
+
project: string | null;
|
|
1518
|
+
}): import("../browser-workspace.js").BrowserMessage[];
|
|
1519
|
+
/** The version of the DISPLAYED conversation (package 2): every fact a
|
|
1520
|
+
* thread or task fragment renders — message identities, card states and
|
|
1521
|
+
* outcomes, the live turn and its step count, the last turn's state, the
|
|
1522
|
+
* decisions the cards name, and the task lens's own facts — and nothing
|
|
1523
|
+
* that merely ticks: no relative age, no freshly minted nonce, no csrf.
|
|
1524
|
+
* Equal versions mean equal fragments; the poll fetches nothing else. */
|
|
1525
|
+
export declare function mateChatVersion(rows: MateThreadRows): string;
|
|
1526
|
+
/** The thread region — the one safe region the live refresh reconciles
|
|
1527
|
+
* (package 2): coordinator cards, the operator/assistant messages with
|
|
1528
|
+
* their cards, the reply-in-progress card, and the starters that follow a
|
|
1529
|
+
* populated thread. Every child carries a stable `data-key` so the page
|
|
1530
|
+
* keeps an unchanged node (its open disclosures, its focus) and replaces
|
|
1531
|
+
* only what the server rendered differently. The composer is never here. */
|
|
1532
|
+
export declare function mateThreadHtml(data: MateThreadRows & {
|
|
1533
|
+
csrf: string;
|
|
1534
|
+
now: Date;
|
|
1535
|
+
problem: string | null;
|
|
1536
|
+
chatProject?: string | null;
|
|
1537
|
+
}): string;
|
|
1538
|
+
/** What follows the composer (package 2): the starters of an EMPTY thread
|
|
1539
|
+
* sit under the box; once the first message lands they move above it. */
|
|
1540
|
+
export declare function mateAfterComposerHtml(data: {
|
|
1541
|
+
messages: MateMessage[];
|
|
1542
|
+
pending: MateTurn | null;
|
|
1543
|
+
focusTask: TaskChatFocus | null;
|
|
1544
|
+
csrf: string;
|
|
1545
|
+
}): string;
|
|
1546
|
+
export declare function matePage(chrome: Chrome, data: MateThreadRows & {
|
|
1547
|
+
session: MateSession;
|
|
1548
|
+
/** A project's own thread (v77); null for the lead conversation or a task. */
|
|
1549
|
+
chatProject?: string | null;
|
|
1550
|
+
follow?: {
|
|
1551
|
+
enabled: boolean;
|
|
1552
|
+
detail: string;
|
|
1553
|
+
};
|
|
1554
|
+
resultRunId?: number | null;
|
|
1555
|
+
latched: ChatTurn[];
|
|
1556
|
+
config: import("../store.js").ChatConfig;
|
|
1557
|
+
turnsToday: number;
|
|
1558
|
+
weeklySpent: number;
|
|
1559
|
+
projects: ChatProjectPulse[];
|
|
1560
|
+
fleetSnapshot: AssignmentChatSnapshot | null;
|
|
1561
|
+
catchUp?: string;
|
|
1562
|
+
csrf: string;
|
|
1563
|
+
problem: string | null;
|
|
1564
|
+
now: Date;
|
|
1565
|
+
/** The task's result detail (package 3), when the URL opened one. */
|
|
1566
|
+
resultPanel?: string | null;
|
|
1567
|
+
}): Screen;
|
|
1568
|
+
export declare function routinesPage(chrome: Chrome, tracks: Track[], form: {
|
|
1569
|
+
csrf: string;
|
|
1570
|
+
revision: number;
|
|
1571
|
+
problem: string | null;
|
|
1572
|
+
values?: URLSearchParams;
|
|
1573
|
+
prefill?: {
|
|
1574
|
+
name: string;
|
|
1575
|
+
goal: string;
|
|
1576
|
+
not: string;
|
|
1577
|
+
touches: string;
|
|
1578
|
+
schedule: string;
|
|
1579
|
+
acceptance: string;
|
|
1580
|
+
} | null;
|
|
1581
|
+
}): Screen;
|
|
1582
|
+
export declare function routineScreenPage(chrome: Chrome, data: {
|
|
1583
|
+
routine: Routine;
|
|
1584
|
+
fires: Track["fires"];
|
|
1585
|
+
spend: Track["spend"];
|
|
1586
|
+
blocker: Track["blocker"];
|
|
1587
|
+
csrf: string;
|
|
1588
|
+
nonce: string;
|
|
1589
|
+
problem: string | null;
|
|
1590
|
+
now: Date;
|
|
1591
|
+
}): Screen;
|
|
1592
|
+
export declare function homePage(chrome: Chrome, data: {
|
|
1593
|
+
csrf: string;
|
|
1594
|
+
taskCount: number;
|
|
1595
|
+
repo: string | null;
|
|
1596
|
+
building: {
|
|
1597
|
+
taskId: string;
|
|
1598
|
+
runner: string;
|
|
1599
|
+
claimedAt: string;
|
|
1600
|
+
expiresAt: string;
|
|
1601
|
+
model: string | null;
|
|
1602
|
+
}[];
|
|
1603
|
+
runners: Runner[];
|
|
1604
|
+
/** v28: open attended sessions per runner. */
|
|
1605
|
+
heldSessions?: Map<string, number>;
|
|
1606
|
+
worktrees: WorktreeRow[];
|
|
1607
|
+
episode: {
|
|
1608
|
+
id: number;
|
|
1609
|
+
startedAt: string;
|
|
1610
|
+
endedAt: string | null;
|
|
1611
|
+
ticks: number;
|
|
1612
|
+
built: number;
|
|
1613
|
+
broke: number;
|
|
1614
|
+
} | null;
|
|
1615
|
+
summary: ReturnType<typeof tally<Run & {
|
|
1616
|
+
taskId: string;
|
|
1617
|
+
}>>;
|
|
1618
|
+
decisions: (Decision & {
|
|
1619
|
+
taskId: string;
|
|
1620
|
+
})[];
|
|
1621
|
+
incidents: (Incident & {
|
|
1622
|
+
taskId: string;
|
|
1623
|
+
})[];
|
|
1624
|
+
stranded: {
|
|
1625
|
+
id: string;
|
|
1626
|
+
blockedBy: string[];
|
|
1627
|
+
}[];
|
|
1628
|
+
gaps: Gap[] | null;
|
|
1629
|
+
outboxPending: number;
|
|
1630
|
+
settings: boolean;
|
|
1631
|
+
now: Date;
|
|
1632
|
+
}): Screen;
|
|
1633
|
+
export type TaskComposerPrefill = {
|
|
1634
|
+
title: string;
|
|
1635
|
+
goal: string;
|
|
1636
|
+
not: string;
|
|
1637
|
+
touches: string;
|
|
1638
|
+
acceptance: string;
|
|
1639
|
+
values?: URLSearchParams;
|
|
1640
|
+
};
|
|
1641
|
+
/** The one front door for new work. The common path is one prompt and one
|
|
1642
|
+
* button; the detailed contract remains available in-place for templates,
|
|
1643
|
+
* experts, and the rare task that should skip repository-aware planning. */
|
|
1644
|
+
/** Where new work goes when no project is open: a choice from the projects
|
|
1645
|
+
* this person already has, never a typed path; a new project is one link
|
|
1646
|
+
* away. Same-named checkouts show their parent folder to tell them apart. */
|
|
1647
|
+
export declare function projectPickerHtml(projects: {
|
|
1648
|
+
path: string;
|
|
1649
|
+
name: string;
|
|
1650
|
+
}[], chosen: string): string;
|
|
1651
|
+
export declare function taskComposerHtml(data: {
|
|
1652
|
+
csrf: string;
|
|
1653
|
+
project: string | null;
|
|
1654
|
+
projectRevision?: number;
|
|
1655
|
+
prefill?: TaskComposerPrefill | null;
|
|
1656
|
+
candidates?: {
|
|
1657
|
+
id: string;
|
|
1658
|
+
title: string;
|
|
1659
|
+
}[];
|
|
1660
|
+
permissionDefault: UnattendedPermissionMode;
|
|
1661
|
+
qualityDefault: QualityMode;
|
|
1662
|
+
/** Projects this person may place work in, most recently opened first. */
|
|
1663
|
+
projects?: {
|
|
1664
|
+
path: string;
|
|
1665
|
+
name: string;
|
|
1666
|
+
}[];
|
|
1667
|
+
}): string;
|
|
1668
|
+
export type WorkRow = WorkFacts & {
|
|
1669
|
+
assignment?: AssignmentSnapshot | null;
|
|
1670
|
+
assignmentProblem?: boolean;
|
|
1671
|
+
executionId?: string;
|
|
1672
|
+
familyNotice?: string | null;
|
|
1673
|
+
status: WorkStatus;
|
|
1674
|
+
resultRunId: number | null;
|
|
1675
|
+
};
|
|
1676
|
+
export declare function publicationFactsOf(publication: Publication | null): PublicationFacts;
|
|
1677
|
+
/** Where a status's next act lives, for a row that links out. */
|
|
1678
|
+
export declare function statusActionHref(status: DisplayStatus, taskId: string, runId: number | null, prUrl: string | null, repo?: string | null): string | null;
|
|
1679
|
+
/** The one status line every surface renders: a dot in the tone, the
|
|
1680
|
+
* label, and the `data-work-status` token tests and CSS key off. */
|
|
1681
|
+
/** A not-yet-finished task's shared headline from its dispatch diagnosis (task-status.ts). */
|
|
1682
|
+
export declare function dispatchHeadline(d: DispatchDiagnosis): string;
|
|
1683
|
+
export declare function statusLineHtml(status: DisplayStatus, extra?: string): string;
|
|
1684
|
+
export declare function workDiagnosticsHtml(diagnostics: WorkStatus["diagnostics"]): string;
|
|
1685
|
+
export declare const TASK_GROUP_LABEL: Readonly<Record<WorkIndexGroup, string>>;
|
|
1686
|
+
export declare function workPage(chrome: Chrome, data: {
|
|
1687
|
+
view: WorkView;
|
|
1688
|
+
projectFilter?: string;
|
|
1689
|
+
work: WorkIndexPage;
|
|
1690
|
+
previous: boolean;
|
|
1691
|
+
multiProject: boolean;
|
|
1692
|
+
now: Date;
|
|
1693
|
+
limits?: BrowserLimits | null;
|
|
1694
|
+
}): Screen;
|
|
1695
|
+
export declare function tasksPage(chrome: Chrome, tasks: Task[], state: TaskState | null, csrf: string, problem: string | null, repo?: string | null, prefill?: TaskComposerPrefill | null, permissionDefault?: UnattendedPermissionMode, qualityDefault?: QualityMode, replaced?: ReadonlyMap<string, string>): Screen;
|
|
1696
|
+
export declare function browsePage(chrome: Chrome, data: {
|
|
1697
|
+
at: string;
|
|
1698
|
+
root: string;
|
|
1699
|
+
roots: string[];
|
|
1700
|
+
parent: string | null;
|
|
1701
|
+
entries: {
|
|
1702
|
+
name: string;
|
|
1703
|
+
path: string;
|
|
1704
|
+
git: boolean;
|
|
1705
|
+
}[];
|
|
1706
|
+
csrf: string;
|
|
1707
|
+
}): Screen;
|
|
1708
|
+
/** The compact completed row shared by both halves of the control room. */
|
|
1709
|
+
export type WorkbenchDone = {
|
|
1710
|
+
taskId: string;
|
|
1711
|
+
title: string;
|
|
1712
|
+
outcome: string | null;
|
|
1713
|
+
repo: string | null;
|
|
1714
|
+
};
|
|
1715
|
+
/**
|
|
1716
|
+
* The control-room rail (attended A1): every state that matters while a
|
|
1717
|
+
* person supervises, ordered by intervention cost. The project chip is
|
|
1718
|
+
* deliberately repeated on every row — a title is not a workspace, and
|
|
1719
|
+
* switching context must never depend on remembering which repo is open.
|
|
1720
|
+
*/
|
|
1721
|
+
export declare function workbenchRail(data: {
|
|
1722
|
+
attention: BoardCard[];
|
|
1723
|
+
building: BoardCard[];
|
|
1724
|
+
waiting: BoardCard[];
|
|
1725
|
+
queued: BoardCard[];
|
|
1726
|
+
done: WorkbenchDone[];
|
|
1727
|
+
selected: string | null;
|
|
1728
|
+
saturated: boolean;
|
|
1729
|
+
}): string;
|
|
1730
|
+
/** The project chip every all-scope row wears: null is UNPLACED, said so. */
|
|
1731
|
+
export declare function projectChip(repo: string | null | undefined): string;
|
|
1732
|
+
/**
|
|
1733
|
+
* The decision card everywhere a person may ANSWER (portfolio, the
|
|
1734
|
+
* selected-project inbox; the task page joins in slice 3): a reversible
|
|
1735
|
+
* option answers with one tap on the card, labeled with its own words —
|
|
1736
|
+
* never a letter; an irreversible option is a LINK to the decision page,
|
|
1737
|
+
* where the server-side confirm=yes guard lives. The roll-up inbox keeps
|
|
1738
|
+
* its links-only cards and never renders this partial. The recommended
|
|
1739
|
+
* option wears a neutral badge — recommendation is not urgency, and magenta
|
|
1740
|
+
* stays on the card's outline.
|
|
1741
|
+
*/
|
|
1742
|
+
export declare function decisionAnswerCard(decision: Decision & {
|
|
1743
|
+
taskId: string;
|
|
1744
|
+
repo?: string | null;
|
|
1745
|
+
}, csrf: string, now: Date, chip: boolean, returnTo?: string | null): string;
|
|
1746
|
+
/**
|
|
1747
|
+
* The inline-answer enhancement (portfolio arc §2): submits a reversible
|
|
1748
|
+
* option's form as the urlencoded POST the forms-only gate expects, follows
|
|
1749
|
+
* the redirect, and — because an answered decision leaves the open list —
|
|
1750
|
+
* replaces ONLY that card with the answered receipt parsed from the
|
|
1751
|
+
* decision page's own rendering, or removes the card. Anything unexpected
|
|
1752
|
+
* (auth, a page that is not the decision's) navigates instead of inserting.
|
|
1753
|
+
* Nothing else on the page is touched: typed input elsewhere survives.
|
|
1754
|
+
*/
|
|
1755
|
+
export declare function decisionAnswerScript(): string;
|
|
1756
|
+
/**
|
|
1757
|
+
* The portfolio overview (arc slice 1a): what waits on you, what the last
|
|
1758
|
+
* 24 hours of run starts amounted to, what is running, and the terminal-run
|
|
1759
|
+
* ledger — all of it across every admitted project, a project chip on every
|
|
1760
|
+
* row. The caller has already applied admission and per-row visibility.
|
|
1761
|
+
*/
|
|
1762
|
+
export declare function portfolioOverview(data: {
|
|
1763
|
+
attention: BoardCard[];
|
|
1764
|
+
building: BoardCard[];
|
|
1765
|
+
waiting: BoardCard[];
|
|
1766
|
+
queued: BoardCard[];
|
|
1767
|
+
done: WorkbenchDone[];
|
|
1768
|
+
saturated: boolean;
|
|
1769
|
+
decisions: (Decision & {
|
|
1770
|
+
taskId: string;
|
|
1771
|
+
repo?: string | null;
|
|
1772
|
+
})[];
|
|
1773
|
+
approvals: {
|
|
1774
|
+
taskId: string;
|
|
1775
|
+
title: string;
|
|
1776
|
+
goal: string;
|
|
1777
|
+
proposedAt: string;
|
|
1778
|
+
repo?: string | null; /** v48 authority repair: the closed consent door's title, or null when a yes could bind. */
|
|
1779
|
+
closed?: string | null;
|
|
1780
|
+
}[];
|
|
1781
|
+
requeueables: {
|
|
1782
|
+
taskId: string;
|
|
1783
|
+
title: string;
|
|
1784
|
+
state: TaskState;
|
|
1785
|
+
strikes: number;
|
|
1786
|
+
incidentCount: number;
|
|
1787
|
+
repo?: string | null;
|
|
1788
|
+
}[];
|
|
1789
|
+
cancelledBlockers: {
|
|
1790
|
+
blockerId: string;
|
|
1791
|
+
dependentCount: number;
|
|
1792
|
+
exampleDependent: string;
|
|
1793
|
+
repo?: string | null;
|
|
1794
|
+
blockerRepo?: string | null;
|
|
1795
|
+
}[];
|
|
1796
|
+
gaps: Gap[];
|
|
1797
|
+
gapsProject: string | null;
|
|
1798
|
+
runs24: (Run & {
|
|
1799
|
+
taskId: string;
|
|
1800
|
+
})[];
|
|
1801
|
+
live: {
|
|
1802
|
+
taskId: string;
|
|
1803
|
+
runner: string;
|
|
1804
|
+
claimedAt: string;
|
|
1805
|
+
expiresAt: string;
|
|
1806
|
+
model: string | null;
|
|
1807
|
+
repo: string | null;
|
|
1808
|
+
}[];
|
|
1809
|
+
ledger: {
|
|
1810
|
+
runId: number;
|
|
1811
|
+
taskId: string;
|
|
1812
|
+
title: string;
|
|
1813
|
+
repo: string | null;
|
|
1814
|
+
outcome: string;
|
|
1815
|
+
role: string;
|
|
1816
|
+
provider: string | null;
|
|
1817
|
+
model: string | null;
|
|
1818
|
+
startedAt: string;
|
|
1819
|
+
ranMinutes: number | null;
|
|
1820
|
+
costUsd: number | null;
|
|
1821
|
+
authMode: "subscription" | "api-key" | null;
|
|
1822
|
+
prNumber: number | null;
|
|
1823
|
+
prUrl: string | null;
|
|
1824
|
+
}[];
|
|
1825
|
+
csrf: string;
|
|
1826
|
+
now: Date;
|
|
1827
|
+
}): string;
|
|
1828
|
+
/** Dollars for a screen: micro-USD stated as money, unknowables in words. */
|
|
1829
|
+
export declare function contestDollars(microusd: number): string;
|
|
1830
|
+
export declare const CONTEST_STATE_WORDS: Record<string, string>;
|
|
1831
|
+
/**
|
|
1832
|
+
* The comparison screen: every agent's result side by side — outcome, cost,
|
|
1833
|
+
* questions, conclusion, and the verified diff — with a pick button only on
|
|
1834
|
+
* results the evidence supports. Plain words throughout: tournament,
|
|
1835
|
+
* agents, results. The pick itself happens on a separate confirmation
|
|
1836
|
+
* screen whose form this page can only reach through a POST.
|
|
1837
|
+
*/
|
|
1838
|
+
export declare function contestPage(chrome: Chrome, data: {
|
|
1839
|
+
view: {
|
|
1840
|
+
contest: Contest;
|
|
1841
|
+
agents: AgentView[];
|
|
1842
|
+
};
|
|
1843
|
+
taskId: string;
|
|
1844
|
+
taskTitle: string;
|
|
1845
|
+
questions: Map<number, number>;
|
|
1846
|
+
totalMicrousd: number;
|
|
1847
|
+
anyUnknown: boolean;
|
|
1848
|
+
rollups: Map<number, {
|
|
1849
|
+
costMicrousd: number;
|
|
1850
|
+
tokensIn: number;
|
|
1851
|
+
tokensOut: number;
|
|
1852
|
+
measuredRuns: number;
|
|
1853
|
+
totalRuns: number;
|
|
1854
|
+
}>;
|
|
1855
|
+
diffs: Map<number, TerminalDiffView | null>;
|
|
1856
|
+
/** Runs whose lease is still the task's current live claim — "still
|
|
1857
|
+
* working" is said only of these; an interrupted agent's unfinished run
|
|
1858
|
+
* reads as stopped (round-4 finding 16). */
|
|
1859
|
+
liveRuns?: ReadonlySet<number>;
|
|
1860
|
+
csrf: string;
|
|
1861
|
+
problem: string | null;
|
|
1862
|
+
}): Screen;
|
|
1863
|
+
/**
|
|
1864
|
+
* The confirmation screen a POST minted: it restates, in full, exactly what
|
|
1865
|
+
* the password will authorize — the identified result, the money, and the
|
|
1866
|
+
* one publication consequence — over a single-use nonce bound to that
|
|
1867
|
+
* restatement. If anything shifts underneath before the yes, the pick
|
|
1868
|
+
* refuses rather than landing on the moved thing.
|
|
1869
|
+
*/
|
|
1870
|
+
export declare function contestCeremonyPage(chrome: Chrome, data: {
|
|
1871
|
+
kind: "pick" | "abandon";
|
|
1872
|
+
contestKind: "race" | "comparison";
|
|
1873
|
+
contestId: number;
|
|
1874
|
+
taskId: string;
|
|
1875
|
+
taskTitle: string;
|
|
1876
|
+
agents: number;
|
|
1877
|
+
totalMicrousd: number;
|
|
1878
|
+
anyUnknown: boolean;
|
|
1879
|
+
chosen?: AgentView;
|
|
1880
|
+
publication?: {
|
|
1881
|
+
githubRepo: string;
|
|
1882
|
+
branch: string;
|
|
1883
|
+
draft: boolean;
|
|
1884
|
+
} | null;
|
|
1885
|
+
nonceValue: string;
|
|
1886
|
+
csrf: string;
|
|
1887
|
+
}): Screen;
|
|
1888
|
+
export type OnboardCardState = {
|
|
1889
|
+
enabled: false;
|
|
1890
|
+
why: string;
|
|
1891
|
+
} | {
|
|
1892
|
+
enabled: true;
|
|
1893
|
+
roots: readonly string[];
|
|
1894
|
+
record: [string, {
|
|
1895
|
+
nameWithOwner: string;
|
|
1896
|
+
rootIndex: number;
|
|
1897
|
+
target: string;
|
|
1898
|
+
diskUsageKib: number | null;
|
|
1899
|
+
large: boolean;
|
|
1900
|
+
mintedAt: number;
|
|
1901
|
+
}] | null;
|
|
1902
|
+
};
|
|
1903
|
+
export type ProjectPeek = {
|
|
1904
|
+
waiting: number;
|
|
1905
|
+
queued: number;
|
|
1906
|
+
running: number;
|
|
1907
|
+
doneRecently: number;
|
|
1908
|
+
};
|
|
1909
|
+
/**
|
|
1910
|
+
* owner/name from a git remote URL — FULLY ANCHORED https/ssh github.com
|
|
1911
|
+
* forms only (ghlist review, finding 2): a foreign host carrying
|
|
1912
|
+
* "github.com" in its path, or a non-github host, must never read as a
|
|
1913
|
+
* GitHub identity. Everything else is null.
|
|
1914
|
+
*/
|
|
1915
|
+
export declare function githubIdentityOf(remoteUrl: string): string | null;
|
|
1916
|
+
/**
|
|
1917
|
+
* The `origin` remote's url from a repository's OWN .git/config. BOUNDED
|
|
1918
|
+
* I/O by construction (ghlist review, finding 1): the final component may
|
|
1919
|
+
* not be a symlink (O_NOFOLLOW + lstat), must be a REGULAR file under a
|
|
1920
|
+
* size cap, and at most 64 KiB are ever read — a sparse monster or a fifo
|
|
1921
|
+
* planted as a "config" reads as null, never as a hang. Parsed LINE BY
|
|
1922
|
+
* LINE with real section tracking (finding 2): a `[remote "origin"]`
|
|
1923
|
+
* embedded inside some other value never opens the section. null when
|
|
1924
|
+
* unreadable or origin-less; a worktree-style `.git` FILE (gitdir pointer)
|
|
1925
|
+
* reads null too, which is honest — its identity lives elsewhere. The
|
|
1926
|
+
* answer is ADVISORY metadata for offering a button: the /projects/open
|
|
1927
|
+
* road re-proves path, ceiling, and git-ness before anything mutates, and
|
|
1928
|
+
* a config that lies about its origin can only mislabel a repository the
|
|
1929
|
+
* operator was already allowed to open.
|
|
1930
|
+
*/
|
|
1931
|
+
export declare function originUrlOf(repoPath: string): string | null;
|
|
1932
|
+
/** The GitHub listing page: the account's repositories with ONE honest
|
|
1933
|
+
* action each. Names, descriptions, and paths are gh/filesystem DATA —
|
|
1934
|
+
* escaped at the sink like everything else. */
|
|
1935
|
+
export declare function githubReposPage(chrome: Chrome, data: {
|
|
1936
|
+
listed: ListOutcome;
|
|
1937
|
+
local: Map<string, string>;
|
|
1938
|
+
registered: Set<string>;
|
|
1939
|
+
csrf: string;
|
|
1940
|
+
cloneReady: boolean;
|
|
1941
|
+
openProject: string | null;
|
|
1942
|
+
}): Screen;
|
|
1943
|
+
export declare function projectsPage(chrome: Chrome, recent: {
|
|
1944
|
+
path: string;
|
|
1945
|
+
name: string;
|
|
1946
|
+
lastOpenedAt: string;
|
|
1947
|
+
}[], candidates: string[], open: string | null, csrf: string, problem: string | null, _unscopedMode: boolean, browsable?: boolean, onboard?: OnboardCardState | null, peeks?: Record<string, ProjectPeek | null>, returnTo?: string, pullRequestsOn?: (path: string) => boolean, checkLevelOf?: (path: string) => CheckLevel): Screen;
|
|
1948
|
+
/**
|
|
1949
|
+
* Creating work is the product's first verb, so it gets a whole calm page:
|
|
1950
|
+
* a title, the goal that becomes the scope draft, and the project it lands
|
|
1951
|
+
* in — then straight to the approve card, which is the aha the flow serves.
|
|
1952
|
+
*/
|
|
1953
|
+
/** One queue card. Taken work renders pinned — visible, never draggable. */
|
|
1954
|
+
/**
|
|
1955
|
+
* The request's own session facts, readable from anywhere below the
|
|
1956
|
+
* dispatcher without threading them through forty call sites: the csrf
|
|
1957
|
+
* token the chrome's switcher forms carry, and the path a switch returns
|
|
1958
|
+
* to. AsyncLocalStorage follows the request's own async chain, so two
|
|
1959
|
+
* interleaved requests never read each other's token.
|
|
1960
|
+
*/
|
|
1961
|
+
/** The person's pinned theme from their own cookie; null follows the device. */
|
|
1962
|
+
export declare function pinnedTheme(cookieHeader: string | undefined): "light" | "dark" | null;
|
|
1963
|
+
export declare function themeAttribute(): string;
|
|
1964
|
+
/** A chosen accent (Settings → Appearance) re-pigments the signal tokens only, after the shared stylesheet. */
|
|
1965
|
+
export declare function accentHead(): string;
|
|
1966
|
+
export declare const requestContext: AsyncLocalStorage<{
|
|
1967
|
+
appOrigins?: readonly (string | null)[];
|
|
1968
|
+
sso?: {
|
|
1969
|
+
label: string;
|
|
1970
|
+
fresh: boolean;
|
|
1971
|
+
} | undefined;
|
|
1972
|
+
refusal?: (response: ServerResponse, status: number, body: string) => void;
|
|
1973
|
+
theme?: "light" | "dark" | null;
|
|
1974
|
+
accent?: string | null;
|
|
1975
|
+
updateSeen?: string | null;
|
|
1976
|
+
csrf: string;
|
|
1977
|
+
returnTo: string;
|
|
1978
|
+
actor?: string;
|
|
1979
|
+
createdTask?: string;
|
|
1980
|
+
browser?: boolean;
|
|
1981
|
+
workspaceRead?: boolean;
|
|
1982
|
+
workspaceRequest?: string | null;
|
|
1983
|
+
workCounts?: ReturnType<typeof workCountsByProject>;
|
|
1984
|
+
workCrew?: {
|
|
1985
|
+
project: string | null;
|
|
1986
|
+
page: WorkIndexPage;
|
|
1987
|
+
};
|
|
1988
|
+
lens?: string | null;
|
|
1989
|
+
workspaceValidator?: {
|
|
1990
|
+
key: string;
|
|
1991
|
+
revision: string;
|
|
1992
|
+
expiresAt: number;
|
|
1993
|
+
etag: string;
|
|
1994
|
+
};
|
|
1995
|
+
}>;
|
|
1996
|
+
/** A same-site path or "/": never a scheme, a host, or a protocol-relative road. */
|
|
1997
|
+
/** The words a result page shows for a refusal its own form led to, by the fixed code a redirect carries. */
|
|
1998
|
+
export declare const ACCEPT_ANYWAY_NEEDS_REASON = "Accepting anyway needs a reason.";
|
|
1999
|
+
export declare const RESULT_REFUSALS: Record<string, string>;
|
|
2000
|
+
/** A same-site page address with a refusal code added, before any fragment. */
|
|
2001
|
+
export declare function withRefusal(href: string, code: keyof typeof RESULT_REFUSALS): string;
|
|
2002
|
+
export declare function safeReturn(raw: string | null | undefined): string;
|
|
2003
|
+
/**
|
|
2004
|
+
* The destination a sign-in returns to (a phone's deep link to an exact
|
|
2005
|
+
* task or result): `safeReturn`, then narrower still — never the sign-in,
|
|
2006
|
+
* sign-up, join or sign-out roads themselves (a loop, or a token in a
|
|
2007
|
+
* path), never an encoded second scheme or host once the browser decodes
|
|
2008
|
+
* it, never a query key that could carry a secret, and never a region
|
|
2009
|
+
* fetch (`?fragment=`), which is a piece of a page and not a place to
|
|
2010
|
+
* land. Only the app's own pages qualify: the task lens, the chat, the
|
|
2011
|
+
* work and board views, and the fixed control destinations a phone
|
|
2012
|
+
* button can name. Nothing here is a second redirect framework: one
|
|
2013
|
+
* same-site path, or "/".
|
|
2014
|
+
*/
|
|
2015
|
+
export declare const LOGIN_RETURN_PAGES: RegExp;
|
|
2016
|
+
export declare function loginReturn(raw: string | null | undefined): string;
|
|
2017
|
+
/** A request path fit for a log: the one-time sign-in link's code is left out. */
|
|
2018
|
+
export declare function redactedPath(path: string): string;
|
|
2019
|
+
/** The sign-in page that comes back to `path` afterwards, or plain /login when the path is not one to come back to. */
|
|
2020
|
+
export declare function loginHref(path: string): string;
|
|
2021
|
+
/** Chat actions may return only to the unified chat or one task-focused
|
|
2022
|
+
* lens. Other same-site paths are valid elsewhere, but not for chat forms. */
|
|
2023
|
+
export declare function safeChatReturn(raw: string | null | undefined): string;
|
|
2024
|
+
export declare function chatReturnWithSaid(back: string, said: string): string;
|
|
2025
|
+
export declare const chatReturnWithLatest: (back: string) => string;
|
|
2026
|
+
/** The no-script "move to the front" sentinel: the form cannot name the
|
|
2027
|
+
* front of a partition, so the handler resolves it (slice 1b, fix 1). */
|
|
2028
|
+
export declare const QUEUE_FRONT = "__TOP__";
|
|
2029
|
+
/** Drawn, one stroke weight, like the tab bar's icons — never a glyph. */
|
|
2030
|
+
export declare const GRIP_ICON: string;
|
|
2031
|
+
export declare const TO_FRONT_ICON: string;
|
|
2032
|
+
/** One stroke weight, from the tab bar's set: the sidebar's primary rows
|
|
2033
|
+
* wear an icon each; the foot's list stays text, the way Linear's does. */
|
|
2034
|
+
export declare const strokeIcon: (paths: string) => string;
|
|
2035
|
+
/** Where the queue lives now: the board, flipped to dispatch order. */
|
|
2036
|
+
export declare const QUEUE_VIEW = "/board?view=order";
|
|
2037
|
+
export declare const FOLDER_PATHS = "<path d=\"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z\"/>";
|
|
2038
|
+
export declare const CHAT_PATHS = "<path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"/>";
|
|
2039
|
+
export declare const WORK_PATHS = "<path d=\"M9 6h11\"/><path d=\"M9 12h11\"/><path d=\"M9 18h11\"/><path d=\"m4 6 1 1 2-2\"/><path d=\"m4 12 1 1 2-2\"/><path d=\"m4 18 1 1 2-2\"/>";
|
|
2040
|
+
export declare const NAV_ICONS: Partial<Record<Chrome["active"], string>>;
|
|
2041
|
+
/** One grouped destination inside an accordion group or the /menu overflow. */
|
|
2042
|
+
export type NavRow = {
|
|
2043
|
+
key: Chrome["active"];
|
|
2044
|
+
href: string;
|
|
2045
|
+
label: string;
|
|
2046
|
+
hint: string;
|
|
2047
|
+
};
|
|
2048
|
+
/**
|
|
2049
|
+
* The workspace shell (package 1): three primary rows (chat, work,
|
|
2050
|
+
* projects) above two accordion groups. Work tools is where work is
|
|
2051
|
+
* arranged, planned, and scheduled — the board, its order view, the
|
|
2052
|
+
* flat task list, recipes, routines, the portfolio, and the action
|
|
2053
|
+
* ledger. Settings is who and what the fleet runs on — fleet,
|
|
2054
|
+
* requirements, people, operating mode, system — plus the agent-defaults
|
|
2055
|
+
* page where the console offers it. Both draw from the same two lists on
|
|
2056
|
+
* a desk (accordion groups) and on a phone (/menu sections), so the two
|
|
2057
|
+
* never disagree, and a project-scoped login sees exactly the rows it
|
|
2058
|
+
* saw before: moving a link never widens role or project visibility.
|
|
2059
|
+
*/
|
|
2060
|
+
export declare function workToolRows(scoped?: boolean, _offersChat?: boolean, offersCode?: boolean): NavRow[];
|
|
2061
|
+
export declare function settingsRows(scoped?: boolean, offersSettings?: boolean): NavRow[];
|
|
2062
|
+
/** Which accordion group opens by default for a given active page. */
|
|
2063
|
+
export declare const TOOL_KEYS: Set<"activity" | "board" | "caps" | "chat" | "code" | "done" | "fleet" | "flows" | "inbox" | "ledger" | "menu" | "mode" | "none" | "people" | "projects" | "queue" | "recipes" | "review" | "routines" | "runs" | "settings" | "spend" | "system" | "tasks" | "work" | "workbench">;
|
|
2064
|
+
export declare const SETTINGS_KEYS: Set<"activity" | "board" | "caps" | "chat" | "code" | "done" | "fleet" | "flows" | "inbox" | "ledger" | "menu" | "mode" | "none" | "people" | "projects" | "queue" | "recipes" | "review" | "routines" | "runs" | "settings" | "spend" | "system" | "tasks" | "work" | "workbench">;
|
|
2065
|
+
/** The builds screen's views (reduction pass §1): done, the review queue,
|
|
2066
|
+
* and activity are ways of looking at builds, not destinations. */
|
|
2067
|
+
export declare function buildsViews(current: "builds" | "done" | "review" | "activity"): string;
|
|
2068
|
+
export declare const CHEVRON_ICON: string;
|
|
2069
|
+
/** The drag grip: a 2rem touch-sized handle that owns its touches
|
|
2070
|
+
* (touch-action: none), so a finger on it drags instead of scrolling. */
|
|
2071
|
+
export declare const GRIP_HANDLE = "<span class=\"queue-handle\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"9\" cy=\"6\" r=\"1.5\"/><circle cx=\"15\" cy=\"6\" r=\"1.5\"/><circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/><circle cx=\"9\" cy=\"18\" r=\"1.5\"/><circle cx=\"15\" cy=\"18\" r=\"1.5\"/></svg></span>";
|
|
2072
|
+
export type QueueCardTask = ReturnType<Store["queueScoped"]>[number] & {
|
|
2073
|
+
dispatch?: DispatchDiagnosis | null;
|
|
2074
|
+
};
|
|
2075
|
+
export declare function queueCard(one: QueueCardTask, csrf: string, revision: number, queueRevision: number, workers: {
|
|
2076
|
+
name: string;
|
|
2077
|
+
retired: boolean;
|
|
2078
|
+
}[], column: string): string;
|
|
2079
|
+
/** The queue columns fragment — shared queue first, then each worker. */
|
|
2080
|
+
export declare function queueBody(tasks: QueueCardTask[], workers: {
|
|
2081
|
+
name: string;
|
|
2082
|
+
retired: boolean;
|
|
2083
|
+
note: string | null;
|
|
2084
|
+
capacity: number;
|
|
2085
|
+
building: number;
|
|
2086
|
+
}[], csrf: string, revision: number, queueRevision: number): string;
|
|
2087
|
+
/**
|
|
2088
|
+
* The fleet screen — one lane per runner, and the work in front of it.
|
|
2089
|
+
* Building claims pin to the top of their worker's lane (live — never
|
|
2090
|
+
* draggable); queued reservations sit below it (draggable to another
|
|
2091
|
+
* worker, re-reserving them). Every card wears its project chip, which is
|
|
2092
|
+
* the whole point: which agent is on which project is the page's answer.
|
|
2093
|
+
* Form-free like the board, except the per-worker note, so the lane stack
|
|
2094
|
+
* can re-render itself while somebody watches.
|
|
2095
|
+
*/
|
|
2096
|
+
export declare function fleetBody(queued: ReturnType<Store["fleetQueue"]>, building: ReturnType<Store["liveClaims"]>, runners: Runner[], csrf: string, queueRevision: number, visibleRepo: (repo: string | null) => boolean): string;
|
|
2097
|
+
/** Identical drag mechanics to the queue — the pointer events land on the worker's column. */
|
|
2098
|
+
export declare function fleetScript(): string;
|
|
2099
|
+
/** The /queue page's one nonce'd script: delegated pointer-event drag (it
|
|
2100
|
+
* survives every fragment swap — finding 17) plus a poller that re-checks
|
|
2101
|
+
* focus, dirty inputs, and an in-flight drag AT SWAP TIME, never only
|
|
2102
|
+
* before the fetch. Select dirtiness compares against data-initial
|
|
2103
|
+
* (finding 18); missing that, a select counts clean.
|
|
2104
|
+
*/
|
|
2105
|
+
export declare function queueScript(): string;
|
|
2106
|
+
export declare function newTaskPage(chrome: Chrome, project: string | null, csrf: string, projectRevision: number, problem: string | null, candidates?: {
|
|
2107
|
+
id: string;
|
|
2108
|
+
title: string;
|
|
2109
|
+
}[], permissionDefault?: UnattendedPermissionMode, qualityDefault?: QualityMode, projects?: {
|
|
2110
|
+
path: string;
|
|
2111
|
+
name: string;
|
|
2112
|
+
}[]): Screen;
|
|
2113
|
+
/** A plan is stored as inert Markdown for portability into the builder
|
|
2114
|
+
* brief, but the console renders its known structure as a useful control
|
|
2115
|
+
* surface. Historical free-form plans keep their safe plain-text fallback. */
|
|
2116
|
+
export declare const MILESTONE_STATE_WORDS: Record<MilestoneState, string>;
|
|
2117
|
+
/** Badges, labels and buttons read in sentence case, whatever word a record stores. */
|
|
2118
|
+
export declare const sentenceCase: (word: string) => string;
|
|
2119
|
+
export declare function milestoneStateWord(state: MilestoneState): string;
|
|
2120
|
+
/** Adaptive execution plans (v44): the live milestone projection a running
|
|
2121
|
+
* (or finished) build has reported. A shared render used by BOTH the task
|
|
2122
|
+
* page and the focused chat's live region (c2) — labeled clearly as an
|
|
2123
|
+
* agent's own report, since a milestone claim is never completion proof
|
|
2124
|
+
* (Priority 2's proof contract is the only thing that adjudicates "done"). */
|
|
2125
|
+
export declare function milestoneProgressHtml(milestones: MilestoneProgressView[] | null | undefined): string;
|
|
2126
|
+
/** Adaptive execution plans (v44): the current plan revision's own reason
|
|
2127
|
+
* and evidence, any revision still awaiting an operator's accept/reject,
|
|
2128
|
+
* and the immutable history. A shared render used by BOTH the task page
|
|
2129
|
+
* and the focused chat's live region (c2), reading a document already
|
|
2130
|
+
* verified before a byte reached this function (c5). */
|
|
2131
|
+
export declare function planRevisionLedgerHtml(ledger: PlanRevisionLedgerView | null | undefined, taskId: string, csrf: string): string;
|
|
2132
|
+
export declare function executionPlanHtml(document: string, compact?: boolean): string;
|
|
2133
|
+
/** One plan revision, its document verified before a byte renders — the
|
|
2134
|
+
* same verify-then-read discipline as `planViewOf`, just carrying the
|
|
2135
|
+
* ledger row's own facts alongside the text. `id: null` marks the
|
|
2136
|
+
* read-only revision-1 projection for a task with no `plan_revision` rows
|
|
2137
|
+
* yet (c1). */
|
|
2138
|
+
export type RevisionDocView = {
|
|
2139
|
+
id: number | null;
|
|
2140
|
+
revision: number;
|
|
2141
|
+
status: PlanRevisionStatus;
|
|
2142
|
+
reason: string;
|
|
2143
|
+
evidenceLink: string | null;
|
|
2144
|
+
author: string;
|
|
2145
|
+
kind: PlanRevisionKind;
|
|
2146
|
+
authorityKind: "plan-only" | "authority-change";
|
|
2147
|
+
changedFields: string[];
|
|
2148
|
+
document: string;
|
|
2149
|
+
sha256: string;
|
|
2150
|
+
createdAt: string;
|
|
2151
|
+
resolvedAt: string | null;
|
|
2152
|
+
resolvedBy: string | null;
|
|
2153
|
+
};
|
|
2154
|
+
export type PlanRevisionLedgerView = {
|
|
2155
|
+
current: RevisionDocView | null;
|
|
2156
|
+
pending: RevisionDocView | null;
|
|
2157
|
+
history: RevisionDocView[];
|
|
2158
|
+
};
|
|
2159
|
+
export type MilestoneProgressView = {
|
|
2160
|
+
id: string;
|
|
2161
|
+
description: string;
|
|
2162
|
+
state: MilestoneState;
|
|
2163
|
+
note: string | null;
|
|
2164
|
+
};
|
|
2165
|
+
/** The revision batch a task's approval screen restates, or the named reason it cannot. */
|
|
2166
|
+
export type RevisionView = {
|
|
2167
|
+
sourceTask: string;
|
|
2168
|
+
sourceRun: number;
|
|
2169
|
+
/** What kind of brief this is — annotations, a CI repair, a criterion repair. */
|
|
2170
|
+
kind: "annotations" | "ci-repair" | "criterion-repair";
|
|
2171
|
+
comments: {
|
|
2172
|
+
path: string | null;
|
|
2173
|
+
line: number | null;
|
|
2174
|
+
note: string;
|
|
2175
|
+
author: string;
|
|
2176
|
+
}[];
|
|
2177
|
+
/** The lineage and ACTUAL terms (contract handoff task 2), read from
|
|
2178
|
+
* the child's own rows — the same projection chat and the approval
|
|
2179
|
+
* card restate. */
|
|
2180
|
+
lineage: RevisionLineage | null;
|
|
2181
|
+
} | {
|
|
2182
|
+
problem: string;
|
|
2183
|
+
};
|
|
2184
|
+
/**
|
|
2185
|
+
* A revision's lineage and ACTUAL terms, in the same words on the task
|
|
2186
|
+
* page, the approval card, and chat (contract handoff task 2): which
|
|
2187
|
+
* source and build it revises, the ancestry to its root, the terms the
|
|
2188
|
+
* child really carries (read from its own scope — never restated from the
|
|
2189
|
+
* brief), what is re-resolved for THIS approval, what never inherits, and
|
|
2190
|
+
* — when the task continues a repair chain — the attempts used against
|
|
2191
|
+
* the signed cap.
|
|
2192
|
+
*/
|
|
2193
|
+
export declare function revisionLineageWords(lineage: RevisionLineage): string[];
|
|
2194
|
+
export declare function revisionLineageHtml(lineage: RevisionLineage | null): string;
|
|
2195
|
+
export declare function taskBodyParts(data: {
|
|
2196
|
+
/** Until the first Ready result: show the task's way there (onboarding). */
|
|
2197
|
+
guide?: boolean;
|
|
2198
|
+
/** What the last attempt missed and what to change, shown when the task failed. */
|
|
2199
|
+
failure?: FailureExplanation | null;
|
|
2200
|
+
/** Run checks in place, for a status row that offers it. */
|
|
2201
|
+
runChecks?: {
|
|
2202
|
+
action: string;
|
|
2203
|
+
level: "quick" | "full";
|
|
2204
|
+
returnTo: string;
|
|
2205
|
+
} | null;
|
|
2206
|
+
/** The demo database: no check runs here, and the Checks row says so. */
|
|
2207
|
+
demo?: boolean;
|
|
2208
|
+
assignment?: AssignmentSnapshot | null;
|
|
2209
|
+
checkProgress?: CheckProgress | null;
|
|
2210
|
+
rootId?: string;
|
|
2211
|
+
rootTitle?: string;
|
|
2212
|
+
history?: string;
|
|
2213
|
+
versionLabel?: string | null;
|
|
2214
|
+
status?: DisplayStatus;
|
|
2215
|
+
task: Task;
|
|
2216
|
+
/** Shared read-side lifecycle answer; the atomic claim still re-proves it. */
|
|
2217
|
+
dispatch?: DispatchDiagnosis | null;
|
|
2218
|
+
/** v52: the exact-run Stop / Stopping / Resume control. */
|
|
2219
|
+
control?: TaskControlView;
|
|
2220
|
+
strikes: number;
|
|
2221
|
+
/** v102: who filed it, and the project's approval rules as they bear on this task (null: none apply). */
|
|
2222
|
+
filer?: {
|
|
2223
|
+
name: string | null;
|
|
2224
|
+
kind: string;
|
|
2225
|
+
} | null;
|
|
2226
|
+
budgetHold?: string | null;
|
|
2227
|
+
policyHold?: string | null;
|
|
2228
|
+
approvalRules?: {
|
|
2229
|
+
words: string;
|
|
2230
|
+
votes: string[];
|
|
2231
|
+
needsTwo: boolean;
|
|
2232
|
+
} | null;
|
|
2233
|
+
plan: "requested" | "drafted" | null;
|
|
2234
|
+
planDocument: string | null;
|
|
2235
|
+
planAuto?: boolean;
|
|
2236
|
+
/** Hash of the verified plan artifact currently shown. */
|
|
2237
|
+
planSha?: string | null;
|
|
2238
|
+
/** The drafted plan's contract record (contract handoff, task 1). */
|
|
2239
|
+
planContract?: PlanContractView | null;
|
|
2240
|
+
/** Adaptive execution plans (v44): the revision ledger and live milestone
|
|
2241
|
+
* projection — null for a task with no plan at all. */
|
|
2242
|
+
planRevisions?: PlanRevisionLedgerView | null;
|
|
2243
|
+
milestoneProgress?: MilestoneProgressView[] | null;
|
|
2244
|
+
/** v34: what this task delivers, and the scout's report when one exists. */
|
|
2245
|
+
deliverable?: "branch" | "report";
|
|
2246
|
+
report?: ReportView | null;
|
|
2247
|
+
revision?: RevisionView | null;
|
|
2248
|
+
/** v40: this task's own place in a bounded repair chain — computed
|
|
2249
|
+
* independent of `completion` (a freshly drafted, unapproved repair has
|
|
2250
|
+
* no run yet, so it must not wait for one to say so). `completion`'s own
|
|
2251
|
+
* branches render it too, once a run exists; this is the fallback for
|
|
2252
|
+
* the moment before that, so "awaiting approval" is never invisible. */
|
|
2253
|
+
repairChain?: RepairChainRow | null;
|
|
2254
|
+
publication?: Publication | null;
|
|
2255
|
+
repo: string | null;
|
|
2256
|
+
/** Immutable filing provenance (v12) — the approver sees which door
|
|
2257
|
+
* filed this (console, cli, intake, template:<name>) at the yes. */
|
|
2258
|
+
filedVia?: string | null;
|
|
2259
|
+
/** Coordinator provenance when an agent filed this; null otherwise. */
|
|
2260
|
+
coordinator?: {
|
|
2261
|
+
label: string;
|
|
2262
|
+
filedAgo: string | null;
|
|
2263
|
+
} | null;
|
|
2264
|
+
holds: Hold[];
|
|
2265
|
+
contest?: {
|
|
2266
|
+
id: number;
|
|
2267
|
+
state: string;
|
|
2268
|
+
agents: number;
|
|
2269
|
+
kind: "race" | "comparison";
|
|
2270
|
+
} | null;
|
|
2271
|
+
claimed: boolean;
|
|
2272
|
+
/** What this task waits for — blockers outside this console's ceiling
|
|
2273
|
+
* are named but carry no state and no link. */
|
|
2274
|
+
waitsFor?: {
|
|
2275
|
+
id: string;
|
|
2276
|
+
title: string | null;
|
|
2277
|
+
state: string | null;
|
|
2278
|
+
admitted: boolean;
|
|
2279
|
+
}[];
|
|
2280
|
+
/** Open tasks a "wait for" select may offer (this console's view only). */
|
|
2281
|
+
waitCandidates?: {
|
|
2282
|
+
id: string;
|
|
2283
|
+
title: string;
|
|
2284
|
+
}[];
|
|
2285
|
+
/** The run whose lease is the CURRENT live claim — computed by the data
|
|
2286
|
+
* layer; the renderer never guesses liveness from a null outcome. */
|
|
2287
|
+
liveRunId?: number | null;
|
|
2288
|
+
/** What the agent did last on that live run, and when (task-activity.ts). */
|
|
2289
|
+
activity?: RunActivity | null;
|
|
2290
|
+
/** Workers that are both alive and authorized for this task's project. */
|
|
2291
|
+
worker?: {
|
|
2292
|
+
answering: number;
|
|
2293
|
+
registered: number;
|
|
2294
|
+
totalRegistered: number;
|
|
2295
|
+
lastHeard: string | null;
|
|
2296
|
+
};
|
|
2297
|
+
/** Unmet capabilities that keep this exact task out of dispatch. */
|
|
2298
|
+
gaps?: Gap[];
|
|
2299
|
+
/** Whether this serve asserted its runner — the live file view exists. */
|
|
2300
|
+
peekable?: boolean;
|
|
2301
|
+
/** Where the task stands in its own column, from the data layer. */
|
|
2302
|
+
position?: {
|
|
2303
|
+
position: number;
|
|
2304
|
+
total: number;
|
|
2305
|
+
column: string | null;
|
|
2306
|
+
} | null;
|
|
2307
|
+
/** The tracker item this task stands for, when it is external work. */
|
|
2308
|
+
mirror?: ExternalMirror | null;
|
|
2309
|
+
scope: Scope | null;
|
|
2310
|
+
/** Filed race terms (v14) — the approval restates them; one yes covers both. */
|
|
2311
|
+
raceTerms?: TournamentTerms | null;
|
|
2312
|
+
/** What the approval nonce/digest bind: scope digest, or the joint fingerprint. */
|
|
2313
|
+
approvalDigest?: string | null;
|
|
2314
|
+
spendDefaults?: {
|
|
2315
|
+
buildPerRunMicrousd: number | null;
|
|
2316
|
+
racePerAgentMicrousd: number | null;
|
|
2317
|
+
raceTotalMicrousd: number | null;
|
|
2318
|
+
raceAgents: number | null;
|
|
2319
|
+
} | null;
|
|
2320
|
+
/** Installation starting value for a task that has no profile yet. */
|
|
2321
|
+
permissionDefault?: UnattendedPermissionMode;
|
|
2322
|
+
/** Durable choice for this task, when one was explicitly made. */
|
|
2323
|
+
permissionMode?: UnattendedPermissionMode | null;
|
|
2324
|
+
/** Installation starting value and this task's explicit evidence depth. */
|
|
2325
|
+
qualityDefault?: QualityMode;
|
|
2326
|
+
qualityMode?: QualityMode | null;
|
|
2327
|
+
/** The phase route (v47) and whether this session may edit it. */
|
|
2328
|
+
route?: RouteView | null;
|
|
2329
|
+
canEditRoute?: boolean;
|
|
2330
|
+
runs: Run[];
|
|
2331
|
+
/** Proof produced by the newest finished attempt. The two booleans are
|
|
2332
|
+
* machine facts about immutable, hash-addressed artifacts — never inferred
|
|
2333
|
+
* from the agent's prose. `proofVerdict` is the closed machine-authored
|
|
2334
|
+
* verdict (Priority 2), computed once at completion and never re-derived
|
|
2335
|
+
* here — null only for a run that predates the proof system. */
|
|
2336
|
+
completion?: {
|
|
2337
|
+
runId: number;
|
|
2338
|
+
outcome: string | null;
|
|
2339
|
+
hasTerminalDiff: boolean;
|
|
2340
|
+
hasHandoff: boolean;
|
|
2341
|
+
proofVerdict: ProofVerdict | null;
|
|
2342
|
+
/** v40: the verdict BEFORE an independent reviewer's judgements were
|
|
2343
|
+
* folded in — null when no review has folded (every run before this
|
|
2344
|
+
* migration, and every run no review has touched). */
|
|
2345
|
+
machineVerdict: ProofVerdict | null;
|
|
2346
|
+
proofReasons: string[];
|
|
2347
|
+
proofMatrix: CriterionMatrixRow[];
|
|
2348
|
+
proofMatrixLinks: EvidenceLinkMap;
|
|
2349
|
+
proofAccepted: boolean;
|
|
2350
|
+
/** v51: the run's signed quality mode — the policy semantic coverage is
|
|
2351
|
+
* read under (default: review optional; strict: review required). */
|
|
2352
|
+
qualityMode?: "default" | "strict";
|
|
2353
|
+
/** v40: this run's own place in a bounded repair chain, if any. */
|
|
2354
|
+
repairChain: RepairChainRow | null;
|
|
2355
|
+
/** v50: every root review attempt of this result and what the
|
|
2356
|
+
* allowance still admits; null when no review was ever asked for. */
|
|
2357
|
+
reviewRetry?: ReviewRetryState | null;
|
|
2358
|
+
/** The same review, as the shared projection reads it (review fixes). */
|
|
2359
|
+
review?: ReviewFacts | null;
|
|
2360
|
+
/** Priority 2's concise, shared result package. */
|
|
2361
|
+
receipt: CompletionReceiptView | null;
|
|
2362
|
+
} | null;
|
|
2363
|
+
/** v50: whether this session may ask for a review retry (an approver's
|
|
2364
|
+
* browser session — the same standing `task review` demands). */
|
|
2365
|
+
canRetryReview?: boolean;
|
|
2366
|
+
decisions: Decision[];
|
|
2367
|
+
incidents: Incident[];
|
|
2368
|
+
/** Pending coordinator proposals on this task (mate arc v3), with the decisions their answer cards name. */
|
|
2369
|
+
coordinatorProposals?: {
|
|
2370
|
+
rows: CoordinatorProposal[];
|
|
2371
|
+
decisions: Map<number, Decision>;
|
|
2372
|
+
now: Date;
|
|
2373
|
+
} | null;
|
|
2374
|
+
/** Operator steering notes (arc 1), delivery state included. */
|
|
2375
|
+
steering?: SteerNote[];
|
|
2376
|
+
/** The publication grant the publisher would act under — from
|
|
2377
|
+
* publicationGrantFor(repo) only; null when none, or no project. */
|
|
2378
|
+
grant?: PublicationGrant | null;
|
|
2379
|
+
/** The pull request opened through Complete (its state, link, CI and merge), or the offer to open one for a
|
|
2380
|
+
* result already marked complete; null when neither applies. */
|
|
2381
|
+
pullRequest?: TaskPullRequest | null;
|
|
2382
|
+
/** The task's whole family (the original and its revisions) and every attempt across it, for the thread. */
|
|
2383
|
+
family?: {
|
|
2384
|
+
root: {
|
|
2385
|
+
id: string;
|
|
2386
|
+
title: string;
|
|
2387
|
+
createdAt: string;
|
|
2388
|
+
goal?: string | null;
|
|
2389
|
+
};
|
|
2390
|
+
versions: {
|
|
2391
|
+
id: string;
|
|
2392
|
+
title: string;
|
|
2393
|
+
state: string;
|
|
2394
|
+
}[];
|
|
2395
|
+
runs: (Run & {
|
|
2396
|
+
taskId: string;
|
|
2397
|
+
})[];
|
|
2398
|
+
} | null;
|
|
2399
|
+
/** Degraded composition (slice 1c). "sensitive": the page carries a
|
|
2400
|
+
* password ceremony, so no live poller runs, the attempt panel is a
|
|
2401
|
+
* static line, and open decisions render link-only — decided by the page
|
|
2402
|
+
* wrapper from the rendered body itself. "pane": the body is embedded in
|
|
2403
|
+
* the workbench's selected-task pane, which carries no run pollers, so
|
|
2404
|
+
* only the attempt panel degrades. */
|
|
2405
|
+
degraded?: "sensitive" | "pane";
|
|
2406
|
+
csrf: string;
|
|
2407
|
+
nonce: string;
|
|
2408
|
+
scopeDraft?: URLSearchParams;
|
|
2409
|
+
/** Arrived to edit the plan (?edit=plan): the approval sheet opens its editor. */
|
|
2410
|
+
editPlan?: boolean;
|
|
2411
|
+
cancelDraft?: string;
|
|
2412
|
+
problem: string | null;
|
|
2413
|
+
/** The attended road (Phase 2E): mint offer, or the open authorization. */
|
|
2414
|
+
attended?: {
|
|
2415
|
+
canMint: boolean;
|
|
2416
|
+
/** The mint picker (P1/C7): claude models to choose from, and the
|
|
2417
|
+
* permission posture default (escalated when the active mode says so). */
|
|
2418
|
+
mint?: {
|
|
2419
|
+
models: string[];
|
|
2420
|
+
pinnedModel: string;
|
|
2421
|
+
posture: "auto" | "acceptEdits" | "bypassPermissions";
|
|
2422
|
+
quick: boolean;
|
|
2423
|
+
};
|
|
2424
|
+
open: {
|
|
2425
|
+
id: string;
|
|
2426
|
+
state: string;
|
|
2427
|
+
expiresAt: string;
|
|
2428
|
+
turnsUsed: number;
|
|
2429
|
+
cap: number;
|
|
2430
|
+
spentMicrousd: number;
|
|
2431
|
+
budgetMicrousd: number;
|
|
2432
|
+
running: boolean;
|
|
2433
|
+
} | null;
|
|
2434
|
+
} | null;
|
|
2435
|
+
now: Date;
|
|
2436
|
+
}): {
|
|
2437
|
+
html: string;
|
|
2438
|
+
view: BrowserTaskView;
|
|
2439
|
+
};
|
|
2440
|
+
export declare function taskBody(data: Parameters<typeof taskBodyParts>[0]): string;
|
|
2441
|
+
export declare function taskPage(chrome: Chrome, data: Parameters<typeof taskBody>[0]): Screen;
|
|
2442
|
+
/** Only an https github.com pull URL earns an anchor (audit IV-11) — a
|
|
2443
|
+
* corrupted row renders as text, never as navigation. */
|
|
2444
|
+
export declare function safePrUrl(url: string | null): string | null;
|
|
2445
|
+
/** The queue reads at most this many completed tasks — the store's own
|
|
2446
|
+
* page ceiling, admission bound before it. */
|
|
2447
|
+
export declare const REVIEW_QUEUE_CAP = 100;
|
|
2448
|
+
/** The one return road the comment endpoint honors besides its own run
|
|
2449
|
+
* page: the cockpit's selected-result link, exactly. */
|
|
2450
|
+
export declare const REVIEW_RETURN: RegExp;
|
|
2451
|
+
/** Review priority: a deterministic, LABELED presentation aid over facts
|
|
2452
|
+
* the done list already carries. Band 0 goes first; the words name why.
|
|
2453
|
+
* It is never a verdict — the stored proof verdict stays the authority,
|
|
2454
|
+
* and an operator's acceptance lowers a band without touching it. */
|
|
2455
|
+
export type ReviewPriority = {
|
|
2456
|
+
band: 0 | 1 | 2;
|
|
2457
|
+
label: "needs action" | "review" | "no flags";
|
|
2458
|
+
reasons: string[];
|
|
2459
|
+
};
|
|
2460
|
+
export type ReviewQueueFacts = {
|
|
2461
|
+
/** Only an exact current task/run match carries the assignment status. */
|
|
2462
|
+
assignment?: AssignmentSnapshot | null;
|
|
2463
|
+
proofReasons?: readonly string[];
|
|
2464
|
+
runId: number | null;
|
|
2465
|
+
outcome: string | null;
|
|
2466
|
+
proofVerdict: ProofVerdict | null;
|
|
2467
|
+
proofAccepted: boolean;
|
|
2468
|
+
proofMatrix: readonly CriterionMatrixRow[];
|
|
2469
|
+
ciFailing: boolean;
|
|
2470
|
+
publicationState: string | null;
|
|
2471
|
+
};
|
|
2472
|
+
export declare const PRIORITY_LABELS: Record<ReviewPriority["band"], ReviewPriority["label"]>;
|
|
2473
|
+
export declare function reviewPriorityOf(row: ReviewQueueFacts): ReviewPriority;
|
|
2474
|
+
/** Ranked, stable: band first, then newest completion, then task id — the
|
|
2475
|
+
* same input always yields the same queue, whoever loads it. */
|
|
2476
|
+
export declare function rankReviewQueue<T extends ReviewQueueFacts & {
|
|
2477
|
+
completedAt: string;
|
|
2478
|
+
taskId: string;
|
|
2479
|
+
}>(rows: readonly T[]): (T & {
|
|
2480
|
+
priority: ReviewPriority;
|
|
2481
|
+
})[];
|
|
2482
|
+
/** Whether a changed path sits inside the scope's signed "touches" — an
|
|
2483
|
+
* exact file, a directory prefix (with or without its slash), or a plain
|
|
2484
|
+
* `*` / `**` glob. Touches are advisory in the scope; here they only
|
|
2485
|
+
* decide which files wear the "outside the signed touches" flag. */
|
|
2486
|
+
export declare function withinSignedTouches(path: string, touches: readonly string[]): boolean;
|
|
2487
|
+
/** A signed touch's glob as a regular expression, gitignore-style: `*`
|
|
2488
|
+
* stays inside one segment; `**` followed by `/` spans zero or more
|
|
2489
|
+
* directories, so `src/**` followed by `/*.ts` matches `src/a.ts` as well
|
|
2490
|
+
* as `src/nested/a.ts` (v2 review, comment 2); a bare `**` spans anything.
|
|
2491
|
+
* Whatever the glob names, its contents are inside it too. */
|
|
2492
|
+
export declare function touchGlob(raw: string): RegExp;
|
|
2493
|
+
/** A stable, attribute-safe anchor for one file of the sealed patch —
|
|
2494
|
+
* derived from the path's bytes, never from the path's characters, so a
|
|
2495
|
+
* hostile file name can neither break the id nor escape the attribute. */
|
|
2496
|
+
export declare function diffFileAnchor(path: string): string;
|
|
2497
|
+
export type ReviewFileRow = {
|
|
2498
|
+
path: string;
|
|
2499
|
+
additions: number | null;
|
|
2500
|
+
deletions: number | null;
|
|
2501
|
+
renamedFrom: string | null;
|
|
2502
|
+
anchor: string | null;
|
|
2503
|
+
outsideTouches: boolean;
|
|
2504
|
+
cited: boolean;
|
|
2505
|
+
};
|
|
2506
|
+
/** Paths whose churn a reviewer reads first — dependency manifests and
|
|
2507
|
+
* locks, CI workflows, container and schema definitions, migrations, the
|
|
2508
|
+
* environment files secrets live in, and files NAMED for credentials.
|
|
2509
|
+
* The credential words match only as whole `-`/`_`/`.`-delimited pieces
|
|
2510
|
+
* of the file name (v2 review, comment 4): `auth.ts`, `api-token.ts`,
|
|
2511
|
+
* `secrets.json` count; `author.ts`, `tokenizer.ts`, and `.envelope.ts`
|
|
2512
|
+
* are ordinary names and do not. `permission` is not a credential word
|
|
2513
|
+
* at all — `permissions-ui.tsx` is a screen, not a secret. */
|
|
2514
|
+
export declare const SENSITIVE_PATH: RegExp;
|
|
2515
|
+
export declare const LARGE_CHANGE_LINES = 200;
|
|
2516
|
+
/** The changed-file list's own review priority: outside the signed
|
|
2517
|
+
* touches first, then files a machine cannot diff or a criterion never
|
|
2518
|
+
* cited, then the rest by churn. Presentation only — the sealed patch
|
|
2519
|
+
* keeps its own order beneath, byte for byte. */
|
|
2520
|
+
export declare function reviewFilePriority(file: ReviewFileRow, proofCitesPaths: boolean): ReviewPriority;
|
|
2521
|
+
export declare function orderChangedFiles(files: readonly ReviewFileRow[], proofCitesPaths: boolean): (ReviewFileRow & {
|
|
2522
|
+
priority: ReviewPriority;
|
|
2523
|
+
})[];
|
|
2524
|
+
export type CompletedWorkRow = ReturnType<Store["listCompletedWorkScoped"]>[number] & {
|
|
2525
|
+
historyProblem?: string | null;
|
|
2526
|
+
};
|
|
2527
|
+
export type RankedReviewRow = CompletedWorkRow & {
|
|
2528
|
+
ciFailing: boolean;
|
|
2529
|
+
priority: ReviewPriority;
|
|
2530
|
+
assignment?: AssignmentSnapshot | null;
|
|
2531
|
+
};
|
|
2532
|
+
/** What the cockpit shows of one selected result — a projection of the
|
|
2533
|
+
* scope, plan, run, artifact, verdict, contest, and publication records,
|
|
2534
|
+
* each read through the verifier the run page already uses. */
|
|
2535
|
+
export type ReviewCockpitView = {
|
|
2536
|
+
taskId: string;
|
|
2537
|
+
title: string;
|
|
2538
|
+
repo: string | null;
|
|
2539
|
+
completedAt: string;
|
|
2540
|
+
historyProblem: string | null;
|
|
2541
|
+
priority: ReviewPriority;
|
|
2542
|
+
assignment: AssignmentSnapshot | null;
|
|
2543
|
+
/** null = no scope was ever filed (a task marked done by hand). */
|
|
2544
|
+
intent: {
|
|
2545
|
+
goal: string;
|
|
2546
|
+
outOfScope: string | null;
|
|
2547
|
+
touches: string[];
|
|
2548
|
+
acceptance: AcceptanceCriterion[];
|
|
2549
|
+
approval: ReturnType<typeof approvalOf>;
|
|
2550
|
+
approvedBy: string | null;
|
|
2551
|
+
} | null;
|
|
2552
|
+
plan: {
|
|
2553
|
+
revision: number;
|
|
2554
|
+
sha256: string;
|
|
2555
|
+
approach: string | null;
|
|
2556
|
+
} | null;
|
|
2557
|
+
contest: {
|
|
2558
|
+
id: number;
|
|
2559
|
+
state: string;
|
|
2560
|
+
kind: "race" | "comparison";
|
|
2561
|
+
agents: number;
|
|
2562
|
+
} | null;
|
|
2563
|
+
/** null = the task is done with no finished build attempt on record. */
|
|
2564
|
+
run: {
|
|
2565
|
+
id: number;
|
|
2566
|
+
role: string;
|
|
2567
|
+
outcome: string | null;
|
|
2568
|
+
runner: string;
|
|
2569
|
+
provider: string;
|
|
2570
|
+
model: string | null;
|
|
2571
|
+
finishedAt: string | null;
|
|
2572
|
+
branch: string | null;
|
|
2573
|
+
headRevision: string | null;
|
|
2574
|
+
ranMinutes: number | null;
|
|
2575
|
+
cost: string;
|
|
2576
|
+
summary: string | null;
|
|
2577
|
+
} | null;
|
|
2578
|
+
/** The shared result detail (package 3) — the same records, the same
|
|
2579
|
+
* panel, as the run page and the chat's result view. */
|
|
2580
|
+
detail: ResultDetail | null;
|
|
2581
|
+
/** v50: the result's bounded review-retry history, null when never asked. */
|
|
2582
|
+
reviewRetry: ReviewRetryState | null;
|
|
2583
|
+
/** The same history as the shared projection's facts (review fixes). */
|
|
2584
|
+
review: ReviewFacts | null;
|
|
2585
|
+
notes: {
|
|
2586
|
+
id: number;
|
|
2587
|
+
author: string;
|
|
2588
|
+
note: string;
|
|
2589
|
+
createdAt: string;
|
|
2590
|
+
}[];
|
|
2591
|
+
/** The demo database: no check runs here, and the Checks row says so. */
|
|
2592
|
+
demo?: boolean;
|
|
2593
|
+
/** A build that didn't deliver a result: what it missed, the suggestion, and Retry when the task can be retried here. */
|
|
2594
|
+
failure?: (FailureExplanation & {
|
|
2595
|
+
retry: {
|
|
2596
|
+
action: string;
|
|
2597
|
+
note: string;
|
|
2598
|
+
} | null;
|
|
2599
|
+
acceptAnyway?: {
|
|
2600
|
+
action: string;
|
|
2601
|
+
run: number;
|
|
2602
|
+
} | null;
|
|
2603
|
+
}) | null;
|
|
2604
|
+
};
|
|
2605
|
+
/** The one result page's address: the task and its run, never a project path
|
|
2606
|
+
* (the page finds the result's project itself and keeps the person's own). */
|
|
2607
|
+
export declare const reviewHref: (taskId: string, runId?: number | null) => string;
|
|
2608
|
+
/** The receipt's own proof-state word, so the cockpit and the task page
|
|
2609
|
+
* never disagree on the state's name or its precedence: the stored
|
|
2610
|
+
* verdict decides, whatever the run's outcome — a no-change run with a
|
|
2611
|
+
* refuted proof reads as refuted, never as "no change needed". The one
|
|
2612
|
+
* word the receipt never needs is "No build record": a manual completion
|
|
2613
|
+
* has no receipt to share it with. */
|
|
2614
|
+
/** The cockpit's headline status: the shared projection over the same
|
|
2615
|
+
* verdict, acceptance, and publication rows the task page reads. */
|
|
2616
|
+
export declare function cockpitStatusOf(view: ReviewCockpitView): DisplayStatus;
|
|
2617
|
+
/** Turn verifier records into one sentence a project owner can act on.
|
|
2618
|
+
* A command-not-found result means the check could not start, not that the
|
|
2619
|
+
* product itself failed. The stored record remains unchanged. */
|
|
2620
|
+
export declare function verificationExplanation(verdict: ProofVerdict | null, reasons: readonly string[]): string;
|
|
2621
|
+
export declare function verificationRecovered(reasons: readonly string[]): boolean;
|
|
2622
|
+
/** Whether a reviewer can annotate this result's diff here: the shared
|
|
2623
|
+
* detail's own answer (a verified, non-empty sealed patch read by a
|
|
2624
|
+
* session that holds a CSRF token) — decided once, in `resultDetailOf`. */
|
|
2625
|
+
export declare function canAnnotateDiff(view: ReviewCockpitView | null, csrf: string): boolean;
|
|
2626
|
+
export declare function reviewCockpitPage(chrome: Chrome, data: {
|
|
2627
|
+
queue: readonly RankedReviewRow[];
|
|
2628
|
+
/** The queue's ceiling — printed when the queue reaches it. */
|
|
2629
|
+
queueCap: number;
|
|
2630
|
+
selected: ReviewCockpitView | null;
|
|
2631
|
+
/** The deep link resolved a completion older than the queue shows. */
|
|
2632
|
+
beyondQueue: boolean;
|
|
2633
|
+
missing: string | null;
|
|
2634
|
+
csrf: string;
|
|
2635
|
+
/** v50: an approver's session may ask for a review retry here. */
|
|
2636
|
+
canRetryReview: boolean;
|
|
2637
|
+
noted: boolean;
|
|
2638
|
+
/** What this person's last post from this page was refused for, in words (`refused=` on the address). */
|
|
2639
|
+
refusal?: string | null;
|
|
2640
|
+
/** Package 3: the selected local result view and the panel's draft keys. */
|
|
2641
|
+
tab: ResultTab;
|
|
2642
|
+
user: string;
|
|
2643
|
+
now: Date;
|
|
2644
|
+
}): Screen;
|
|
2645
|
+
/** The selected result: intent → proof → changes → publication → acts,
|
|
2646
|
+
* one scan path, every fact labeled by its source. */
|
|
2647
|
+
export declare function reviewCockpitDetail(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview?: boolean, tab?: ResultTab, user?: string): string;
|
|
2648
|
+
/** The selected result's HTML and, for the rebuilt page, the same result
|
|
2649
|
+
* as data: every form and panel body is the one the HTML carries. */
|
|
2650
|
+
export declare function reviewCockpitDetailParts(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview: boolean, tab: ResultTab, user: string): {
|
|
2651
|
+
html: string;
|
|
2652
|
+
selected: NonNullable<BrowserResultView["selected"]>;
|
|
2653
|
+
};
|
|
2654
|
+
/** What Accept and finish owes before it completes, as the form asks for it: the server's own rule
|
|
2655
|
+
* (owedAcceptance), so the form asks for exactly what the completion requires. */
|
|
2656
|
+
export declare function owedAcceptanceOf(receipt: AssignmentSnapshot["receipt"]): {
|
|
2657
|
+
note: string | null;
|
|
2658
|
+
} | null;
|
|
2659
|
+
export declare function completionForm(taskId: string, runId: number, digest: string, csrf: string, pullRequestTo?: string | null, accept?: {
|
|
2660
|
+
note: string | null;
|
|
2661
|
+
} | null): string;
|
|
2662
|
+
/** What the task page shows about a result's pull request: the view, or the offer to open one. */
|
|
2663
|
+
export type TaskPullRequest = {
|
|
2664
|
+
taskId: string;
|
|
2665
|
+
view: PullRequestView | null;
|
|
2666
|
+
offer: {
|
|
2667
|
+
taskId: string;
|
|
2668
|
+
runId: number;
|
|
2669
|
+
digest: string;
|
|
2670
|
+
} | null;
|
|
2671
|
+
target: string | null;
|
|
2672
|
+
/** The same pull request as the shared status's detail row reads it. */
|
|
2673
|
+
fact?: PullRequestFact | null;
|
|
2674
|
+
};
|
|
2675
|
+
/** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
|
|
2676
|
+
* behind the password when checks passed, or Open a pull request for a result completed without one. */
|
|
2677
|
+
export declare function pullRequestCardHtml(pr: TaskPullRequest, csrf: string): string;
|
|
2678
|
+
/** Exactly one primary road per result, chosen from its state; the
|
|
2679
|
+
* others stay reachable from their own sections. Every form posts to the
|
|
2680
|
+
* endpoint that already owns the act, with the session's CSRF token; a
|
|
2681
|
+
* bearer session (no token) sees the road named, never a form. */
|
|
2682
|
+
export declare function reviewNextAction(view: ReviewCockpitView, csrf: string, _accepted: boolean, _canAnnotate: boolean): string;
|
|
2683
|
+
export declare function reviewNextActionOf(view: ReviewCockpitView, csrf: string): {
|
|
2684
|
+
kind: string;
|
|
2685
|
+
title: string;
|
|
2686
|
+
detail: string;
|
|
2687
|
+
control: string;
|
|
2688
|
+
} | null;
|
|
2689
|
+
/**
|
|
2690
|
+
* Plain words for the machine's own vocabulary — no internal token ever
|
|
2691
|
+
* reaches a page. Every map here has a generic fallback: a kind a newer
|
|
2692
|
+
* daemon invents degrades to honest generic prose, never to its raw name.
|
|
2693
|
+
*/
|
|
2694
|
+
export declare const PHASE_WORDS: Record<string, string>;
|
|
2695
|
+
/** An agent's phase on the home card, from the machine's own vocabulary. */
|
|
2696
|
+
export declare function homePhaseWords(run: Pick<Run, "role" | "phase">): string;
|
|
2697
|
+
export declare function phaseWords(phase: string): string;
|
|
2698
|
+
/** A recorded reason in one plain line; machine output (a stack trace, a path, "Error:" text) waits on the run record. */
|
|
2699
|
+
export declare function reasonWords(reason: string): string;
|
|
2700
|
+
export declare const INCIDENT_WORDS: Record<string, string>;
|
|
2701
|
+
export declare function incidentWords(kind: string): string;
|
|
2702
|
+
export declare const EVIDENCE_WORDS: Record<string, string>;
|
|
2703
|
+
/** Byte counts as people read them. */
|
|
2704
|
+
export declare function humanBytes(bytes: number): string;
|
|
2705
|
+
export declare function evidenceWords(kind: string): string;
|
|
2706
|
+
/** A shortened fingerprint for display — enough to compare by eye; the
|
|
2707
|
+
* full value rides in the title attribute and in every form field. */
|
|
2708
|
+
export declare function shortDigest(digest: string): string;
|
|
2709
|
+
/** The badge for a run's outcome. A null outcome reads "running" ONLY when
|
|
2710
|
+
* the caller proved the run's lease is the task's current live claim — an
|
|
2711
|
+
* orphaned run keeps saying what actually became of it. */
|
|
2712
|
+
export declare function runOutcomeBadge(run: Run, live: boolean): string;
|
|
2713
|
+
export declare const runNoun: (run: Pick<Run, "role">) => string;
|
|
2714
|
+
export declare function runsPage(chrome: Chrome, rows: (Run & {
|
|
2715
|
+
taskId: string;
|
|
2716
|
+
})[], liveIds: ReadonlySet<number>, nextCursor: number | null, verdicts?: Map<number, {
|
|
2717
|
+
verdict: ProofVerdict;
|
|
2718
|
+
matrix?: CriterionMatrixRow[];
|
|
2719
|
+
}>, accepted?: ReadonlySet<number>): Screen;
|
|
2720
|
+
/** The evidence bundle (Priority 2): the closed machine-authored verdict,
|
|
2721
|
+
* the agent's proof (or why it cannot be shown), the plane's own re-run
|
|
2722
|
+
* check, and every validated screenshot — each row labeled by source so
|
|
2723
|
+
* "the agent said" and "the machine proved" never blur together. */
|
|
2724
|
+
export type ProofBundleView = {
|
|
2725
|
+
verdict: ProofVerdict | null;
|
|
2726
|
+
reasons: string[];
|
|
2727
|
+
accepted: {
|
|
2728
|
+
by: string;
|
|
2729
|
+
note: string | null;
|
|
2730
|
+
at: string;
|
|
2731
|
+
} | null;
|
|
2732
|
+
proof: {
|
|
2733
|
+
criteria: {
|
|
2734
|
+
statement: string;
|
|
2735
|
+
verdict: string;
|
|
2736
|
+
how: string;
|
|
2737
|
+
}[];
|
|
2738
|
+
checks: {
|
|
2739
|
+
command: string;
|
|
2740
|
+
exitCode: number;
|
|
2741
|
+
summary: string;
|
|
2742
|
+
}[];
|
|
2743
|
+
caveats: string[];
|
|
2744
|
+
} | null;
|
|
2745
|
+
proofProblem: string | null;
|
|
2746
|
+
/** The check log as stored: its text when the bytes verify, else the
|
|
2747
|
+
* `problem` (repair 2026-09-14) — a log that no longer verifies shows no
|
|
2748
|
+
* text and offers no download. `bytesOriginal`/`bytesStored` let a
|
|
2749
|
+
* shortened log's download be described as the stored part only. */
|
|
2750
|
+
checkLog: {
|
|
2751
|
+
text: string;
|
|
2752
|
+
artifactId: number;
|
|
2753
|
+
truncated: boolean;
|
|
2754
|
+
problem: string | null;
|
|
2755
|
+
bytesOriginal: number;
|
|
2756
|
+
bytesStored: number;
|
|
2757
|
+
} | null;
|
|
2758
|
+
/** Every stored screenshot artifact; one whose bytes no longer verify
|
|
2759
|
+
* carries its `problem` and is never rendered or called validated
|
|
2760
|
+
* (workspace package 3). */
|
|
2761
|
+
screenshots: ResultScreenshot[];
|
|
2762
|
+
/** Screenshot paths the proof cites that no stored artifact answers. */
|
|
2763
|
+
uncapturedScreenshots: string[];
|
|
2764
|
+
/** v39: the criterion-to-evidence matrix, one row per signed criterion —
|
|
2765
|
+
* `[]` when the scope this run built against signed no rubric. */
|
|
2766
|
+
matrix: CriterionMatrixRow[];
|
|
2767
|
+
/** v39 review finding: where a row's answered evidence ref resolves to a
|
|
2768
|
+
* stored artifact, for `criterionMatrixHtml` to link. */
|
|
2769
|
+
matrixLinks: EvidenceLinkMap;
|
|
2770
|
+
/** v40: the verdict BEFORE an independent reviewer's judgements were
|
|
2771
|
+
* folded in — null when no review has folded. */
|
|
2772
|
+
machineVerdict: ProofVerdict | null;
|
|
2773
|
+
/** v40: this run's own place in a bounded repair chain, if any. */
|
|
2774
|
+
repairChain: RepairChainRow | null;
|
|
2775
|
+
/** v51: the policy semantic coverage is read under. */
|
|
2776
|
+
qualityMode: "default" | "strict";
|
|
2777
|
+
};
|
|
2778
|
+
/** The smallest complete answer to "what did this task deliver?". It is a
|
|
2779
|
+
* projection of the same sealed handoff, proof, screenshot, and diff records
|
|
2780
|
+
* used by the run page—not a new persistence layer or another verdict. */
|
|
2781
|
+
export type CompletionReceiptView = {
|
|
2782
|
+
runId: number;
|
|
2783
|
+
/** The run's role — a scout's receipt is a report, not a diff. */
|
|
2784
|
+
role: string;
|
|
2785
|
+
outcome: string | null;
|
|
2786
|
+
summary: string | null;
|
|
2787
|
+
verdict: ProofVerdict | null;
|
|
2788
|
+
/** The verdict's recorded reasons — what tells a failed check from
|
|
2789
|
+
* mismatched evidence (package 1). */
|
|
2790
|
+
reasons: string[];
|
|
2791
|
+
accepted: boolean;
|
|
2792
|
+
/** A no-change conclusion's two records — handoff and sealed diff. */
|
|
2793
|
+
recordComplete: boolean;
|
|
2794
|
+
/** The run's own publication record, when one exists: the heading names
|
|
2795
|
+
* a PR or a merge only from this, never from the outcome. */
|
|
2796
|
+
publication: PublicationFacts;
|
|
2797
|
+
/** This run's independent review, when one was asked (review fixes). */
|
|
2798
|
+
review: ReviewFacts | null;
|
|
2799
|
+
matrix: CriterionMatrixRow[];
|
|
2800
|
+
/** v51: the semantic-coverage lines (`coverageWords`) — independent
|
|
2801
|
+
* review standing under the run's policy, plus every named context gap.
|
|
2802
|
+
* Rendered beside the machine verdict on the task and chat receipts. */
|
|
2803
|
+
coverage: string[];
|
|
2804
|
+
/** Concise pass (2026-09-13): the coverage lines are secondary — behind a
|
|
2805
|
+
* disclosure — only while nothing is owed: review optional and not yet
|
|
2806
|
+
* settled, or satisfied. A strict-quality shortfall, a strict review
|
|
2807
|
+
* still pending, or a named context gap stays in the open. */
|
|
2808
|
+
coverageSecondary: boolean;
|
|
2809
|
+
diff: {
|
|
2810
|
+
fileCount: number;
|
|
2811
|
+
additions: number;
|
|
2812
|
+
deletions: number;
|
|
2813
|
+
binaryCount: number;
|
|
2814
|
+
filesTruncated: boolean;
|
|
2815
|
+
} | {
|
|
2816
|
+
problem: string;
|
|
2817
|
+
} | null;
|
|
2818
|
+
screenshots: ResultScreenshot[];
|
|
2819
|
+
caveats: string[];
|
|
2820
|
+
/** The run's own report artifact (a scout's deliverable), verified at
|
|
2821
|
+
* render — the investigation's result leads with it (package 3). */
|
|
2822
|
+
report: RunReportView | null;
|
|
2823
|
+
/** The facts every result surface prints identically (package 3). */
|
|
2824
|
+
facts: SharedResultFacts;
|
|
2825
|
+
};
|
|
2826
|
+
/** A scout run's report as this run stored it: verified and parsed, or
|
|
2827
|
+
* the reason it cannot be shown. The download link serves the exact
|
|
2828
|
+
* stored bytes as text — never as a page. */
|
|
2829
|
+
export type RunReportView = {
|
|
2830
|
+
ok: true;
|
|
2831
|
+
artifactId: number;
|
|
2832
|
+
title: string;
|
|
2833
|
+
summary: string;
|
|
2834
|
+
document: string;
|
|
2835
|
+
followUps: number;
|
|
2836
|
+
truncated: boolean;
|
|
2837
|
+
items: ReportItem[];
|
|
2838
|
+
shots: ReportShot[];
|
|
2839
|
+
} | {
|
|
2840
|
+
ok: false;
|
|
2841
|
+
artifactId: number;
|
|
2842
|
+
problem: string;
|
|
2843
|
+
};
|
|
2844
|
+
/** What a scout found, each with its link and picture, then any screenshot no item shows. Every link is the cited
|
|
2845
|
+
* http(s) address the report parser admitted; every picture is served from verified evidence. */
|
|
2846
|
+
export declare function reportItemsHtml(items: readonly ReportItem[], shots: readonly ReportShot[], runId: number): string;
|
|
2847
|
+
export declare function runReportView(artifacts: Artifact[], root: string): RunReportView | null;
|
|
2848
|
+
/** The shared facts (package 3), computed ONCE from the same verified
|
|
2849
|
+
* records every surface reads: the head from the sealed diff summary when
|
|
2850
|
+
* it verifies (else the run record, and the source is named), the signed
|
|
2851
|
+
* criteria passed from the machine's matrix, the proof's caveats, every
|
|
2852
|
+
* evidence problem in words, and the observed publication state. */
|
|
2853
|
+
export declare function sharedResultFactsOf(run: Run, proof: ProofBundleView | null, terminal: TerminalDiffView | null, handoff: StructuredHandoffView | null, report: RunReportView | null, publication: PublicationFacts): SharedResultFacts;
|
|
2854
|
+
export declare function proofBundleView(store: Store, run: Run, artifacts: Artifact[], root: string): ProofBundleView | null;
|
|
2855
|
+
export declare function completionReceiptView(store: Store, run: Run, artifacts: Artifact[], root: string, review?: ReviewFacts | null): CompletionReceiptView;
|
|
2856
|
+
export type ReviewDiffLine = {
|
|
2857
|
+
kind: "context" | "addition" | "deletion" | "meta";
|
|
2858
|
+
text: string;
|
|
2859
|
+
oldLine: number | null;
|
|
2860
|
+
newLine: number | null;
|
|
2861
|
+
};
|
|
2862
|
+
export type ReviewDiffHunk = {
|
|
2863
|
+
header: string;
|
|
2864
|
+
lines: ReviewDiffLine[];
|
|
2865
|
+
};
|
|
2866
|
+
export type ReviewDiffFile = {
|
|
2867
|
+
path: string;
|
|
2868
|
+
oldPath: string | null;
|
|
2869
|
+
meta: string[];
|
|
2870
|
+
hunks: ReviewDiffHunk[];
|
|
2871
|
+
};
|
|
2872
|
+
export type ReviewDiff = {
|
|
2873
|
+
files: ReviewDiffFile[];
|
|
2874
|
+
linesTruncated: boolean;
|
|
2875
|
+
};
|
|
2876
|
+
export declare const REVIEW_DIFF_LINE_CAP = 4000;
|
|
2877
|
+
/** Parse only the stable structure Git's unified patch format guarantees.
|
|
2878
|
+
* Unknown metadata remains visible, and a patch that cannot be structured
|
|
2879
|
+
* falls back to the sealed raw record—presentation never becomes proof. */
|
|
2880
|
+
export declare function parseReviewDiff(text: string): ReviewDiff;
|
|
2881
|
+
export declare function reviewDiffHtml(patch: {
|
|
2882
|
+
text: string;
|
|
2883
|
+
truncated: boolean;
|
|
2884
|
+
artifactId: number;
|
|
2885
|
+
}, stat: TerminalDiffView["stat"], runId: number, commentable: boolean, anchors?: ReadonlyMap<string, string>): string;
|
|
2886
|
+
/** The check log as stored (repair 2026-09-14): its output behind a
|
|
2887
|
+
* disclosure when the bytes verify — a shortened log says so in the
|
|
2888
|
+
* summary and its download is the stored part; a log that no longer
|
|
2889
|
+
* verifies is a named problem with no output and no download. */
|
|
2890
|
+
export declare function checkLogHtml(log: NonNullable<ProofBundleView["checkLog"]>, runId: number, attributes: string): string;
|
|
2891
|
+
/** The download link for a stored record (repair 2026-09-14): a shortened
|
|
2892
|
+
* record's link says it holds the stored part only — never "the full"
|
|
2893
|
+
* bytes that were cut at storage and exist nowhere. */
|
|
2894
|
+
export declare function storedDownloadLink(runId: number, artifactId: number, what: string, truncated: boolean): string;
|
|
2895
|
+
/** Render the terminal diff card: stat and capture health first, the bounded
|
|
2896
|
+
* patch beneath a fold. `editor` (arc 6) links file rows to vscode:// on the
|
|
2897
|
+
* reviewing device; `commentable` adds a per-file "comment" button the
|
|
2898
|
+
* page's prefill script reads — a real button, keyboard-reachable, separate
|
|
2899
|
+
* from the link so the two never fight over one click (finding 4). */
|
|
2900
|
+
export declare function terminalDiffCard(view: TerminalDiffView, runId: number, editor?: {
|
|
2901
|
+
worktree: string;
|
|
2902
|
+
} | null, commentable?: boolean): string;
|
|
2903
|
+
/**
|
|
2904
|
+
* The evidence bundle (Priority 2): the closed verdict first — the one
|
|
2905
|
+
* sentence every other surface agrees with — then criteria, the agent's
|
|
2906
|
+
* declared checks, the plane's own re-run (labeled "re-run here", never
|
|
2907
|
+
* confused with the agent's own claim), caveats, and every validated
|
|
2908
|
+
* screenshot as a thumbnail linking to the full image. Renders nothing
|
|
2909
|
+
* when the run predates the proof system.
|
|
2910
|
+
*/
|
|
2911
|
+
export declare function evidenceBundleCard(view: ProofBundleView | null, run: Pick<Run, "id" | "role" | "outcome">, publication?: PublicationFacts, review?: ReviewFacts | null): string;
|
|
2912
|
+
/** A calm, scan-first result receipt for the task and its focused chat.
|
|
2913
|
+
* The full ledger remains one click away; this card carries only the facts
|
|
2914
|
+
* needed to decide whether to inspect, discuss, or move on. */
|
|
2915
|
+
/** The receipt's proof-state word and tone, from the stored verdict and
|
|
2916
|
+
* the operator's acceptance alone — shared with the review cockpit's
|
|
2917
|
+
* header chip so the two surfaces never name one state differently. */
|
|
2918
|
+
/** The receipt's facts as the shared projection reads them. */
|
|
2919
|
+
/** A result's follow-ups: checks run on its commit since it finished, and its tests task. */
|
|
2920
|
+
export declare function followUpsFor(store: Store, evidenceRoot: string, run: Run, now: Date): NonNullable<ResultDetail["followUps"]> | null;
|
|
2921
|
+
export declare const FOLLOW_UP_WORDS: Record<FollowUpCheck["state"], string>;
|
|
2922
|
+
/** Run checks and Add tests, under the result's Checks: what ran since, then the two acts. */
|
|
2923
|
+
export declare function followUpsHtml(followUps: NonNullable<ResultDetail["followUps"]>, runId: number, o: ResultPanelOptions): string;
|
|
2924
|
+
export declare function receiptStatusOf(view: CompletionReceiptView, review?: ReviewFacts | null): DisplayStatus;
|
|
2925
|
+
export declare function completionReceiptCard(view: CompletionReceiptView, taskId: string, place: "task" | "chat", standing?: DisplayStatus, assignment?: AssignmentSnapshot | null, headline?: boolean): string;
|
|
2926
|
+
/** The chat's result detail (package 3): the same panel the run page
|
|
2927
|
+
* and the cockpit render, opened beside the conversation when the screen
|
|
2928
|
+
* has room and as a dedicated view with Back to chat when it does not. */
|
|
2929
|
+
export declare const chatResultHref: (taskId: string, runId: number, tab?: ResultTab) => string;
|
|
2930
|
+
/**
|
|
2931
|
+
* ONE result presentation (workspace package 3) for the run page, the
|
|
2932
|
+
* review cockpit, and the chat's result detail. Every fact here comes
|
|
2933
|
+
* from the same verified records the receipt reads; the surfaces differ
|
|
2934
|
+
* only in where the panel sits and which links lead away from it.
|
|
2935
|
+
*/
|
|
2936
|
+
export type ResultDetail = {
|
|
2937
|
+
/** Where "Complete and open a pull request" opens one; null when only "Mark complete" is offered. */
|
|
2938
|
+
pullRequestTo?: string | null;
|
|
2939
|
+
/** Where publishing stands: a pull request can be opened, publishing isn't set up, or neither said. Accept never publishes. */
|
|
2940
|
+
publishing?: "pull-request" | "off" | "other";
|
|
2941
|
+
learning?: string;
|
|
2942
|
+
skillTest?: boolean;
|
|
2943
|
+
rootId?: string;
|
|
2944
|
+
history?: string;
|
|
2945
|
+
taskId: string;
|
|
2946
|
+
run: Run;
|
|
2947
|
+
receipt: CompletionReceiptView;
|
|
2948
|
+
assignment?: AssignmentSnapshot | null;
|
|
2949
|
+
handoff: StructuredHandoffView | null;
|
|
2950
|
+
proof: ProofBundleView | null;
|
|
2951
|
+
terminal: TerminalDiffView | null;
|
|
2952
|
+
publication: Publication | null;
|
|
2953
|
+
ciFailing: boolean;
|
|
2954
|
+
files: (ReviewFileRow & {
|
|
2955
|
+
priority: ReviewPriority;
|
|
2956
|
+
})[];
|
|
2957
|
+
outsideTouches: string[];
|
|
2958
|
+
fileAnchors: ReadonlyMap<string, string>;
|
|
2959
|
+
comments: DiffComment[];
|
|
2960
|
+
pastComments?: DiffComment[];
|
|
2961
|
+
reviewerFindings: DiffComment[];
|
|
2962
|
+
/** The build's one automatic review, when its project had review on. */
|
|
2963
|
+
automaticReview?: BuildReviewView | null;
|
|
2964
|
+
/** Revisions already sealed from this run — the forward link. Each
|
|
2965
|
+
* carries the shared status projection's own words for the child
|
|
2966
|
+
* (repair 2026-09-14): CURRENT exact approval and actual activity, so
|
|
2967
|
+
* held or paused work is never called building because it was once
|
|
2968
|
+
* approved, and a rescoped child reads as needing approval again. */
|
|
2969
|
+
revisions: {
|
|
2970
|
+
id: string;
|
|
2971
|
+
title: string;
|
|
2972
|
+
state: TaskState;
|
|
2973
|
+
approved: boolean;
|
|
2974
|
+
standing: string;
|
|
2975
|
+
tone: string;
|
|
2976
|
+
}[];
|
|
2977
|
+
/** The source scope digest the panel was rendered against (null = no
|
|
2978
|
+
* scope) — the revision form's source binding. */
|
|
2979
|
+
sourceDigest: string | null;
|
|
2980
|
+
route: RouteStamp | null;
|
|
2981
|
+
editor: {
|
|
2982
|
+
worktree: string;
|
|
2983
|
+
} | null;
|
|
2984
|
+
/** Signed criteria on the scope this run built against. */
|
|
2985
|
+
signedCriteria: number;
|
|
2986
|
+
/** Whether a reviewer can annotate here (sealed non-empty patch verifies, cookie session). */
|
|
2987
|
+
canAnnotate: boolean;
|
|
2988
|
+
/** Follow-ups on this result (result-follow-ups.ts): checks run since, the tests task, and what can run. */
|
|
2989
|
+
followUps?: {
|
|
2990
|
+
repo: string | null;
|
|
2991
|
+
checks: FollowUpCheck[];
|
|
2992
|
+
testsTask: string | null;
|
|
2993
|
+
quick: boolean;
|
|
2994
|
+
full: boolean;
|
|
2995
|
+
} | null;
|
|
2996
|
+
};
|
|
2997
|
+
export type ResultPanelOptions = {
|
|
2998
|
+
place: "run" | "review" | "chat";
|
|
2999
|
+
tab: ResultTab;
|
|
3000
|
+
csrf: string;
|
|
3001
|
+
/** The server-named account, for the browser's bounded draft keys. */
|
|
3002
|
+
user: string;
|
|
3003
|
+
/** The page carries a receipt for a just-posted note: focus the box. */
|
|
3004
|
+
noted: boolean;
|
|
3005
|
+
/** This render's request token for the note form (replay dedupe). */
|
|
3006
|
+
requestToken: string;
|
|
3007
|
+
hrefFor: (tab: ResultTab) => string;
|
|
3008
|
+
/** Where the forms send the reader back (validated server-side too). */
|
|
3009
|
+
returnTo: string;
|
|
3010
|
+
back: {
|
|
3011
|
+
href: string;
|
|
3012
|
+
label: string;
|
|
3013
|
+
} | null;
|
|
3014
|
+
/** Surface-specific acts that belong under Checks (the cockpit's review
|
|
3015
|
+
* retry panel and its accept-with-exception form). */
|
|
3016
|
+
extraChecks?: string;
|
|
3017
|
+
/** The cockpit's header already carries the status chip: said once. */
|
|
3018
|
+
headStatus?: boolean;
|
|
3019
|
+
/** The cockpit renders its own next-action card: no panel action. */
|
|
3020
|
+
action?: boolean;
|
|
3021
|
+
/** Accept and finish sits beside the panel: it records the person's own checks too, so the panel offers no separate Accept. */
|
|
3022
|
+
finishes?: boolean;
|
|
3023
|
+
};
|
|
3024
|
+
/** The revision form's binding (repair 2026-09-14): the exact ids of the
|
|
3025
|
+
* notes this page displays and the source terms it was rendered against.
|
|
3026
|
+
* The server seals these ids and no others. */
|
|
3027
|
+
export declare function revisionSealFields(comments: readonly {
|
|
3028
|
+
id: number;
|
|
3029
|
+
}[], sourceDigest: string | null): string;
|
|
3030
|
+
/** Older machine-checked results saved a mechanical sentence; say the same
|
|
3031
|
+
* facts in plain words (the version id stays, shortened). */
|
|
3032
|
+
export declare function plainConclusionOf(conclusion: string): string;
|
|
3033
|
+
export declare function resultPanelHtml(detail: ResultDetail, o: ResultPanelOptions): string;
|
|
3034
|
+
/** The panel's HTML and the same panel in parts, for the rebuilt result page. */
|
|
3035
|
+
export declare function resultPanelParts(detail: ResultDetail, o: ResultPanelOptions): {
|
|
3036
|
+
html: string;
|
|
3037
|
+
panel: BrowserResultPanel;
|
|
3038
|
+
};
|
|
3039
|
+
/** Changed lines shown beside one "You check this one" item: at most this many per file, three files. */
|
|
3040
|
+
export declare const CHECK_EXCERPT_LINES = 12;
|
|
3041
|
+
/** Each requirement only a person can confirm, with the evidence to judge
|
|
3042
|
+
* it by, inline: the changed lines in the files it cites (citing none, the
|
|
3043
|
+
* change's first file, labelled as not cited), the screenshots it cites
|
|
3044
|
+
* (none cited: every validated one), and the agent's own note. */
|
|
3045
|
+
export declare function personCheckItems(proof: ProofBundleView, patchText: string | null, shots: readonly ResultScreenshot[], runId: number): BrowserCheckItem[];
|
|
3046
|
+
/** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
|
|
3047
|
+
* stopped behind the password. Null under every other headline. */
|
|
3048
|
+
export declare function needActionOf(assignment: AssignmentSnapshot | null, status: TaskStatus | null, csrf: string, returnTo: string, result?: {
|
|
3049
|
+
accepted: boolean;
|
|
3050
|
+
humanReview: boolean;
|
|
3051
|
+
run: number;
|
|
3052
|
+
action: string;
|
|
3053
|
+
rebuildable?: boolean;
|
|
3054
|
+
refuted?: boolean;
|
|
3055
|
+
}): BrowserNeedAction | null;
|
|
3056
|
+
/** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
|
|
3057
|
+
export declare const needsCheck: (assignment: AssignmentSnapshot) => boolean;
|
|
3058
|
+
/** The server page's form of the same: the need's action first, Request changes as the quiet second. */
|
|
3059
|
+
export declare function resultNeedAction(need: BrowserNeedAction, o: ResultPanelOptions, canRequest: boolean): string;
|
|
3060
|
+
/** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
|
|
3061
|
+
* first sentence, cut at a word when it runs past 180 characters. The
|
|
3062
|
+
* full conclusion stays available behind the Summary's disclosure. */
|
|
3063
|
+
export declare function conciseOutcomeOf(conclusion: string): string;
|
|
3064
|
+
/** Exactly one primary act for the result (repair 2026-09-14), chosen from
|
|
3065
|
+
* its state: draft the CI repair, create the revision the notes are
|
|
3066
|
+
* waiting for, open the pull request, or request changes. A bearer session
|
|
3067
|
+
* (no token) gets no control; the cockpit renders its own next-action
|
|
3068
|
+
* card instead (`action: false`). */
|
|
3069
|
+
export declare function resultPrimaryAction(detail: ResultDetail, o: ResultPanelOptions, prUrl: string | null): string;
|
|
3070
|
+
/** The run's facts as rows — one renderer for the page and its live
|
|
3071
|
+
* fragment (A4). Elapsed ticks client-side while the run is open. */
|
|
3072
|
+
/** The run's route provenance in plain words (v47): the exact agent it
|
|
3073
|
+
* spent as, in which role, and how that agent was chosen. */
|
|
3074
|
+
export declare function runAgentWords(route: RouteStamp | null | undefined): string | null;
|
|
3075
|
+
export declare function runFactsRows(run: Run, taskId: string, live: boolean, route?: RouteStamp | null): string;
|
|
3076
|
+
export declare function runPage(chrome: Chrome, run: Run, taskId: string, artifacts: Artifact[], terminal?: TerminalDiffView | null, notes?: {
|
|
3077
|
+
id: number;
|
|
3078
|
+
author: string;
|
|
3079
|
+
note: string;
|
|
3080
|
+
createdAt: string;
|
|
3081
|
+
}[], csrf?: string, comments?: DiffComment[], ciRepair?: {
|
|
3082
|
+
pr: number;
|
|
3083
|
+
} | null, liveScript?: string, peekable?: boolean, running?: boolean, editor?: {
|
|
3084
|
+
worktree: string;
|
|
3085
|
+
} | null, editorToggle?: {
|
|
3086
|
+
on: boolean;
|
|
3087
|
+
} | null, noted?: boolean, heldTurns?: {
|
|
3088
|
+
turns: SessionTurn[];
|
|
3089
|
+
open: boolean;
|
|
3090
|
+
state: string;
|
|
3091
|
+
cap: number;
|
|
3092
|
+
} | null, continueOffer?: {
|
|
3093
|
+
taskId: string;
|
|
3094
|
+
} | null, structuredHandoff?: StructuredHandoffView | null, proofBundle?: ProofBundleView | null, route?: RouteStamp | null, publicationRow?: Publication | null, review?: ReviewFacts | null, result?: {
|
|
3095
|
+
detail: ResultDetail;
|
|
3096
|
+
tab: ResultTab;
|
|
3097
|
+
user: string;
|
|
3098
|
+
requestToken: string;
|
|
3099
|
+
} | null, sourceDigest?: string | null): Screen;
|
|
3100
|
+
/**
|
|
3101
|
+
* Click-to-prefill (arc 6, finding 4): client-side FORM mutation, named as
|
|
3102
|
+
* such — normal viewing stays the default; "Annotate" reveals line pins.
|
|
3103
|
+
* A pin or file button copies its target into the comment form and focuses
|
|
3104
|
+
* the note field. No fetch, endpoint, or submit: comments still leave
|
|
3105
|
+
* through the same CSRF'd form POST. Reads data attributes, writes input
|
|
3106
|
+
* values, and flips presentational state — never markup. The note counter
|
|
3107
|
+
* (follow-up on build 1540) reads the textarea's own maxlength — the
|
|
3108
|
+
* server's LIMITS.note — into the helper text, so the two cannot drift.
|
|
3109
|
+
*/
|
|
3110
|
+
export declare function prefillScript(): string;
|
|
3111
|
+
/** The primary review act opens the Checks view it names. The anchor
|
|
3112
|
+
* still lands on the result without JavaScript (the server honours
|
|
3113
|
+
* `?tab=checks`); this only removes the reload. */
|
|
3114
|
+
export declare function reviewEvidenceScript(): string;
|
|
3115
|
+
/** The facts region alone, for the open-run poll (A4). A finished run's
|
|
3116
|
+
* fragment says so instead of quietly growing forms (finding 5), and a run
|
|
3117
|
+
* that stopped being the task's live claim says so too — both carry the
|
|
3118
|
+
* stop marker, so an open tab quits refetching a dead build (round-4
|
|
3119
|
+
* finding 15). */
|
|
3120
|
+
export declare function runFactsFragment(run: Run, taskId: string, live: boolean, route?: RouteStamp | null): string;
|
|
3121
|
+
/** One accessible two-choice control everywhere permissions are selected.
|
|
3122
|
+
* The words describe behavior; the raw flag stays secondary detail. */
|
|
3123
|
+
export declare function permissionModeChoices(name: string, selected: UnattendedPermissionMode): string;
|
|
3124
|
+
/** Quality selects the configured agent route; permissions remain separate. */
|
|
3125
|
+
export declare function qualityModeChoices(name: string, selected: QualityMode): string;
|
|
3126
|
+
export declare function capsPage(chrome: Chrome, caps: Capability[] | null, gaps: Gap[], repo: string, now?: Date): Screen;
|
|
3127
|
+
/** Whether the bot's saved delivery state reports a problem (no new check runs). */
|
|
3128
|
+
export declare function telegramTrouble(store: Store, bot: TokenSource | null): boolean;
|
|
3129
|
+
/** v98: how the bot's messages reach Toolroll, in words: pushed to the public address, or asked for. */
|
|
3130
|
+
export declare function telegramDeliveryWords(store: Store, bot: TokenSource | null): string | null;
|
|
3131
|
+
/** The projects this person can see, by name, and whether they muted each one's pings. */
|
|
3132
|
+
export declare function notificationProjects(store: Store, account: string): {
|
|
3133
|
+
repo: string;
|
|
3134
|
+
name: string;
|
|
3135
|
+
muted: boolean;
|
|
3136
|
+
}[];
|
|
3137
|
+
export declare function settingsPage(chrome: Chrome, existing: TokenSource | null, hasEnv: boolean, csrf: string, problem: string | null, messaging?: {
|
|
3138
|
+
channel: string | null;
|
|
3139
|
+
implicit: boolean;
|
|
3140
|
+
configured: string[];
|
|
3141
|
+
} | null, push?: {
|
|
3142
|
+
available: boolean;
|
|
3143
|
+
devices: PushSubscription[];
|
|
3144
|
+
} | null, providerKeys?: {
|
|
3145
|
+
provider: string;
|
|
3146
|
+
envName: string;
|
|
3147
|
+
set: boolean;
|
|
3148
|
+
updatedAt: string | null;
|
|
3149
|
+
ambient: boolean;
|
|
3150
|
+
mode: "subscription" | "api-key";
|
|
3151
|
+
subscriptionCapable: boolean;
|
|
3152
|
+
connection?: ProviderConnection;
|
|
3153
|
+
}[] | null, digest?: {
|
|
3154
|
+
everyMs: number | null;
|
|
3155
|
+
lastSentAt: string | null;
|
|
3156
|
+
held: number;
|
|
3157
|
+
} | null, permissionDefault?: {
|
|
3158
|
+
mode: UnattendedPermissionMode;
|
|
3159
|
+
updatedAt: string | null;
|
|
3160
|
+
updatedBy: string | null;
|
|
3161
|
+
canManage: boolean;
|
|
3162
|
+
} | null, qualityDefault?: {
|
|
3163
|
+
mode: QualityMode;
|
|
3164
|
+
updatedAt: string | null;
|
|
3165
|
+
updatedBy: string | null;
|
|
3166
|
+
canManage: boolean;
|
|
3167
|
+
} | null, email?: NonNullable<BrowserSettingsView["email"]> | null, telegramDelivery?: string | null, workers?: NonNullable<BrowserSettingsView["workers"]> | null, updates?: BrowserUpdates | null, firstResult?: string | null, chatNotices?: {
|
|
3168
|
+
mode: "quiet" | "all";
|
|
3169
|
+
digestAt: string | null;
|
|
3170
|
+
screenshots?: ResultScreenshots;
|
|
3171
|
+
projects?: {
|
|
3172
|
+
repo: string;
|
|
3173
|
+
name: string;
|
|
3174
|
+
muted: boolean;
|
|
3175
|
+
}[];
|
|
3176
|
+
} | null, telegramFailing?: boolean, phone?: BrowserPhoneCard | null): Screen;
|
|
3177
|
+
/** The phone setup for the server-rendered Settings page: the same choices as the React card. */
|
|
3178
|
+
export declare function phoneSetupHtml(phone: BrowserPhoneCard): string;
|
|
3179
|
+
/** Every worker that is not retired: its capacity, and the tasks it holds now. */
|
|
3180
|
+
export declare function settingsWorkers(store: Store, now: Date): NonNullable<BrowserSettingsView["workers"]>;
|
|
3181
|
+
/** The page's workers section, for browsers without the app script. */
|
|
3182
|
+
export declare function workersCard(workers: NonNullable<BrowserSettingsView["workers"]> | null): string;
|
|
3183
|
+
/** Settings → Updates, for browsers without the app script. */
|
|
3184
|
+
export declare function updatesCard(updates: BrowserUpdates | null, csrf: string): string;
|
|
3185
|
+
/** Choices save the moment they change; without the script the Save
|
|
3186
|
+
* button stays and the form works the same. */
|
|
3187
|
+
export declare const SETTINGS_AUTOSAVE_SCRIPT = "(function(){document.querySelectorAll('form[data-autosave]').forEach(function(form){form.classList.add('js-autosave');form.addEventListener('change',function(ev){var t=ev.target;if(t&&(t.type==='radio'||t.tagName==='SELECT')){if(form.requestSubmit)form.requestSubmit();else form.submit();}});});})();";
|
|
3188
|
+
/** The newest release, remembered for a while so the page stays quick. */
|
|
3189
|
+
export declare let latestSeen: {
|
|
3190
|
+
at: number;
|
|
3191
|
+
value: {
|
|
3192
|
+
version: string;
|
|
3193
|
+
} | {
|
|
3194
|
+
problem: string;
|
|
3195
|
+
};
|
|
3196
|
+
} | null;
|
|
3197
|
+
export declare function latestReleaseFor(latest: (() => Promise<{
|
|
3198
|
+
version: string;
|
|
3199
|
+
}>) | undefined): Promise<{
|
|
3200
|
+
version: string;
|
|
3201
|
+
} | {
|
|
3202
|
+
problem: string;
|
|
3203
|
+
}>;
|
|
3204
|
+
/** Settings destinations under short headings: an icon and a name each, and a chat app's logo and state. */
|
|
3205
|
+
export declare function settingsTiles(groups: BrowserSettingsGroup[]): string;
|
|
3206
|
+
/** Each destination once, in order: a group's heading, then its [href, label, icon] tiles. */
|
|
3207
|
+
export declare const SETTINGS_GROUPS: [string, [string, string, string][]][];
|
|
3208
|
+
export declare const SETTINGS_TILE_ICONS: Map<string, string>;
|
|
3209
|
+
export declare const CHAT_APP_TILES: Record<string, BrandIconId>;
|
|
3210
|
+
/**
|
|
3211
|
+
* The settings groups, with each chat app's state from what the server already holds: not set up,
|
|
3212
|
+
* connected, or getting alerts (only when that service was chosen, or is the only one).
|
|
3213
|
+
* A Telegram bot whose saved delivery state reports a problem says so instead. With no config folder
|
|
3214
|
+
* the state is unknown, so the tiles carry none.
|
|
3215
|
+
*/
|
|
3216
|
+
export declare function settingsGroups(messaging: {
|
|
3217
|
+
channel: string | null;
|
|
3218
|
+
implicit: boolean;
|
|
3219
|
+
configured: string[];
|
|
3220
|
+
} | null, telegramFailing?: boolean): BrowserSettingsGroup[];
|
|
3221
|
+
/** Light, dark or the device's choice, one tap each. Per browser (a cookie). */
|
|
3222
|
+
export declare function appearanceCard(csrf: string): string;
|
|
3223
|
+
/**
|
|
3224
|
+
* The triage flow: everything waiting on a person, one card at a time.
|
|
3225
|
+
* Full context ON the card, the act inline, and every act lands back here
|
|
3226
|
+
* — clearing the queue is taps, not navigation. Read state travels in the
|
|
3227
|
+
* URL (the bounded skip cursor), never in the session: two tabs cannot
|
|
3228
|
+
* fight, and a shared link shows the same queue.
|
|
3229
|
+
*/
|
|
3230
|
+
export declare function nextPage(chrome: Chrome, data: {
|
|
3231
|
+
item: {
|
|
3232
|
+
key: string;
|
|
3233
|
+
kind: "decision";
|
|
3234
|
+
decision: Decision & {
|
|
3235
|
+
taskId: string;
|
|
3236
|
+
};
|
|
3237
|
+
} | {
|
|
3238
|
+
key: string;
|
|
3239
|
+
kind: "approval";
|
|
3240
|
+
approval: {
|
|
3241
|
+
taskId: string;
|
|
3242
|
+
title: string;
|
|
3243
|
+
goal: string;
|
|
3244
|
+
digest: string;
|
|
3245
|
+
proposedAt: string;
|
|
3246
|
+
};
|
|
3247
|
+
} | {
|
|
3248
|
+
key: string;
|
|
3249
|
+
kind: "requeue";
|
|
3250
|
+
stalled: {
|
|
3251
|
+
taskId: string;
|
|
3252
|
+
title: string;
|
|
3253
|
+
strikes: number;
|
|
3254
|
+
incidentCount: number;
|
|
3255
|
+
};
|
|
3256
|
+
} | {
|
|
3257
|
+
key: string;
|
|
3258
|
+
kind: "gap";
|
|
3259
|
+
gap: Gap;
|
|
3260
|
+
} | null;
|
|
3261
|
+
scope: Scope | null;
|
|
3262
|
+
planDocument: string | null;
|
|
3263
|
+
planContract?: PlanContractView | null;
|
|
3264
|
+
approvalDigest: string | null;
|
|
3265
|
+
raceTerms: TournamentTerms | null;
|
|
3266
|
+
/** v34: said inside the ceremony when the yes buys a report, not a branch. */
|
|
3267
|
+
deliverable?: "branch" | "report";
|
|
3268
|
+
/** v48: the agents the yes freezes — the one block every ceremony shows. */
|
|
3269
|
+
route: RouteView | null;
|
|
3270
|
+
csrf: string;
|
|
3271
|
+
nonce: string;
|
|
3272
|
+
remaining: number;
|
|
3273
|
+
skipped: string[];
|
|
3274
|
+
now: Date;
|
|
3275
|
+
}): Screen;
|
|
3276
|
+
/**
|
|
3277
|
+
* A decision's answer forms — one source of truth for the decision screen
|
|
3278
|
+
* and the triage flow. The consequence reads BEFORE the button that buys
|
|
3279
|
+
* it; irreversible options arm behind one deliberate tap AND the server
|
|
3280
|
+
* independently requires the confirm field. `returnTo` is allow-listed by
|
|
3281
|
+
* the answer handler, never an arbitrary URL.
|
|
3282
|
+
*/
|
|
3283
|
+
export declare function decisionOptionForms(decision: Decision, csrf: string, returnTo: string | null): string;
|
|
3284
|
+
export declare function decisionPage(chrome: Chrome, decision: Decision, taskId: string, artifacts: Artifact[], who: Who, now: Date, returnTo?: string | null): Screen;
|
|
3285
|
+
export declare function taskOf(store: Store, decision: Decision): string;
|
|
3286
|
+
export declare function form(request: IncomingMessage, cap?: number): Promise<URLSearchParams>;
|
|
3287
|
+
/** Attended liveness in words for the task card — reads the durable clock. */
|
|
3288
|
+
export declare function attendedWatchWords(lastBeatAt: string | null, now: Date, absoluteExpiry: string): string;
|
|
3289
|
+
/** One display line, bounded — turn text is data, never layout. */
|
|
3290
|
+
export declare function oneLineOf(text: string, cap: number): string;
|
|
3291
|
+
export type SsoIntent = "sign-in" | "reauth" | "link";
|
|
3292
|
+
/** Live replies (chat streaming): per thread, the turn being answered —
|
|
3293
|
+
* each step's tools in plain words and its text as it is written. Memory
|
|
3294
|
+
* only and display only; the saved turn stays the record. */
|
|
3295
|
+
export type LiveTurn = {
|
|
3296
|
+
steps: MateLiveStep[];
|
|
3297
|
+
done: boolean;
|
|
3298
|
+
ok: boolean;
|
|
3299
|
+
listeners: Set<() => void>;
|
|
3300
|
+
expiry?: NodeJS.Timeout;
|
|
3301
|
+
};
|
|
3302
|
+
export type ChatEnablement = {
|
|
3303
|
+
ok: true;
|
|
3304
|
+
billing: "metered";
|
|
3305
|
+
config: ChatConfig & {
|
|
3306
|
+
provider: DirectChatProviderId;
|
|
3307
|
+
};
|
|
3308
|
+
key: string;
|
|
3309
|
+
keySource: "environment" | "stored";
|
|
3310
|
+
price: import("../converse.js").ModelPrice;
|
|
3311
|
+
credentialKey: string;
|
|
3312
|
+
} | {
|
|
3313
|
+
ok: true;
|
|
3314
|
+
billing: "subscription";
|
|
3315
|
+
config: ChatConfig & {
|
|
3316
|
+
provider: SubscriptionChatProviderId;
|
|
3317
|
+
};
|
|
3318
|
+
key: null;
|
|
3319
|
+
keySource: null;
|
|
3320
|
+
price: null;
|
|
3321
|
+
credentialKey: string;
|
|
3322
|
+
} | {
|
|
3323
|
+
ok: false;
|
|
3324
|
+
code: "demo" | "unscoped" | "roots" | "unresolved" | "empty" | "unconfigured" | "unpriced" | "no-key";
|
|
3325
|
+
why: string;
|
|
3326
|
+
};
|
|
3327
|
+
export type PeekAdmission = {
|
|
3328
|
+
run: Run;
|
|
3329
|
+
/** The run's checkout, PROVEN non-null by the guards: a reviewer run
|
|
3330
|
+
* (v29, artifact-only) is refused before admission ever forms. */
|
|
3331
|
+
worktree: string;
|
|
3332
|
+
epoch: string;
|
|
3333
|
+
entries: ReturnType<typeof parseBaseTreeSnapshot>;
|
|
3334
|
+
};
|