toolroll 0.9.99 → 0.9.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/accent-colors.js +1 -1
  3. package/dist/acp-check.js +4 -2
  4. package/dist/activity-line.d.ts +1 -1
  5. package/dist/activity-line.js +1 -1
  6. package/dist/agent-state.js +2 -1
  7. package/dist/agents-board.js +3 -2
  8. package/dist/agents-live.js +3 -2
  9. package/dist/approval-authority.d.ts +2 -1
  10. package/dist/approval-authority.js +2 -1
  11. package/dist/browser/terminal.css +1 -1
  12. package/dist/browser/terminal.js +42 -42
  13. package/dist/browser/workspace.css +2 -2
  14. package/dist/browser/workspace.js +32 -32
  15. package/dist/browser-workspace.d.ts +31 -2
  16. package/dist/chat-ask.js +4 -2
  17. package/dist/chat-core.js +4 -2
  18. package/dist/chat-decide.js +1 -2
  19. package/dist/chat-delivery-state.d.ts +29 -6
  20. package/dist/chat-delivery-state.js +43 -13
  21. package/dist/chat-delivery.js +12 -5
  22. package/dist/chat-flow.js +15 -13
  23. package/dist/chat-live.js +5 -2
  24. package/dist/chat-message-states.d.ts +75 -0
  25. package/dist/chat-message-states.js +74 -0
  26. package/dist/chat-messages.d.ts +10 -4
  27. package/dist/chat-messages.js +12 -7
  28. package/dist/check-failure.js +2 -1
  29. package/dist/check-report.js +1 -1
  30. package/dist/claim.d.ts +40 -2
  31. package/dist/claim.js +92 -19
  32. package/dist/contracts/chat-tables.js +3 -0
  33. package/dist/coordinator.js +3 -3
  34. package/dist/demo.js +7 -2
  35. package/dist/design/kit.css +94 -2
  36. package/dist/design/kit.d.ts +12 -4
  37. package/dist/design/kit.js +8 -2
  38. package/dist/design/tokens.css +5 -5
  39. package/dist/desktop-update.d.ts +1 -1
  40. package/dist/discord-chat.js +2 -3
  41. package/dist/dispatch.js +2 -1
  42. package/dist/dispose.d.ts +4 -1
  43. package/dist/dispose.js +2 -1
  44. package/dist/fallback.js +3 -2
  45. package/dist/fast-start.js +2 -2
  46. package/dist/first-run.d.ts +10 -6
  47. package/dist/first-run.js +19 -18
  48. package/dist/guides.js +1 -1
  49. package/dist/monitoring.js +2 -1
  50. package/dist/operate.js +77 -36
  51. package/dist/plane-review.js +2 -1
  52. package/dist/project-knowledge.d.ts +3 -3
  53. package/dist/release-authority.d.ts +9 -0
  54. package/dist/release-authority.js +13 -0
  55. package/dist/release-auto.js +2 -3
  56. package/dist/release-gate.js +1 -1
  57. package/dist/release-view.js +2 -1
  58. package/dist/release.js +18 -30
  59. package/dist/result-shots.js +1 -1
  60. package/dist/route-results.js +2 -1
  61. package/dist/run-ending.d.ts +21 -0
  62. package/dist/run-ending.js +19 -0
  63. package/dist/scope.d.ts +5 -0
  64. package/dist/scope.js +10 -3
  65. package/dist/server/chrome.d.ts +1 -1
  66. package/dist/server/chrome.js +1 -1
  67. package/dist/server/console.css +46 -17
  68. package/dist/server/kit.d.ts +36 -3
  69. package/dist/server/kit.js +33 -4
  70. package/dist/server/render-chat.d.ts +2 -0
  71. package/dist/server/render-chat.js +1 -0
  72. package/dist/server/render-pages.js +1 -1
  73. package/dist/server/render-results.js +6 -5
  74. package/dist/server/render-tasks.d.ts +10 -0
  75. package/dist/server/render-tasks.js +234 -64
  76. package/dist/server/task-views.js +5 -1
  77. package/dist/settings-groups-ui.js +1 -1
  78. package/dist/setup-guide.d.ts +2 -2
  79. package/dist/slack-chat.js +2 -3
  80. package/dist/store.d.ts +61 -2
  81. package/dist/store.js +141 -80
  82. package/dist/subagent-question.js +8 -6
  83. package/dist/surface.js +2 -2
  84. package/dist/task-archive.d.ts +3 -0
  85. package/dist/task-archive.js +5 -0
  86. package/dist/task-moves.d.ts +90 -0
  87. package/dist/task-moves.js +68 -0
  88. package/dist/task-status.d.ts +2 -2
  89. package/dist/task-status.js +3 -3
  90. package/dist/teams-chat.js +2 -2
  91. package/dist/telegram-flow.d.ts +1 -1
  92. package/dist/telegram-flow.js +1 -1
  93. package/dist/telegram-lead.d.ts +1 -2
  94. package/dist/telegram-lead.js +18 -23
  95. package/dist/telegram-progress.js +2 -1
  96. package/dist/telegram.d.ts +0 -1
  97. package/dist/telegram.js +6 -7
  98. package/dist/tui.js +1 -1
  99. package/dist/vanished-retry.js +3 -2
  100. package/dist/workspace-ui.d.ts +69 -0
  101. package/dist/workspace-ui.js +105 -1
  102. package/dist/worktree.js +3 -2
  103. package/package.json +1 -1
package/dist/operate.js CHANGED
@@ -123,7 +123,7 @@ import { homedir, hostname } from "node:os";
123
123
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
124
124
  import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
125
125
  import { dur } from "./design/kit.js";
126
- import { checkoutIdle, setCheckoutIdle, leadQuickAnswersOn, setLeadQuickAnswers, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
126
+ import { requeueEligibility, checkoutIdle, setCheckoutIdle, leadQuickAnswersOn, setLeadQuickAnswers, openStore, openStoreNoMigrate, initLocalMode, enrollOwnerPassword, installationCreatedBy, laptopPortOf, recordLaptopPort, localModeOf, localOwnerBinding, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
127
127
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
128
128
  import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
129
129
  import { backgroundOff, backgroundOn, backgroundShow, laptopStatus, laptopStop, restartOwnBuilder } from "./laptop-background.js";
@@ -220,6 +220,7 @@ import { describePublicationGrant, observeChecks, publishPass, sweepMerges, } fr
220
220
  import { checkPublishing, publishingOf, savePublishing, followPullRequests, newestPullRequestOf } from "./pull-request-flow.js";
221
221
  import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
222
222
  import { acquire, acquireIfReady, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, finalize, } from "./claim.js";
223
+ import { moveTask } from "./task-moves.js";
223
224
  import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
224
225
  import { proposeGrant, describeGrant, describeWithheld, permits, MUTATION_CLASSES, DEFAULT_MUTATIONS, } from "./grant.js";
225
226
  import { builtIn, guarded } from "./backend.js";
@@ -296,6 +297,7 @@ import { machineActing } from "./scope.js";
296
297
  import { reevaluateResultRecords } from "./route-results.js";
297
298
  import { holdRuleOff, routeRulesPass, taskEvidenceWords } from "./route-rules.js";
298
299
  import { ruleHoldOf } from "./route-store.js";
300
+ import { wasInterrupted } from "./run-ending.js";
299
301
  import { loginFileOf, migrateLegacyLogin, savedLoginFile, writableLoginFile } from "./login-secrets.js";
300
302
  /**
301
303
  * 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
@@ -1672,12 +1674,9 @@ function claimCommand(positional, flags, context) {
1672
1674
  return fail(write, json, "claim", verdict.reason, verdict.message, EXIT.refused);
1673
1675
  }
1674
1676
  const ref = store.refFor(backend, id);
1675
- const result = acquire(store, ref.id, runner, {
1676
- now,
1677
- token,
1678
- ttlMs: ttl,
1679
- mutation: mutationFrom(flags, now),
1680
- });
1677
+ // The grant and the `running` it makes are one transaction (closed-task-claims.md §3.5): nobody can close the task
1678
+ // between them, and a refusal writes neither.
1679
+ const result = claimAndMarkRunning(store, backend, id, ref.id, runner, { now, token, ttlMs: ttl, mutation: mutationFrom(flags, now) });
1681
1680
  if (!result.ok) {
1682
1681
  // Two distinct refusals: somebody HOLDS it right now, or it is
1683
1682
  // RESERVED for a specific worker whoever asks (queue columns, v19).
@@ -1712,24 +1711,12 @@ function claimCommand(positional, flags, context) {
1712
1711
  return fail(write, json, "claim", "archived", `${id} is put away — restore it with \`toolroll task restore ${id}\` first`, EXIT.refused);
1713
1712
  if (result.reason === "draining")
1714
1713
  return fail(write, json, "claim", "draining", `Toolroll is restarting (draining): ${runner} takes no new work until it is back (by ${result.until})`, EXIT.refused, { until: result.until });
1714
+ if (result.reason === "closed")
1715
+ return fail(write, json, "claim", "closed", closedClaimWords(store, id, result.state, now), EXIT.refused, { state: result.state });
1716
+ if (result.reason === "no-task")
1717
+ return fail(write, json, "claim", "no-task", `no task \`${id}\``, EXIT.refused);
1715
1718
  return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
1716
1719
  }
1717
- // Taking a task is what makes it running; leaving that to the caller would
1718
- // let a claimed task keep showing up as queued to anything reading state.
1719
- // On an idempotent REPLAY the transition runs only as the ONE narrow
1720
- // repair (external dispatch, finding 36): the stored claim is still the
1721
- // live lease and the task never left queued — anything else (cancelled,
1722
- // done, reopened elsewhere) mutates nothing.
1723
- if (result.replayed === true) {
1724
- const task = store.getTask(id);
1725
- const ref = store.lookupRef(id);
1726
- const stillMine = ref !== null && store.currentLiveLease(ref.id, now) === result.claim.leaseId;
1727
- if (task !== null && task.state === "queued" && stillMine)
1728
- store.setTaskState(id, "running", now);
1729
- }
1730
- else {
1731
- store.setTaskState(id, "running", now);
1732
- }
1733
1720
  return succeed(write, json, "claim", { lease: result.claim, reclaimed: result.reclaimed }, () => [
1734
1721
  `Claimed ${id} as ${runner}.`,
1735
1722
  ` lease ${result.claim.leaseId}`,
@@ -1751,20 +1738,18 @@ function leaseCommand(command, positional, flags, context) {
1751
1738
  const ttl = readTtl(flags);
1752
1739
  if (ttl === null)
1753
1740
  return fail(write, json, name, "usage", "--ttl takes whole seconds", EXIT.usage);
1754
- const result = command === "heartbeat" ? heartbeat(store, lease, now, ttl) : release(store, lease, now);
1741
+ // Releasing says the runner is finished with it, not that it succeeded, so a task left running is put back rather
1742
+ // than marked done, in the release's own transaction (releaseToQueue).
1743
+ const result = command === "heartbeat" ? heartbeat(store, lease, now, ttl) : releaseToQueue(store, lease, now);
1755
1744
  if (!result.ok) {
1745
+ if (result.reason === "closed") {
1746
+ return fail(write, json, name, "closed", `this lease ran out and its task is ${result.state} now, so it can't be renewed; stop rather than retry`, EXIT.refused, { state: result.state });
1747
+ }
1756
1748
  const message = result.reason === "fenced"
1757
1749
  ? "superseded — another runner holds this task now; stop rather than retry"
1758
1750
  : "no such lease";
1759
1751
  return fail(write, json, name, result.reason, message, EXIT.refused);
1760
1752
  }
1761
- if (command === "release") {
1762
- const task = store.getTask(String(refExternalId(store, result.claim.taskRef)));
1763
- // Releasing says the runner is finished with it, not that it succeeded, so
1764
- // a task left running is put back rather than marked done.
1765
- if (task !== null && task.state === "running")
1766
- store.setTaskState(task.id, "queued", now);
1767
- }
1768
1753
  return succeed(write, json, name, { lease: result.claim }, () => [
1769
1754
  command === "heartbeat"
1770
1755
  ? `Still yours until ${result.claim.expiresAt}.`
@@ -3512,7 +3497,7 @@ async function tickCommand(flags, context) {
3512
3497
  ? "auth-expired — requeued, sign-in needed"
3513
3498
  : disposition.disposition === "fallback"
3514
3499
  ? `out of plan — moved to the next approved agent`
3515
- : `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : ""}`
3500
+ : `${disposition.failureClass}${disposition.disposition === "backoff" ? ` — retry ${disposition.strikes}/3` : disposition.disposition === "stalled" ? " — stalled" : disposition.closedState !== null ? ` — the task was already ${disposition.closedState}, so it stays ${disposition.closedState}` : ""}`
3516
3501
  : "fenced",
3517
3502
  ...(result.ok ? {} : { detail: result.message.slice(0, 200) }),
3518
3503
  worktree: leased.worktree.path,
@@ -9543,7 +9528,7 @@ function reviewStatusLines(review, sourceRun) {
9543
9528
  queued: "legacy request retained; the worker will close it without running a model review",
9544
9529
  running: `running — attempt ${review.live?.attempt ?? review.attempts.length} of ${review.cap}`,
9545
9530
  succeeded: `succeeded on attempt ${review.succeeded?.attempt ?? 1} of ${review.cap}`,
9546
- retryable: `${review.latest?.outcome === "interrupted" || review.latest?.reason === "interrupted" ? "interrupted" : "failed"} on attempt ${review.latest?.attempt ?? review.attempts.length} of ${review.cap} — inspect the saved result; model reviews no longer retry`,
9531
+ retryable: `${review.latest !== null && wasInterrupted(review.latest) ? "interrupted" : "failed"} on attempt ${review.latest?.attempt ?? review.attempts.length} of ${review.cap} — inspect the saved result; model reviews no longer retry`,
9547
9532
  exhausted: `exhausted — all ${review.cap} attempts ended without a review; nothing retries a fourth time`,
9548
9533
  };
9549
9534
  const first = review.state === "queued" && review.attempts.length === 0 ? "queued — waiting for a worker" : word[review.state];
@@ -10546,6 +10531,8 @@ function describeApproveFailure(reason, id) {
10546
10531
  return "only a person can approve this — approve it on the task page or from your paired chat";
10547
10532
  if (reason === "archived")
10548
10533
  return `${id} is put away — restore it with \`toolroll task restore ${id}\` first`;
10534
+ if (reason === "cancelled")
10535
+ return `${id} was cancelled, so there is nothing to approve`;
10549
10536
  if (reason === "second-approver")
10550
10537
  return `your approval of ${id} is recorded; it is protected work, so a second person needs to approve it (toolroll task approve ${id} as them)`;
10551
10538
  if (reason === "no-approvers") {
@@ -12066,9 +12053,6 @@ function slug(title, now) {
12066
12053
  const stamp = now.toISOString().slice(11, 19).replace(/:/g, "");
12067
12054
  return words === "" ? `task-${stamp}` : `${words}-${stamp}`;
12068
12055
  }
12069
- function refExternalId(store, taskRef) {
12070
- return store.externalIdFor(taskRef);
12071
- }
12072
12056
  function describe(error) {
12073
12057
  return error instanceof Error ? error.message : String(error);
12074
12058
  }
@@ -14299,3 +14283,60 @@ function startServiceBeats(store) {
14299
14283
  void delivery.runNow();
14300
14284
  return beats;
14301
14285
  }
14286
+ /**
14287
+ * The CLI `claim`'s grant and its `running`, one transaction (docs/plans/closed-task-claims.md §3.5). A fresh grant of
14288
+ * a built-in task moves it to running through the narrow `claim` move; a replay of the same key repairs `running` only
14289
+ * when the task is still queued and the replayed lease is still the live one, as before. A task of another backend is
14290
+ * never written in the built-in table.
14291
+ */
14292
+ function claimAndMarkRunning(store, backend, id, taskRef, runner, options) {
14293
+ const { now } = options;
14294
+ return store.transact(() => {
14295
+ const result = acquire(store, taskRef, runner, options);
14296
+ if (!result.ok || backend !== BUILT_IN)
14297
+ return result;
14298
+ if (result.replayed === true) {
14299
+ const task = store.getTask(id);
14300
+ const stillMine = store.currentLiveLease(taskRef, now) === result.claim.leaseId;
14301
+ if (task !== null && task.state === "queued" && stillMine)
14302
+ store.setTaskState(id, "running", now);
14303
+ return result;
14304
+ }
14305
+ // The grant just read this row open in this transaction, so the move cannot miss; if it ever did, throwing rolls
14306
+ // the grant back with it.
14307
+ if (!moveTask(store.handle, id, "claim", now))
14308
+ throw new Error(`claim: ${id} could not be marked running`);
14309
+ store.bumpWake();
14310
+ return result;
14311
+ });
14312
+ }
14313
+ /**
14314
+ * The refusal words for a claim on a closed task: what state it is in, and only a next step that works for this task
14315
+ * (a requeue the store would accept, a cancelled task reopened through the state verb, otherwise the task itself).
14316
+ */
14317
+ function closedClaimWords(store, id, state, now) {
14318
+ const head = `${id} is ${state}, so it can't be claimed.`;
14319
+ if (state === "cancelled")
14320
+ return `${head} To run it again, reopen it: \`toolroll task state ${id} queued\``;
14321
+ if (requeueEligibility(store, id, now).ok)
14322
+ return `${head} To run it again: \`toolroll task requeue ${id}\``;
14323
+ return `${head} See its result: \`toolroll task show ${id}\``;
14324
+ }
14325
+ /**
14326
+ * `worker release`: hand the lease back and, when this release is the one that ended the claim, put a task left
14327
+ * running back in the queue, in one transaction, so no one can claim it in between (closed-task-claims.md §5). A
14328
+ * duplicate release changes nothing: by then another runner may hold the task.
14329
+ */
14330
+ function releaseToQueue(store, lease, now) {
14331
+ return store.transact(() => {
14332
+ const result = release(store, lease, now);
14333
+ if (!result.ok || result.duplicate === true)
14334
+ return result;
14335
+ const ref = store.refForId(result.claim.taskRef);
14336
+ if (ref === null || ref.backend !== BUILT_IN)
14337
+ return result;
14338
+ if (store.getTask(ref.externalId)?.state === "running")
14339
+ store.setTaskState(ref.externalId, "queued", now);
14340
+ return result;
14341
+ });
14342
+ }
@@ -30,6 +30,7 @@
30
30
  import { scanForSecrets } from "./evidence.js";
31
31
  import { scrubIntegrationText } from "./integrations.js";
32
32
  import { taskWaitSnapshot } from "./lead-status.js";
33
+ import { wasInterrupted } from "./run-ending.js";
33
34
  export const REVIEW_HOURS = 24;
34
35
  const EVIDENCE_LINES = 5;
35
36
  const WAITING_DAYS = 1;
@@ -73,7 +74,7 @@ export function runCause(run) {
73
74
  return "timeout";
74
75
  if (reason === "no-handoff")
75
76
  return "no-handoff";
76
- if (reason === "interrupted" || run.outcome === "interrupted")
77
+ if (wasInterrupted(run))
77
78
  return run.stopped ? null : "orphaned";
78
79
  if (PROVIDER_REASONS.has(reason) || run.terminalClass === "transient-throttle")
79
80
  return "provider";
@@ -22,18 +22,17 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
22
22
  export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
23
23
  /** Chat reads only an admitted project. Keep the full source in the project UI. */
24
24
  export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
25
- content?: never;
26
25
  history?: never;
27
26
  revision: number;
28
27
  stale: boolean;
29
28
  notice: string;
30
29
  title?: never;
30
+ content?: never;
31
31
  instructions?: never;
32
32
  references?: never;
33
33
  decisions?: never;
34
34
  waiting?: never;
35
35
  } | {
36
- content?: never;
37
36
  revision?: never;
38
37
  history?: never;
39
38
  stale?: never;
@@ -54,6 +53,7 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
54
53
  afterwards?: string;
55
54
  notice: string;
56
55
  title?: never;
56
+ content?: never;
57
57
  instructions?: never;
58
58
  references?: never;
59
59
  decisions?: never;
@@ -70,9 +70,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
70
70
  decisions?: never;
71
71
  waiting?: never;
72
72
  } | {
73
- content?: never;
74
73
  stale?: never;
75
74
  title?: never;
75
+ content?: never;
76
76
  revision: number;
77
77
  history: {
78
78
  revision: number;
@@ -7,6 +7,15 @@ import { type ReleasePolicy, type ReleaseTerms } from "./contracts/release-terms
7
7
  import type { Store } from "./store.js";
8
8
  export type ReleaseState = "open" | "deploying" | "merge-reserved" | "merged" | "revoked" | "expired" | "superseded";
9
9
  export declare const LIVE_RELEASE_STATES: readonly ReleaseState[];
10
+ /**
11
+ * Whether an authority's approval has run out at `now`. Nothing writes the `expired` state: expiry is worked out from
12
+ * `expires_at` here, and every reader that asks whether an authority is still good asks through this (or
13
+ * releaseStateAt). The one SQL reader, approval-authority.ts releaseStandsSql, states the same rule in SQL.
14
+ */
15
+ export declare function releaseExpiredAt(a: Pick<ReleaseAuthority, "state" | "expiresAt">, now: Date): boolean;
16
+ /** An authority's state as read at `now`: a live one (open, deploying, merge-reserved) past its time reads as `expired`;
17
+ * an ended one keeps the state it ended in. */
18
+ export declare function releaseStateAt(a: Pick<ReleaseAuthority, "state" | "expiresAt">, now: Date): ReleaseState;
10
19
  /** One generation of a release a person approved: the single source of truth for it. */
11
20
  export type ReleaseAuthority = {
12
21
  id: number;
@@ -5,6 +5,19 @@
5
5
  */
6
6
  import { readReleasePolicy } from "./contracts/release-terms.js";
7
7
  export const LIVE_RELEASE_STATES = ["open", "deploying", "merge-reserved"];
8
+ /**
9
+ * Whether an authority's approval has run out at `now`. Nothing writes the `expired` state: expiry is worked out from
10
+ * `expires_at` here, and every reader that asks whether an authority is still good asks through this (or
11
+ * releaseStateAt). The one SQL reader, approval-authority.ts releaseStandsSql, states the same rule in SQL.
12
+ */
13
+ export function releaseExpiredAt(a, now) {
14
+ return a.state === "expired" || a.expiresAt <= now.toISOString();
15
+ }
16
+ /** An authority's state as read at `now`: a live one (open, deploying, merge-reserved) past its time reads as `expired`;
17
+ * an ended one keeps the state it ended in. */
18
+ export function releaseStateAt(a, now) {
19
+ return LIVE_RELEASE_STATES.includes(a.state) && releaseExpiredAt(a, now) ? "expired" : a.state;
20
+ }
8
21
  function readReleaseAuthority(row) {
9
22
  const text = (key) => row[key] == null ? null : String(row[key]);
10
23
  let policy = null;
@@ -26,7 +26,7 @@ import { releaseCheckStanding } from "./check-failure.js";
26
26
  import { CARRY_RULES, rulePath } from "./release-carry.js";
27
27
  import { AUTO_EVIDENCE_ACTION, AUTO_WITHDRAWN_ACTION, LIVE_RELEASE_STATES, RELEASE_APPROVAL_ACTION, releaseAuthorityOfTask, releaseAutoApproves, releaseApprovalSetting } from "./release-authority.js";
28
28
  import { autoReleaseApprovalOf, personApprovalOf } from "./scope.js";
29
- import { releaseOperations, revokeRelease } from "./store.js";
29
+ import { releaseOperations, revokeRelease, withdrawApproval } from "./store.js";
30
30
  import { RELEASE_CI_HELD_ACTION, RELEASE_CI_PASSED_ACTION, RELEASE_CI_RUNNING_WORDS, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
31
31
  import { verificationEvidence } from "./verification-evidence.js";
32
32
  import { nightlyDebtHold } from "./nightly-github.js";
@@ -87,8 +87,7 @@ export function setReleaseApproval(store, repo, auto, by, now) {
87
87
  * marked withdrawn, so no later reading — the setting turned on again included — lets it stand. */
88
88
  function unseal(store, taskId, now) {
89
89
  withdraw(store, taskId, now);
90
- store.handle.prepare(`UPDATE task_scope SET approved_at = NULL, approved_by = NULL, approved_digest = NULL, approved_profile_json = NULL,
91
- approved_route_json = NULL, approval_basis = NULL, mode_digest = NULL WHERE task_id = ? AND approval_basis IN ('auto-check', 'auto-green')`).run(taskId);
90
+ withdrawApproval(store, { where: "WHERE task_id = ? AND approval_basis IN ('auto-check', 'auto-green')", args: [taskId] }, { basisAfter: null, clearRoute: true, clearModeDigest: true });
92
91
  }
93
92
  /** Mark a task's automatic seal's evidence withdrawn for good (its seal row may stay while an issued merge reconciles). */
94
93
  function withdraw(store, taskId, now) {
@@ -27,7 +27,7 @@ import { BEFORE_SWAP, RELEASED } from "./deploy-phase.js";
27
27
  /** The journal phases a deployment passes before it stops the old service (deploy-phase.ts, the one table). */
28
28
  export { BEFORE_SWAP };
29
29
  /** Journal fields that only the swap and later phases write. */
30
- const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt"];
30
+ const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt", "launchd", "newServiceStop"];
31
31
  const GATE_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
32
32
  const refuse = (reason, message) => ({ ok: false, reason, message: `${message} The update pause was left as it is.` });
33
33
  /** The owner id written into the live pause's triggers, when there is one pause and it names one id. */
@@ -6,6 +6,7 @@
6
6
  import { liveReleaseOf } from "./scope.js";
7
7
  import { nightlyReleaseLines } from "./nightly-github.js";
8
8
  import { releaseAuthorityOfTask, releaseCarries } from "./store.js";
9
+ import { releaseStateAt } from "./release-authority.js";
9
10
  export function releaseViewOf(store, taskId, now, obligations = () => []) {
10
11
  const found = releaseAuthorityOfTask(store, taskId);
11
12
  if (found === null)
@@ -18,7 +19,7 @@ export function releaseViewOf(store, taskId, now, obligations = () => []) {
18
19
  return {
19
20
  kind: found.carry === null ? "anchor" : "carried",
20
21
  authority: a.id, version: a.version, pr: a.pr, state: a.state,
21
- carrying: a.state === "open" && live.applies && live.ok, why: live.applies && !live.ok ? live.why : null,
22
+ carrying: releaseStateAt(a, now) === "open" && live.applies && live.ok, why: live.applies && !live.ok ? live.why : null,
22
23
  approver: anchor?.approvedBy ?? null, anchorTask: a.anchorTask, tipTask: a.tipTask, expiresAt: a.expiresAt,
23
24
  landed: releaseCarries(store, a.id).flatMap(carry => carry.commits.map(commit => ({ sha: commit.sha, task: carry.taskId, files: commit.files }))),
24
25
  obligations: obligations(a.id),
package/dist/release.js CHANGED
@@ -667,6 +667,17 @@ export async function runRelease(options, adapters) {
667
667
  return head !== null && head !== j.candidate.sha ? head : null;
668
668
  });
669
669
  };
670
+ /**
671
+ * Move the release back to an earlier step: write the fields the caller names (and only those), drop the named steps'
672
+ * intents, set the step, and save the journal. Every step-back goes through here.
673
+ */
674
+ const rewindTo = (step, fields, dropIntents = []) => {
675
+ Object.assign(j, fields);
676
+ for (const one of dropIntents)
677
+ delete j.intents[one];
678
+ j.step = step;
679
+ save();
680
+ };
670
681
  const restart = async (head, updated = false) => {
671
682
  // The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
672
683
  mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
@@ -678,16 +689,10 @@ export async function runRelease(options, adapters) {
678
689
  const carried = await rel.carry(j.authority.id, head, randomUUID(), epoch);
679
690
  if (carried.ok) {
680
691
  const view = await rel.authority(carried.taskId);
692
+ // The candidate is written before the gate is read, as it always was: a gate that fails leaves it moved.
681
693
  j.candidate = { sha: head, tree };
682
- j.task = { id: carried.taskId, scopeDigest: (await adapters.toolroll.gate(carried.taskId)).scopeDigest };
683
- j.check = null;
684
- j.completion = null;
685
- j.landed = view?.landed ?? j.landed ?? [];
686
- delete j.intents.check;
687
- delete j.intents.complete;
688
- j.step = "check";
689
- j.stopped = null;
690
- save();
694
+ const task = { id: carried.taskId, scopeDigest: (await adapters.toolroll.gate(carried.taskId)).scopeDigest };
695
+ rewindTo("check", { task, check: null, completion: null, landed: view?.landed ?? j.landed ?? [], stopped: null }, ["check", "complete"]);
691
696
  say(` carried the approval to ${head.slice(0, 7)} (task ${carried.taskId})`);
692
697
  return;
693
698
  }
@@ -695,30 +700,16 @@ export async function runRelease(options, adapters) {
695
700
  }
696
701
  else
697
702
  say(` ${j.branch} moved to ${head.slice(0, 7)}: filing it for approval`);
698
- j.candidate = { sha: head, tree };
699
- j.task = { id: gateTaskId(j.packageVersion, head), scopeDigest: null };
700
- j.approval = null;
701
- j.check = null;
702
- j.completion = null;
703
- j.authority = null;
704
- j.auto = null;
705
703
  epoch = null;
706
- delete j.intents.gate;
707
- delete j.intents.check;
708
- delete j.intents.complete;
709
- j.step = "gate";
710
- j.stopped = null;
711
- save();
704
+ rewindTo("gate", { candidate: { sha: head, tree }, task: { id: gateTaskId(j.packageVersion, head), scopeDigest: null },
705
+ approval: null, check: null, completion: null, authority: null, auto: null, stopped: null }, ["gate", "check", "complete"]);
712
706
  };
713
707
  /**
714
708
  * The automatic road ended without an approval: the plane takes its `auto-check` back (the task asks a person, its
715
709
  * passing check kept) and tells the owner; the release waits at approval for a person, as without the setting.
716
710
  */
717
711
  const autoRefused = async (why) => {
718
- j.auto = { state: "refused", why };
719
- j.check = null;
720
- j.step = "approval";
721
- save();
712
+ rewindTo("approval", { auto: { state: "refused", why }, check: null });
722
713
  await rel?.auto?.giveUp(j.task.id, why);
723
714
  throw new Stop("approval", `Not approved automatically: ${why}. A person approves task ${j.task.id} (\`toolroll task approve ${j.task.id} --digest ${j.task.scopeDigest}\`, or the console), then rerun \`${resume}\`.`);
724
715
  };
@@ -1053,10 +1044,7 @@ export async function runRelease(options, adapters) {
1053
1044
  }
1054
1045
  catch (error) {
1055
1046
  if (error instanceof Stop && error.reason === "ci-failed") {
1056
- j.auto = { state: "refused", why: error.message };
1057
- j.check = null;
1058
- j.step = "approval";
1059
- save();
1047
+ rewindTo("approval", { auto: { state: "refused", why: error.message }, check: null });
1060
1048
  await rel?.auto?.giveUp(j.task.id, error.message.replace(/\.$/, ""));
1061
1049
  }
1062
1050
  throw error;
@@ -169,6 +169,6 @@ export function refuseResultShots(state, store, part, content, app, now) {
169
169
  const title = chatTitle(store, content.image?.taskId ?? content.task ?? "");
170
170
  const line = { text: `Screenshots for ${title} weren't sent: the ${app} app isn't allowed to upload files here.`,
171
171
  ...(content.task === undefined ? {} : { task: content.task }), ...(content.run === undefined ? {} : { run: content.run }) };
172
- state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=?").run(savedChatPart(line), now.toISOString(), part.id);
172
+ state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=? AND state<>'dropped'").run(savedChatPart(line), now.toISOString(), part.id);
173
173
  state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE provider=:provider AND event=? AND id<>? AND state='pending'").run(part.event, part.id);
174
174
  }
@@ -14,6 +14,7 @@ import { INELIGIBLE_REASONS, REASON_WORDS, selectionSchema, } from "./contracts/
14
14
  import { changedFilesOf, COMPLETION_ACTION } from "./result-completion.js";
15
15
  import { runAccounting } from "./store.js";
16
16
  import { INFRASTRUCTURE_TERMINALS, saveResultRecord } from "./route-store.js";
17
+ import { wasInterrupted } from "./run-ending.js";
17
18
  // The execution class and the cohort snapshot live with their writer (route-store.ts), which store.ts imports: kept
18
19
  // light so the store's module graph doesn't reach the evaluator's readers.
19
20
  export { cohortOf, EXEC_CLASS_OF_REASON, execClassOf } from "./route-store.js";
@@ -49,7 +50,7 @@ function classOfRun(run, kept) {
49
50
  return kept;
50
51
  if (run.terminalClass != null && INFRASTRUCTURE_TERMINALS.has(run.terminalClass))
51
52
  return "infrastructure";
52
- if ((run.outcome === "interrupted" || run.reason === "interrupted") && run.providerStartedAt === null)
53
+ if (wasInterrupted(run) && run.providerStartedAt === null)
53
54
  return "infrastructure";
54
55
  return "model";
55
56
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * How a run ended, asked one way.
3
+ *
4
+ * "Interrupted" is stored in two spellings. Every ending written today is the outcome `failed` with the reason
5
+ * `interrupted`. Rows written between schema 25 and 114 carry the outcome `interrupted` itself (a held session cut
6
+ * down mid-flight) and still read. A check that knows only one spelling treats the same event two ways, so every check
7
+ * asks here.
8
+ */
9
+ export declare function wasInterrupted(run: {
10
+ outcome: string | null;
11
+ reason?: string | null;
12
+ }): boolean;
13
+ /**
14
+ * The spelling written today, and only it. The roads that hand an interrupted attempt's draft to the next attempt
15
+ * (salvage in the checkout, the recovery grant, the check a run never reached) ask this one: they were built for the
16
+ * runs that write it, and reading the old spelling there would widen what they recover.
17
+ */
18
+ export declare function endedInterrupted(run: {
19
+ outcome: unknown;
20
+ reason?: unknown;
21
+ }): boolean;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * How a run ended, asked one way.
3
+ *
4
+ * "Interrupted" is stored in two spellings. Every ending written today is the outcome `failed` with the reason
5
+ * `interrupted`. Rows written between schema 25 and 114 carry the outcome `interrupted` itself (a held session cut
6
+ * down mid-flight) and still read. A check that knows only one spelling treats the same event two ways, so every check
7
+ * asks here.
8
+ */
9
+ export function wasInterrupted(run) {
10
+ return run.outcome === "interrupted" || (run.outcome === "failed" && run.reason === "interrupted");
11
+ }
12
+ /**
13
+ * The spelling written today, and only it. The roads that hand an interrupted attempt's draft to the next attempt
14
+ * (salvage in the checkout, the recovery grant, the check a run never reached) ask this one: they were built for the
15
+ * runs that write it, and reading the old spelling there would widen what they recover.
16
+ */
17
+ export function endedInterrupted(run) {
18
+ return run.outcome === "failed" && run.reason === "interrupted";
19
+ }
package/dist/scope.d.ts CHANGED
@@ -605,6 +605,11 @@ export type ApproveResult = {
605
605
  | {
606
606
  ok: false;
607
607
  reason: "archived";
608
+ }
609
+ /** The task was cancelled: a cancelled task takes no approval, by any road (a stale page, the CLI). */
610
+ | {
611
+ ok: false;
612
+ reason: "cancelled";
608
613
  };
609
614
  /**
610
615
  * Whether this name-and-token pair is a person the store knows. Shared by
package/dist/scope.js CHANGED
@@ -52,7 +52,7 @@ import { z } from "zod";
52
52
  import {} from "./contracts/acceptance-terms.js";
53
53
  import { parseContract } from "./contracts/contract.js";
54
54
  import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
55
- import { taskArchived } from "./task-archive.js";
55
+ import { taskArchived, taskCancelled } from "./task-archive.js";
56
56
  import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
57
57
  import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
58
58
  /** Same shape as a runner's credential, for the same reasons. */
@@ -1120,6 +1120,10 @@ chat) {
1120
1120
  // A put-away task (v145) is restored first; sealScopeApproval refuses it too, on every road.
1121
1121
  if (taskArchived(store, taskId))
1122
1122
  return { ok: false, reason: "archived" };
1123
+ // A cancelled task takes no yes either — read here, in the seal's transaction, before any vote or proof is
1124
+ // written: an approval page left open while the task was cancelled elsewhere, or the CLI, approves nothing.
1125
+ if (taskCancelled(store, taskId))
1126
+ return { ok: false, reason: "cancelled" };
1123
1127
  // The digest is required, not optional. An operator reads a scope, somebody
1124
1128
  // rewrites it, and an approval that did not name what it saw would agree to
1125
1129
  // the new one in silence — which is the whole failure this guards.
@@ -1443,7 +1447,7 @@ import { permissionPlainWords } from "./chat-terms.js";
1443
1447
  import { limitRecognised } from "./exhaustion.js";
1444
1448
  import { SUBSCRIPTION_CAPABLE } from "./keys.js";
1445
1449
  import { releaseCheckPurposeDigest, releaseTermsDigest } from "./contracts/release-terms.js";
1446
- import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem } from "./release-authority.js";
1450
+ import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem, releaseStateAt } from "./release-authority.js";
1447
1451
  /** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
1448
1452
  export const machineActing = () => currentActor()?.lead === true || activeRemote() !== null;
1449
1453
  /** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
@@ -1550,6 +1554,9 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
1550
1554
  // A put-away task (v145, task-tidy.md), or a put-away anchor, deploys, merges and carries nothing: restore it first.
1551
1555
  if (taskArchived(store, taskId) || taskArchived(store, a.anchorTask))
1552
1556
  return refuse("the release task is put away");
1557
+ // A cancelled task, or a cancelled anchor, the same: cancelling ends what its approval allows.
1558
+ if (taskCancelled(store, taskId) || taskCancelled(store, a.anchorTask))
1559
+ return refuse("the release task was cancelled");
1553
1560
  const newest = newestReleaseGeneration(store, a.repo, a.version, a.pr);
1554
1561
  if (newest === null || newest.id !== a.id)
1555
1562
  return refuse("a newer approval of this release replaced this one");
@@ -1558,7 +1565,7 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
1558
1565
  if (a.state === "revoked")
1559
1566
  return refuse(`${a.endedBy ?? "an approver"} stopped carrying commits for this release`);
1560
1567
  // Expiry stops every live state (security review): an operation already reserved finishes only through reconciliation.
1561
- if (a.state === "expired" || ((a.state === "open" || a.state === "deploying" || a.state === "merge-reserved") && a.expiresAt <= now.toISOString()))
1568
+ if (releaseStateAt(a, now) === "expired")
1562
1569
  return refuse("the release approval expired");
1563
1570
  if (a.state === "merged")
1564
1571
  return refuse("the release was already merged");
@@ -56,7 +56,7 @@ export declare const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a
56
56
  * a person's accent (accentHead) still wins over the generated default. */
57
57
  export declare const STYLE: string;
58
58
  /** Appearance: a three-way segmented switch, one tap per choice. */
59
- 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}";
59
+ 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-neutral-soft);color:var(--so-ink)}.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}";
60
60
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
61
61
  export declare const PAGE_CSS: string;
62
62
  /** The Inbox tabs (console v2): a segmented track of real links; the dot marks a tab with something new, on phones only. */
@@ -150,7 +150,7 @@ export const STYLE = readFileSync(join(import.meta.dirname, "..", "design", "tok
150
150
  // server pages (server/kit.ts) and React views (browser/kit/) looks the same everywhere.
151
151
  readFileSync(join(import.meta.dirname, "..", "design", "kit.css"), "utf8");
152
152
  /** Appearance: a three-way segmented switch, one tap per choice. */
153
- export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
153
+ export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-neutral-soft);color:var(--so-ink)}.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}`;
154
154
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
155
155
  export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
156
156
  /** The Inbox tabs (console v2): a segmented track of real links; the dot marks a tab with something new, on phones only. */