toolroll 0.9.91 → 0.9.93

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 (124) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/approval-authority.d.ts +9 -1
  3. package/dist/approval-authority.js +15 -2
  4. package/dist/assignment-status.d.ts +1 -1
  5. package/dist/assignment-ui.js +2 -2
  6. package/dist/assignment.js +11 -3
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/browser/workspace.js +30 -30
  9. package/dist/browser-crew.js +2 -2
  10. package/dist/browser-workspace.d.ts +3 -5
  11. package/dist/build-review.js +2 -0
  12. package/dist/builder.js +21 -2
  13. package/dist/chat-actions.d.ts +11 -3
  14. package/dist/chat-actions.js +47 -2
  15. package/dist/chat-decide.js +2 -0
  16. package/dist/chat-review.d.ts +2 -1
  17. package/dist/chat-review.js +2 -2
  18. package/dist/check-facts.d.ts +124 -0
  19. package/dist/check-facts.js +272 -0
  20. package/dist/check-report.d.ts +5 -0
  21. package/dist/check-report.js +4 -3
  22. package/dist/cli.js +5 -0
  23. package/dist/contracts/chat-actions.d.ts +10 -0
  24. package/dist/contracts/chat-actions.js +4 -0
  25. package/dist/contracts/console-api.d.ts +3 -2
  26. package/dist/contracts/console-api.js +5 -2
  27. package/dist/contracts/release-check.d.ts +25 -1
  28. package/dist/contracts/release-check.js +19 -0
  29. package/dist/contracts/route-results.d.ts +121 -0
  30. package/dist/contracts/route-results.js +71 -0
  31. package/dist/contracts/route.d.ts +343 -5
  32. package/dist/contracts/route.js +36 -7
  33. package/dist/contracts/store-json.d.ts +2 -0
  34. package/dist/desktop-update.d.ts +2 -2
  35. package/dist/desktop-update.js +13 -9
  36. package/dist/dispose.js +6 -0
  37. package/dist/eval-hooks.d.ts +55 -0
  38. package/dist/eval-hooks.js +227 -4
  39. package/dist/flaky.d.ts +17 -0
  40. package/dist/flaky.js +9 -0
  41. package/dist/flow-engine.js +1 -1
  42. package/dist/flow-pull-request.js +1 -1
  43. package/dist/lead-doors.js +4 -2
  44. package/dist/lead-tools.js +10 -2
  45. package/dist/local-owner.d.ts +1 -1
  46. package/dist/local-owner.js +11 -0
  47. package/dist/memory-cli.d.ts +3 -0
  48. package/dist/memory-cli.js +3 -3
  49. package/dist/memory-pass.js +5 -1
  50. package/dist/model-choices.d.ts +7 -3
  51. package/dist/model-choices.js +26 -11
  52. package/dist/models-cli.d.ts +205 -0
  53. package/dist/models-cli.js +143 -4
  54. package/dist/models-ui.d.ts +57 -0
  55. package/dist/models-ui.js +44 -1
  56. package/dist/needs-you.d.ts +7 -7
  57. package/dist/needs-you.js +12 -10
  58. package/dist/operate-remote-arguments.js +4 -4
  59. package/dist/operate-remote.js +1 -1
  60. package/dist/operate.js +191 -23
  61. package/dist/phase-routing.d.ts +82 -2
  62. package/dist/phase-routing.js +132 -18
  63. package/dist/project-delete.d.ts +4 -0
  64. package/dist/project-delete.js +16 -4
  65. package/dist/project-memory.d.ts +8 -0
  66. package/dist/project-memory.js +20 -1
  67. package/dist/pull-request-flow.js +1 -1
  68. package/dist/push.d.ts +10 -3
  69. package/dist/push.js +22 -6
  70. package/dist/release-adapters.js +11 -1
  71. package/dist/release-approval.d.ts +191 -0
  72. package/dist/release-approval.js +382 -0
  73. package/dist/release-auto.js +22 -4
  74. package/dist/release-card.d.ts +73 -4
  75. package/dist/release-card.js +168 -23
  76. package/dist/release-cli.js +9 -1
  77. package/dist/release.d.ts +30 -0
  78. package/dist/release.js +42 -3
  79. package/dist/result-actions.d.ts +4 -0
  80. package/dist/result-actions.js +3 -0
  81. package/dist/result-acts.d.ts +3 -3
  82. package/dist/result-acts.js +1 -1
  83. package/dist/route-hooks.d.ts +54 -0
  84. package/dist/route-hooks.js +179 -0
  85. package/dist/route-results.d.ts +70 -0
  86. package/dist/route-results.js +340 -0
  87. package/dist/route-rules.d.ts +324 -0
  88. package/dist/route-rules.js +781 -0
  89. package/dist/route-savings.d.ts +136 -0
  90. package/dist/route-savings.js +193 -0
  91. package/dist/route-store.d.ts +300 -0
  92. package/dist/route-store.js +598 -0
  93. package/dist/scope.d.ts +3 -1
  94. package/dist/scope.js +4 -1
  95. package/dist/server/guards.js +1 -0
  96. package/dist/server/pages-assets.js +1 -1
  97. package/dist/server/render-chat.js +2 -2
  98. package/dist/server/render-release.d.ts +17 -0
  99. package/dist/server/render-release.js +84 -0
  100. package/dist/server/render-results.d.ts +0 -2
  101. package/dist/server/render-results.js +4 -6
  102. package/dist/server/render-tasks.d.ts +7 -0
  103. package/dist/server/render-tasks.js +16 -3
  104. package/dist/server/route-table.d.ts +1 -1
  105. package/dist/server/route-table.js +19 -1
  106. package/dist/server/settings.d.ts +2 -1
  107. package/dist/server/settings.js +87 -1
  108. package/dist/server/task-views.js +6 -0
  109. package/dist/server/tasks.d.ts +15 -0
  110. package/dist/server/tasks.js +105 -20
  111. package/dist/settings-proofs.js +2 -0
  112. package/dist/spend-ui.d.ts +11 -0
  113. package/dist/spend-ui.js +49 -2
  114. package/dist/stop-proof.d.ts +67 -0
  115. package/dist/stop-proof.js +293 -0
  116. package/dist/store.d.ts +106 -31
  117. package/dist/store.js +653 -128
  118. package/dist/surface.js +17 -5
  119. package/dist/task-outcome-cli.js +24 -2
  120. package/dist/toolroll-update-ui.js +2 -2
  121. package/dist/toolroll-update.js +4 -2
  122. package/dist/work-index.js +7 -9
  123. package/dist/work-summary.d.ts +2 -2
  124. package/package.json +1 -1
@@ -6,11 +6,11 @@
6
6
  * The words never name the machine's internals; `plainSentence` keeps a
7
7
  * recorded reason only when it is free of them. */
8
8
  /** What a person is asked for. */
9
- export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "confirm-stopped" | "check-stopped" | "hold" | "choose-project" | "define-task" | "fix-request" | "choose-agent" | "fix-dependency" | "add-requirement" | "connect-builder" | "start-builder" | "vanished" | "review-result" | "rebuild" | "replan" | "earlier-version" | "other";
9
+ export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "stop-unproven" | "hold" | "choose-project" | "define-task" | "fix-request" | "choose-agent" | "fix-dependency" | "add-requirement" | "connect-builder" | "start-builder" | "vanished" | "review-result" | "rebuild" | "replan" | "earlier-version" | "other";
10
10
  /** What the work waits for when no person can act. */
11
- export type WaitKey = "build-stopping" | "build-finishing" | "other-computer" | "card-reply" | "card-time" | "card-ci";
11
+ export type WaitKey = "build-stopping" | "build-finishing" | "build-checking" | "other-computer" | "card-reply" | "card-time" | "card-ci";
12
12
  /** The action's code: the WorkAction that owns the act, or one of the acts a surface renders itself. */
13
- export type NeedActionCode = "approve-scope" | "approve-card" | "answer-decision" | "inspect-decisions" | "sign-in" | "confirm-stopped" | "inspect-hold" | "place-task" | "write-scope" | "select-agent" | "repair-dependency" | "repair-capability" | "start-worker" | "reconcile-run" | "open-result" | "retry-task" | "inspect-task";
13
+ export type NeedActionCode = "approve-scope" | "approve-card" | "answer-decision" | "inspect-decisions" | "sign-in" | "recheck-stopped" | "inspect-hold" | "place-task" | "write-scope" | "select-agent" | "repair-dependency" | "repair-capability" | "start-worker" | "reconcile-run" | "open-result" | "retry-task" | "inspect-task";
14
14
  export type NeedAction = {
15
15
  code: NeedActionCode;
16
16
  label: string;
@@ -76,14 +76,14 @@ export declare const hasInternalWords: (text: string) => boolean;
76
76
  * it reads plainly, otherwise the need's own words. */
77
77
  export declare function needSentence(key: NeedKey, reason: string | null, context?: NeedContext): string;
78
78
  export declare function waitSentence(key: WaitKey | undefined, reason: string | null, context?: NeedContext): string;
79
- /** A finished run Toolroll can't prove stopped (store.stopQuiescenceFact):
80
- * a person can confirm it only when nothing of it may still be running.
81
- * `unknown`: Toolroll can't check, so the person confirms they checked. */
79
+ /** A finished run Toolroll can't yet prove stopped (store.stopQuiescenceFact). Nobody is asked to confirm it:
80
+ * `unprovable` is Toolroll's own check still to run (stop-proof.ts), which settles by itself; `unknown` is that this
81
+ * computer can't look, or something Toolroll didn't start is in the build's folder — held, with nothing to vouch for. */
82
82
  export declare function processNeedOf(fact: {
83
83
  run: number;
84
84
  kind: "open" | "alive" | "elsewhere" | "unprovable" | "unknown" | "settling";
85
85
  } | null): {
86
- need: "confirm-stopped" | "check-stopped";
86
+ need: "stop-unproven";
87
87
  build: number;
88
88
  } | {
89
89
  wait: WaitKey;
package/dist/needs-you.js CHANGED
@@ -22,9 +22,10 @@ export const NEEDS = {
22
22
  answer: { sentence: () => "Answer the question so the work can continue.", action: { code: "answer-decision", label: "Answer the question" }, useReason: always },
23
23
  questions: { sentence: () => "Too many questions are open. Answer one so this task can start.", action: { code: "inspect-decisions", label: "Answer questions" } },
24
24
  "sign-in": { sentence: context => `${context.provider ?? "Your agent"} needs you to sign in again. The task starts on its own after.`, action: { code: "sign-in", label: "Sign in again" }, useReason: reason => /sign in|API key/i.test(reason) },
25
- "confirm-stopped": { sentence: context => `Toolroll can't confirm ${build(context)} stopped. Nothing from it is running.`, action: { code: "confirm-stopped", label: "Confirm it stopped" } },
26
- // Toolroll can't look at all: never "nothing is running"; the person checks, says so, then confirms.
27
- "check-stopped": { sentence: context => `Toolroll can't check whether ${build(context)} stopped. Make sure nothing from it is running, then confirm.`, action: { code: "confirm-stopped", label: "Confirm it stopped" } },
25
+ // Toolroll proves a build stopped itself (stop-proof.ts); nobody is asked to vouch for it. Only when this computer
26
+ // can't look, or something Toolroll didn't start is using the build's folder, is the work held: the person can close
27
+ // what's theirs and have it look again, nothing more.
28
+ "stop-unproven": { sentence: context => `Toolroll can't prove ${build(context)} stopped from this computer, so the work is held. Close anything of yours that is using its folder, then check again.`, action: { code: "recheck-stopped", label: "Check again" } },
28
29
  hold: { sentence: () => "A hold is keeping this task from starting. Review it to release it.", action: { code: "inspect-hold", label: "Review hold" } },
29
30
  "choose-project": { sentence: () => "Choose a project so a builder can start.", action: { code: "place-task", label: "Choose a project" } },
30
31
  "define-task": { sentence: () => "Say what the task should do so it can be planned.", action: { code: "write-scope", label: "Define the task" } },
@@ -70,7 +71,7 @@ export function askChipOf(read) {
70
71
  return "Question";
71
72
  if (read.need === "hold")
72
73
  return "On hold";
73
- if (read.need === "vanished" || read.need === "confirm-stopped" || read.need === "check-stopped")
74
+ if (read.need === "vanished" || read.need === "stop-unproven")
74
75
  return "Stopped";
75
76
  if (read.need === "sign-in")
76
77
  return "Sign in";
@@ -100,11 +101,12 @@ export const NEED_ASK = {
100
101
  approval: "decide", card: "decide", answer: "decide", questions: "decide", "choose-project": "decide", "define-task": "decide",
101
102
  "fix-request": "decide", "choose-agent": "decide",
102
103
  "review-result": "review", rebuild: "review", replan: "decide",
103
- "sign-in": "unblock", "confirm-stopped": "unblock", "check-stopped": "unblock", hold: "unblock", "fix-dependency": "unblock",
104
+ "sign-in": "unblock", "stop-unproven": "unblock", hold: "unblock", "fix-dependency": "unblock",
104
105
  "add-requirement": "unblock", "connect-builder": "unblock", "start-builder": "unblock", vanished: "unblock", "earlier-version": "unblock", other: "unblock",
105
106
  };
106
107
  export const WAITS = {
107
108
  "build-finishing": () => "Finishing up; this clears on its own.",
109
+ "build-checking": context => `Making sure ${build(context)} stopped. This clears on its own.`,
108
110
  "build-stopping": context => `Waiting for ${build(context)} to stop. Nothing is needed from you.`,
109
111
  "other-computer": context => `Waiting for the computer that ran ${build(context)} to confirm it stopped.`,
110
112
  "card-reply": () => "Waiting for a reply.",
@@ -130,18 +132,18 @@ export function waitSentence(key, reason, context = {}) {
130
132
  const plain = reason?.trim() || null;
131
133
  return plain !== null && !hasInternalWords(plain) ? plain : "Waiting for the work to continue on its own.";
132
134
  }
133
- /** A finished run Toolroll can't prove stopped (store.stopQuiescenceFact):
134
- * a person can confirm it only when nothing of it may still be running.
135
- * `unknown`: Toolroll can't check, so the person confirms they checked. */
135
+ /** A finished run Toolroll can't yet prove stopped (store.stopQuiescenceFact). Nobody is asked to confirm it:
136
+ * `unprovable` is Toolroll's own check still to run (stop-proof.ts), which settles by itself; `unknown` is that this
137
+ * computer can't look, or something Toolroll didn't start is in the build's folder — held, with nothing to vouch for. */
136
138
  export function processNeedOf(fact) {
137
139
  if (fact === null)
138
140
  return null;
139
141
  if (fact.kind === "settling")
140
142
  return { wait: "build-finishing", build: fact.run };
141
143
  if (fact.kind === "unprovable")
142
- return { need: "confirm-stopped", build: fact.run };
144
+ return { wait: "build-checking", build: fact.run };
143
145
  if (fact.kind === "unknown")
144
- return { need: "check-stopped", build: fact.run };
146
+ return { need: "stop-unproven", build: fact.run };
145
147
  return { wait: fact.kind === "elsewhere" ? "other-computer" : "build-stopping", build: fact.run };
146
148
  }
147
149
  /** Dispatch and work-index codes that wait on a person, as their need. */
@@ -57,10 +57,10 @@ export const REMOTE_ARGUMENTS = new Map([
57
57
  ['template show', args('name')],
58
58
  ...['search', 'impact'].map(action => [`knowledge ${action}`, args('query', 'base')]),
59
59
  ['knowledge refresh', args('', 'base')],
60
- // Decision reads and supersession are already project-bound in project-memory.ts. Proposal IDs are
61
- // accepted only by memory apply (step-up), never by the listing commands below.
62
- ...['search', 'decisions', 'show', 'decide', 'retire', 'review', 'status'].map(action => [`memory ${action}`,
63
- args(['search', 'decide'].includes(action) ? 'query*' : ['show', 'retire'].includes(action) ? 'query' : '', 'why reason supersedes source all decision sessions no-local')]),
60
+ // Decision reads are already project-bound in project-memory.ts. Memory writes (decide, retire, apply) are a person's
61
+ // step-up, never a token's act, so none is listed here.
62
+ ...['search', 'decisions', 'show', 'review', 'status'].map(action => [`memory ${action}`,
63
+ args(action === 'search' ? 'query*' : action === 'show' ? 'query' : '', 'why reason supersedes source all decision sessions no-local')]),
64
64
  ['flows list', args()],
65
65
  ['flows show', args('flow:flow')], ['flows export', args('flow:flow')],
66
66
  ['flows create', args('', 'yes name template', { steps: 'steps' })],
@@ -50,7 +50,7 @@ export const REMOTE_SCOPES = new Map([
50
50
  // The person's own remote actions, or everyone's within their projects for an approver (remote-audit.ts).
51
51
  ["audit", self],
52
52
  ["knowledge search", repo], ["knowledge impact", repo], ["knowledge refresh", repo],
53
- ...["search", "decisions", "show", "decide", "retire", "review", "status"].map(action => [`memory ${action}`, self]),
53
+ ...["search", "decisions", "show", "review", "status"].map(action => [`memory ${action}`, self]),
54
54
  ...["list", "show", "export", "create", "edit", "trigger add", "trigger pause", "trigger resume", "trigger remove", "card add", "archive"]
55
55
  .map(action => [`flows ${action}`, self]),
56
56
  ["models status", global], ["models list", global],
package/dist/operate.js CHANGED
@@ -33,6 +33,7 @@ import { personProof } from "./person-proof.js";
33
33
  import { RemoteMains } from "./remote-main.js";
34
34
  import { checkoutPlan, cleanCheckouts, discardCheckout, finishedCheckouts, slimKeptCheckouts, whyWords } from "./checkout-cleanup.js";
35
35
  import { closeOutCheckouts, closeOutRuns, stoppedWords } from "./run-closeout.js";
36
+ import { proveFinishedRunsStopped, proveRunStopped } from "./stop-proof.js";
36
37
  import { MIN_DAYS, RETENTION_KINDS, countWords, dailyRetention, isRetentionKind, lastSweepAt, parsePeriod, periodLabel, periodWords, retentionPlan, sweepWords } from "./retention.js";
37
38
  import { checkPolicy, parseList, policyParts } from "./policy.js";
38
39
  import { billingOf, budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, subagentNames, usd as spendUsd } from "./spend.js";
@@ -50,6 +51,7 @@ import { startLiveBackups } from "./live-backup.js";
50
51
  import { pushLimitSink } from "./provider-limits.js";
51
52
  import { heuristicSizing, installFilingSizer, ownerSizer, refineFiledSizing, settleSizings } from "./task-sizing.js";
52
53
  import { tierLines, tierReport } from "./tier-report.js";
54
+ import { savingsLines, savingsReportOrError } from "./route-savings.js";
53
55
  import { limitsView } from "./limits-ui.js";
54
56
  import { startCodexLimits } from "./codex-limits.js";
55
57
  import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
@@ -62,6 +64,8 @@ import { sendSubagentSummaries } from "./subagent-admin.js";
62
64
  import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
63
65
  import { refreshConnections } from "./mcp-connect.js";
64
66
  import { releaseCheckPurposeFor, releaseTermsFor } from "./release-carry.js";
67
+ import { approveReleaseCard, journalMatchesPlane, releaseApprovalCard, releaseCardLines, releaseTaskOf, waitingJournals } from "./release-approval.js";
68
+ import { journalFile } from "./release.js";
65
69
  import { refreshTrustedMains, settleReleaseChecks } from "./nightly-check.js";
66
70
  import { githubNightlyRepos, readGithubNightlies } from "./nightly-github.js";
67
71
  import { exec as releaseExec, githubReads } from "./release-adapters.js";
@@ -262,6 +266,9 @@ import { latestRelease, newerRelease, notifySecurityRelease, notifyVersionLive,
262
266
  import { installMethod } from "./install-method.js";
263
267
  import { PACKAGE_VERSION } from "./version.js";
264
268
  import { machineActing } from "./scope.js";
269
+ import { reevaluateResultRecords } from "./route-results.js";
270
+ import { holdRuleOff, routeRulesPass, taskEvidenceWords } from "./route-rules.js";
271
+ import { ruleHoldOf } from "./route-store.js";
265
272
  import { loginFileOf, migrateLegacyLogin, savedLoginFile, writableLoginFile } from "./login-secrets.js";
266
273
  /**
267
274
  * 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
@@ -1211,6 +1218,9 @@ async function dispatch(command, positional, flags, context) {
1211
1218
  switch (command) {
1212
1219
  case "status":
1213
1220
  return statusCommand(positional, flags, context);
1221
+ // `toolroll release approve` (cli.ts routes only this verb here; every other `release` form is release-cli.ts).
1222
+ case "release":
1223
+ return releaseApproveCommand(positional, flags, context);
1214
1224
  case "evals":
1215
1225
  return evalsCommand(positional, flags, context);
1216
1226
  case "ready":
@@ -1296,14 +1306,25 @@ async function dispatch(command, positional, flags, context) {
1296
1306
  return runKnowledgeApply(flags, { ...context, now: context.clock(), actor: verified !== null && verified.ok ? acting.name : null });
1297
1307
  }
1298
1308
  case "models": {
1299
- const acting = await askCredentials(flags, context);
1309
+ // A local owner's lowering act (models turn-off / dismiss, through the terminal UI) asks for no credential: the
1310
+ // rule door re-checks the owner and its exact act itself.
1311
+ const localOnly = currentLocalOwner() !== null && !flags.has("as") && !flags.has("token") && ["turn-off", "dismiss", "suggestions", "stop"].includes(positional[0] ?? "");
1312
+ const acting = localOnly ? null : await askCredentials(flags, context);
1313
+ // The credential generation the proof is checked against, read BEFORE it (as `config set` does): a rotation in
1314
+ // between makes the rule door refuse, never borrow the new generation.
1315
+ const provedGeneration = acting === null ? -1 : context.store.accountOf(acting.name)?.generation ?? -1;
1300
1316
  const verified = acting === null ? null : authenticateApprover(context.store, acting.name, acting.token);
1301
- return runModelsCommand(positional, flags, { store: context.store, write: context.write, json: context.json, now: context.clock(), actor: verified !== null && verified.ok ? acting.name : null });
1317
+ const proof = verified !== null && verified.ok ? { actor: acting.name, generation: provedGeneration, method: verified.method } : null;
1318
+ return runModelsCommand(positional, flags, { store: context.store, write: context.write, json: context.json, now: context.clock(), actor: proof?.actor ?? null, proof });
1302
1319
  }
1303
1320
  case "memory": {
1304
1321
  // The remembered local login (or --as/--token) names the person; reads
1305
- // and writes both record who asked.
1322
+ // and writes both record who asked. Writes are a person's: never the lead's token or chain (like knowledge apply).
1323
+ if (memoryWriteVerb(positional) && currentActor()?.lead === true)
1324
+ return fail(context.write, context.json, `memory ${positional[0]}`, "unauthenticated", MEMORY_WRITE_REFUSAL, EXIT.refused);
1306
1325
  const acting = await askCredentials(flags, context);
1326
+ if (memoryWriteVerb(positional) && acting !== null && parseLeadToken(acting.token) !== null)
1327
+ return fail(context.write, context.json, `memory ${positional[0]}`, "unauthenticated", MEMORY_WRITE_REFUSAL, EXIT.refused);
1307
1328
  const verified = acting === null ? null : verifyApproverByPassword(context.store, acting.name, acting.token, context.store.knownRepos().filter(repo => context.store.accountCanAccess(acting.name, repo)));
1308
1329
  const who = verified !== null && verified.ok ? verified.who : null;
1309
1330
  return runMemoryCommand(positional, flags, { store: context.store, write: context.write, json: context.json, now: context.clock(), actor: who?.name ?? null, repos: who?.repos ?? [], configDir: dirname(context.databaseFile), ...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }) });
@@ -2703,6 +2724,17 @@ async function tickCommand(flags, context) {
2703
2724
  continue;
2704
2725
  }
2705
2726
  }
2727
+ // THE RULE HOLD, before any claim or row (results routing, route v3): a task approved under an earned rule that
2728
+ // then switched itself off on its sealed trigger never starts on that model. Nothing opens, so nothing strikes;
2729
+ // the task is held, in words, until a person approves it again (the hold goes with that yes).
2730
+ if (!wantsPlan && !wantsScout) {
2731
+ const ruleHeld = ruleHoldOf(store, id);
2732
+ if (ruleHeld !== null) {
2733
+ holdRuleOff(store, ref.id, id, ruleHeld.model, clock());
2734
+ dispatched.push({ id, outcome: "skipped", reason: "rule-switched-off", detail: `${id}: results for ${ruleHeld.model} dropped after this was approved — approve it again or re-file it` });
2735
+ continue;
2736
+ }
2737
+ }
2706
2738
  // THE READINESS HALT (v47): a provider THIS runner has reported
2707
2739
  // unavailable never claims and never spends — no substitution, no
2708
2740
  // second-best; the skip names the observation and the ways out.
@@ -3550,6 +3582,9 @@ async function reconcileCommand(flags, context) {
3550
3582
  // A witness reserved for a spawn that never made a process (a crash or a
3551
3583
  // thrown spawn) settles once its run's process groups are proven gone.
3552
3584
  store.settleUnspawnedWitnesses(clock());
3585
+ // What no record proves stopped, Toolroll proves itself from this computer's processes (stop-proof.ts): nobody is
3586
+ // asked to confirm a build stopped, so a release waiting on its check moves on by itself.
3587
+ await proveFinishedRunsStopped(store, clock);
3553
3588
  store.settleQuiescentStops(clock());
3554
3589
  // An update whose updater ended while letting work finish must not leave new work paused.
3555
3590
  try {
@@ -3630,6 +3665,18 @@ async function reconcileCommand(flags, context) {
3630
3665
  tidied = runTidy(store, clock(), releaseFactsFor(context.databaseFile), TIDY_PASS_LIMIT, { principal: "operator", repos: [repo], includeUnplaced: false });
3631
3666
  }
3632
3667
  catch { /* the next pass tries again */ }
3668
+ // Result records (results-routing.md, slice 1): each finished family's record is evaluated again from its rows —
3669
+ // a completion, a follow-up check, a revision, a proof, a merge or the task ending each change what it says.
3670
+ try {
3671
+ reevaluateResultRecords(store, canonicalProject(repo) ?? resolve(repo), context.evidenceRoot, clock());
3672
+ }
3673
+ catch { /* the next pass tries again */ }
3674
+ // Earned rules (results-routing.md, slice 2): each live rule checked against its sealed terms on the records just
3675
+ // evaluated; tasks approved under one that switched itself off are held for a new approval.
3676
+ try {
3677
+ routeRulesPass(store, canonicalProject(repo) ?? resolve(repo), clock());
3678
+ }
3679
+ catch { /* the next pass tries again */ }
3633
3680
  // Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
3634
3681
  // the task is complete or cancelled); their branches stay. Each removal is in the ledger.
3635
3682
  // Completing or cancelling a task stops anything still running from its checkout first.
@@ -9321,7 +9368,7 @@ function showTask(positional, context) {
9321
9368
  ...(detail.automaticReview === null ? reviewStatusLines(detail.review, latestFinished?.id ?? null) : buildReviewLines(detail.automaticReview)),
9322
9369
  ...(scope === null
9323
9370
  ? [" no scope — nothing will build this until one is written and approved"]
9324
- : describeScope(scope, readiness)),
9371
+ : describeScope(scope, readiness, taskEvidenceWords(store, task.id, now))),
9325
9372
  ...planContractLines(store, context.evidenceRoot, id),
9326
9373
  // The route's standing (v47): sealed, proposed, legacy, or unreadable —
9327
9374
  // the projection itself is in describeScope's lines above.
@@ -10752,43 +10799,47 @@ async function resumeTaskCommand(positional, flags, context) {
10752
10799
  ]);
10753
10800
  }
10754
10801
  /**
10755
- * `toolroll run settle <id> --why "<text>" --as <you> --token <t>`: an
10756
- * approver's last resort for a finished run whose process witnesses can be
10757
- * proven neither exited nor alive (a spawn interrupted before its pid was
10758
- * written). Refuses while anything of the run is alive; the reason is kept
10759
- * in the ledger against the approver's name. Signals nothing.
10802
+ * `toolroll run settle <id> --as <you> --token <t>`: have Toolroll check again whether a finished run stopped. Nobody
10803
+ * vouches that a build stopped (a person can't see whether a process exited): this runs Toolroll's own look at this
10804
+ * computer's processes (stop-proof.ts), the one the worker makes on its own, and settles the run when none of them is
10805
+ * the run's. A leftover proven the run's is stopped; anything else is left alone. `--why` is still accepted from older
10806
+ * scripts and ignored.
10760
10807
  */
10761
10808
  async function runCommand(positional, flags, context) {
10762
10809
  const { store, write, json, clock } = context;
10763
10810
  const [action, id, ...extra] = positional;
10764
10811
  const command = "run settle";
10765
- const usage = "`toolroll run settle <run-id> --why \"<reason>\" --as <you> --token <t>`";
10812
+ const usage = "`toolroll run settle <run-id>`";
10766
10813
  if (action !== "settle")
10767
10814
  return fail(write, json, "run", "usage", usage, EXIT.usage);
10768
10815
  const runId = Number(id ?? "");
10769
- const why = text(flags, "why")?.trim();
10770
10816
  if (id === undefined || extra.length > 0 || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(runId))
10771
10817
  return fail(write, json, command, "usage", usage, EXIT.usage);
10772
- if (why === undefined || why === "" || why.length > 300 || /[\x00-\x1f\x7f]/.test(why))
10773
- return fail(write, json, command, "usage", "say why in one line with --why (at most 300 characters) — it is kept in the ledger", EXIT.usage);
10774
10818
  const run = store.getRun(runId);
10775
10819
  if (run === null)
10776
10820
  return fail(write, json, command, "unknown-run", `no run #${runId}`, EXIT.refused);
10777
10821
  const acting = await askCredentials(flags, context);
10778
10822
  if (acting === null)
10779
- return fail(write, json, command, "usage", "settling takes `--as <you> --token <t>` — it is an approver's act and is recorded against your name", EXIT.usage);
10823
+ return fail(write, json, command, "usage", "checking a run again takes `--as <you> --token <t>` (or your sign-in from `toolroll up`)", EXIT.usage);
10780
10824
  const authenticated = authenticateApprover(store, acting.name, acting.token);
10781
10825
  const repo = store.refById(run.taskRef)?.repo ?? null;
10782
10826
  if (!authenticated.ok || !store.accountCanAccess(acting.name, repo))
10783
10827
  return fail(write, json, command, "not-an-approver", "that is not an approver for this run's project, or the token does not match", EXIT.refused);
10784
- const settled = store.settleRunWitnessesByApprover({ runId, by: acting.name, why }, clock());
10785
- if (!settled.ok)
10786
- return fail(write, json, command, settled.reason, settled.detail, EXIT.refused, { run: runId });
10787
- return succeed(write, json, command, { run: runId, witnesses: settled.witnesses, repeated: settled.repeated }, () => [
10788
- settled.repeated
10789
- ? `Run #${runId} has no unsettled process witness; nothing changed.`
10790
- : `Settled run #${runId}: ${settled.witnesses.length} process witness${settled.witnesses.length === 1 ? "" : "es"} recorded as ended, with your reason in the ledger.`,
10791
- ]);
10828
+ if (run.outcome === null)
10829
+ return fail(write, json, command, "still-running", `run #${runId} is still open`, EXIT.refused, { run: runId });
10830
+ const proved = await proveRunStopped(store, runId, clock);
10831
+ const stopped = store.stopQuiescenceFact(runId);
10832
+ if (stopped === null) {
10833
+ return succeed(write, json, command, { run: runId, outcome: proved.outcome === "skipped" ? "already-stopped" : "stopped", stoppedProcesses: proved.stopped ?? 0 }, () => [
10834
+ proved.outcome === "skipped" ? `Run #${runId} had already stopped; nothing changed.`
10835
+ : `Run #${runId} stopped: Toolroll checked this computer's processes and none of them is the run's${proved.stopped ? ` (it stopped ${proved.stopped} the run left running)` : ""}. The check is in the ledger.`,
10836
+ ]);
10837
+ }
10838
+ const reason = proved.outcome === "blocked" ? "unproven" : stopped.kind === "alive" ? "alive" : "waiting";
10839
+ const words = proved.outcome === "blocked" ? `Run #${proved.run} ${proved.words}.`
10840
+ : stopped.kind === "alive" ? `Something of run #${stopped.run} is still running; Toolroll stops what is proven the run's on its own.`
10841
+ : `Toolroll is still making sure run #${stopped.run} stopped. This clears on its own.`;
10842
+ return fail(write, json, command, reason, words, EXIT.refused, { run: runId });
10792
10843
  }
10793
10844
  // ---- shared ---------------------------------------------------------------
10794
10845
  /**
@@ -11132,12 +11183,15 @@ function spendCommand(flags, context) {
11132
11183
  const windows = limitsView(limits, [], { project: repo => basename(repo), subagent: id => String(id) }, context.clock())?.tiles ?? [];
11133
11184
  // Sized routing: each tier's tasks, time to a result and plan use, and what the tiers saved.
11134
11185
  const tiers = tierReport(context.store.handle, month.from, month.to, items);
11135
- return succeed(context.write, context.json, command, { month: month.name, totalMicrousd: total, unpriced, items: items.length, budgets, limits, tiers: { rows: tiers.rows, saved: tiers.saved } }, () => [
11186
+ // Suggestions and rules (results-routing slice 4): what each rule, trial and suggestion observed, over 90 days a side.
11187
+ const suggestions = savingsReportOrError(context.store, context.store.knownRepos(), context.clock());
11188
+ return succeed(context.write, context.json, command, { month: month.name, totalMicrousd: total, unpriced, items: items.length, budgets, limits, tiers: { rows: tiers.rows, saved: tiers.saved }, suggestions }, () => [
11136
11189
  `${month.name}: ${spendUsd(total)} (${items.length} pieces of work${unpriced > 0 ? `, ${unpriced} unpriced` : ""}; subscription work is $0)`,
11137
11190
  ...windows.map(tile => ` ${`${tile.name} ${tile.window}`.padEnd(24)} ${tile.value}% · ${tile.detail}`),
11138
11191
  ...[...byProject.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([repo, microusd]) => ` ${basename(repo).padEnd(24)} ${spendUsd(microusd)}`),
11139
11192
  ...budgets.map(one => ` budget ${budgetLabel(one, names.get(Number(one.key))).replace(/'s$/, "").padEnd(24)} ${spendUsd(one.spentMicrousd)} of ${spendUsd(one.limitMicrousd)} (${one.percent}%)${one.hardStop ? "" : " alerts only"}`),
11140
11193
  ...tierLines(tiers, spendUsd),
11194
+ ...savingsLines(suggestions),
11141
11195
  ]);
11142
11196
  }
11143
11197
  /** `backup now | list` (sprint 8): back the database up now, or list backups and how the latest ones went. */
@@ -13418,3 +13472,117 @@ export function upLaptopPortCandidates(store, fromSeam) {
13418
13472
  return [range[0] ?? 4180];
13419
13473
  return [...range];
13420
13474
  }
13475
+ /** The memory verbs that change project memory (memory-cli.ts), and `propose`, which files the proposals a person then
13476
+ * applies: a person's act, never the lead's. The reads stay as they are. */
13477
+ const MEMORY_WRITE_VERBS = new Set(["decide", "retire", "apply", "propose"]);
13478
+ function memoryWriteVerb(positional) {
13479
+ return MEMORY_WRITE_VERBS.has(positional[0] ?? "");
13480
+ }
13481
+ const MEMORY_WRITE_REFUSAL = "Project memory is changed by a person with their own sign-in, not the lead. The lead proposes decisions in chat for you to confirm.";
13482
+ /**
13483
+ * `toolroll release approve [<branch>] [--repo <path>]` (docs/plans/trusted-release-reports.md, slice A, A8): approve
13484
+ * the release waiting for a person from this terminal, without copying a task id or a digest. The release journals are
13485
+ * discovery only, read strictly; the plane's sealed release identity decides which release this is. The card prints
13486
+ * control-free; the person is asked through `askCredentials` (or its remembered login, under its existing conditions);
13487
+ * the card is built again AFTER that prompt and anything changed refuses; then the shared card wrapper hands the
13488
+ * snapshot's scope digest to the unchanged approve(). `--json` never prompts.
13489
+ */
13490
+ async function releaseApproveCommand(positional, flags, context) {
13491
+ const { store, write, json } = context;
13492
+ const command = "release approve";
13493
+ const usage = "`toolroll release approve [<branch>] [--repo <path>]` (with --json: --as <you> --token <password> --digest <card digest>)";
13494
+ const branchFlag = text(flags, "branch");
13495
+ if (positional[0] !== "approve" || positional.length > 2 || (branchFlag !== undefined && positional.length > 1))
13496
+ return fail(write, json, command, "usage", usage, EXIT.usage);
13497
+ const branch = branchFlag ?? positional[1];
13498
+ if (branch !== undefined && (!/^[A-Za-z0-9._/-]{1,255}$/.test(branch) || branch.startsWith("-") || branch.includes("..")))
13499
+ return fail(write, json, command, "usage", `${branch} is not a branch name this command takes.`, EXIT.usage);
13500
+ const repoFlag = text(flags, "repo");
13501
+ const repo = repoFlag === undefined ? null : resolve(repoFlag);
13502
+ const stateDir = dirname(context.databaseFile);
13503
+ const { journals, unreadable } = waitingJournals(stateDir, name => journalFile(stateDir, name));
13504
+ // The exact branch asked for, never another that shares its file name (feature/foo and feature_foo).
13505
+ const named = branch === undefined ? journals : journals.filter(one => one.branch === branch);
13506
+ if (branch !== undefined && named.length === 0 && unreadable.includes(basename(journalFile(stateDir, branch)))) {
13507
+ return fail(write, json, command, "unreadable", `${branch}'s release journal can't be read as one; nothing was approved. Run \`toolroll release ${branch}\` again.`, EXIT.refused);
13508
+ }
13509
+ const matches = [], refusals = [];
13510
+ for (const one of named) {
13511
+ if (releaseTaskOf(store, one.taskId) !== null && approvalOf(store.getScope(one.taskId)).approved)
13512
+ continue;
13513
+ const proved = journalMatchesPlane(store, one, repo);
13514
+ if (proved.ok)
13515
+ matches.push(one);
13516
+ else
13517
+ refusals.push(proved.why);
13518
+ }
13519
+ if (matches.length === 0) {
13520
+ const why = refusals.length > 0 ? `${refusals[0]}; nothing was approved` : branch === undefined ? "No release is waiting for your approval" : `No release of ${branch} is waiting for your approval`;
13521
+ return fail(write, json, command, refusals.length > 0 ? "mismatch" : "none", `${why}.`, EXIT.refused);
13522
+ }
13523
+ if (matches.length > 1) {
13524
+ return fail(write, json, command, "ambiguous", `More than one release is waiting: ${matches.map(one => `${one.branch} (${one.version})`).join(", ")}. Name the branch: \`toolroll release approve <branch>\`.`, EXIT.refused, { releases: matches.map(one => ({ branch: one.branch, version: one.version, task: one.taskId })) });
13525
+ }
13526
+ const chosen = matches[0];
13527
+ const built = releaseApprovalCard(store, chosen.taskId);
13528
+ if (!built.ok)
13529
+ return fail(write, json, command, "none", built.why, EXIT.refused);
13530
+ const shown = built.card;
13531
+ const cardData = { task: shown.taskId, version: shown.seal.identity.packageVersion, branch: shown.seal.identity.branch, candidate: shown.seal.identity.approvedSha, digest: shown.digest, ci: shown.ci?.state ?? null };
13532
+ if (shown.approved !== null)
13533
+ return fail(write, json, command, "approved", `Release ${cardData.version} is already approved by ${shown.approved.by}.`, EXIT.refused, { release: cardData });
13534
+ if (shown.refusal !== null) {
13535
+ if (!json)
13536
+ for (const line of releaseCardLines(shown))
13537
+ write(line);
13538
+ return fail(write, json, command, "refused", `Can't approve now: ${shown.refusal}`, EXIT.refused, { release: cardData });
13539
+ }
13540
+ let name, token;
13541
+ if (json) {
13542
+ const given = credentialsFrom(flags, context);
13543
+ if (given.name === undefined || given.token === undefined || given.name === "" || given.token === "")
13544
+ return fail(write, json, command, "credentials", "Approving a release needs who is agreeing: --as <you> --token <password>. Nothing was approved.", EXIT.usage, { release: cardData });
13545
+ if (text(flags, "digest") !== shown.digest)
13546
+ return fail(write, json, command, "unconfirmed", "Name the card you read: --digest <card digest>. Nothing was approved.", EXIT.refused, { release: cardData });
13547
+ name = given.name;
13548
+ token = given.token;
13549
+ }
13550
+ else {
13551
+ for (const line of releaseCardLines(shown))
13552
+ write(line);
13553
+ write("");
13554
+ if (interactive()) {
13555
+ if (!await confirm(`Approve release ${cardData.version}?`)) {
13556
+ write("Nothing approved.");
13557
+ return EXIT.refused;
13558
+ }
13559
+ }
13560
+ else if (!flags.has("yes") || text(flags, "digest") !== shown.digest) {
13561
+ write(`Nothing has been approved. Agree to exactly this card with: toolroll release approve ${chosen.branch} --yes --digest ${shown.digest} --as <you> --token <your password>`);
13562
+ return EXIT.refused;
13563
+ }
13564
+ const acting = await askCredentials(flags, context);
13565
+ if (acting === null)
13566
+ return fail(write, json, command, "credentials", "Approving a release needs who is agreeing. Nothing was approved.", EXIT.usage);
13567
+ name = acting.name;
13568
+ token = acting.token;
13569
+ }
13570
+ // After the prompt: the card again, from the plane. A change refuses; it is never approved in place of what was shown.
13571
+ const again = releaseApprovalCard(store, chosen.taskId);
13572
+ if (!again.ok || again.card.digest !== shown.digest)
13573
+ return fail(write, json, command, "changed", "This release changed while you were being asked; nothing was approved. Run the command again to read it.", EXIT.refused);
13574
+ const now = context.clock();
13575
+ const answer = approveReleaseCard(store, { taskId: chosen.taskId, cardDigest: shown.digest, approveNow: scopeDigest => approve(store, chosen.taskId, name, now, scopeDigest, token, mutationFrom(flags, now)) });
13576
+ if (!answer.ok) {
13577
+ if (answer.reason !== "approve")
13578
+ return fail(write, json, command, answer.reason, `${answer.message} Nothing was approved.`, EXIT.refused);
13579
+ const refused = answer.result;
13580
+ if (refused.reason === "policy")
13581
+ return fail(write, json, command, "policy", `${chosen.taskId}: ${refused.message}`, EXIT.refused);
13582
+ return fail(write, json, command, refused.reason, describeApproveFailure(refused.reason, chosen.taskId), EXIT.refused);
13583
+ }
13584
+ return succeed(write, json, command, { release: cardData }, () => [
13585
+ `Approved release ${cardData.version} (${cardData.candidate.slice(0, 7)}). The release check runs next; it deploys, merges and publishes once GitHub CI and the check pass.`,
13586
+ ...(shown.stopped === null ? [] : [`The release isn't running: run \`toolroll release ${chosen.branch}\` to go on.`]),
13587
+ ]);
13588
+ }
@@ -49,7 +49,7 @@
49
49
  * a legacy approval governed by its sealed profile alone.
50
50
  */
51
51
  import type { EvidenceKind } from "./contracts/acceptance-terms.js";
52
- import { MODEL_ID_SHAPE, RISKS, ROUTE_VERSION, type PhaseRoute, type RouteLeg, type RouteOverride, type RouteVersion, type TaskSizing } from "./contracts/route.js";
52
+ import { MODEL_ID_SHAPE, PLAIN_ROUTE_VERSION, RISKS, ROUTE_VERSION, type PhaseRoute, type RouteLeg, type RouteOverride, type RouteVersion, type TaskSizing } from "./contracts/route.js";
53
53
  import type { Phase, ProviderId } from "./provider.js";
54
54
  import type { QualityMode } from "./quality.js";
55
55
  /** The sealed route's shape is its contract (contracts/route.ts); these are its types. */
@@ -62,7 +62,7 @@ export declare function exactModelId(v: unknown): v is string;
62
62
  /** v2 adds the task's size (small / medium / large, risky or not), the
63
63
  * light tier, and plan headroom. A v1 route — sealed before sizing — still
64
64
  * reads back exactly and keeps its own digest domain. */
65
- export { ROUTE_VERSION };
65
+ export { PLAIN_ROUTE_VERSION, ROUTE_VERSION };
66
66
  /** The durable route ERA a scope row carries once it was filed under this
67
67
  * policy: NULL on a row proven to predate v47 (legacy — the sealed profile
68
68
  * alone governs), this value on every row filed since. A row with an era
@@ -191,6 +191,37 @@ export type RouteInput = {
191
191
  * candidates on more than one provider, the one with more room runs; a
192
192
  * provider past its limits gives way even to a stronger tier. */
193
193
  headroom?: readonly ProviderRoom[];
194
+ /** Results routing (route v3, docs/plans/results-routing.md slice 2): the project's live earned rules, and the two
195
+ * eligibility facts the route itself doesn't carry (a release or candidate task; declared paths that touch the
196
+ * project's protected paths). Absent or null: no rule applies. */
197
+ earned?: EarnedInput | null;
198
+ };
199
+ /** One live earned rule, as routing reads it: which project-size cohort it covers, the exact Claude model it builds
200
+ * on, and the configured builder it was earned against (a configuration change pauses the rule; a mismatch here
201
+ * skips it all the same). */
202
+ export type EarnedRule = {
203
+ id: number;
204
+ generation: number;
205
+ cohortSize: "small" | "medium";
206
+ provider: ProviderId;
207
+ model: string;
208
+ configured: {
209
+ provider: string;
210
+ model: string | null;
211
+ };
212
+ };
213
+ /** A live trial (results routing slice 3): try a cheaper Claude model on the next `limit` eligible tasks of one
214
+ * cohort, each still approved by a person. `reservation` is this filing's place in it (route-store.ts
215
+ * reserveTrialPlace): its id, or null when every place is taken, so the configured builder runs it. */
216
+ export type EarnedTrial = EarnedRule & {
217
+ limit: number;
218
+ reservation: number | null;
219
+ };
220
+ export type EarnedInput = {
221
+ rules: readonly EarnedRule[];
222
+ release: boolean;
223
+ protected: boolean;
224
+ trial?: EarnedTrial | null;
194
225
  };
195
226
  export declare function specWords(spec: {
196
227
  provider: string;
@@ -203,6 +234,55 @@ export declare function specWords(spec: {
203
234
  * guess.
204
235
  */
205
236
  export declare function recommendRoute(input: RouteInput): PhaseRoute;
237
+ /**
238
+ * THE BUILD LEG'S ONE PRECEDENCE ORDER (results routing, docs/plans/results-routing.md slice 2), pure:
239
+ *
240
+ * pin > person override > trial > earned rule > configured row (light / routine / strong by size)
241
+ * > headroom move
242
+ *
243
+ * Headroom moves only a configured leg, never a rule's or a trial's: their evidence is about one exact model. A rule
244
+ * or trial whose provider is past the headroom limits is skipped for this filing, and its reason says so. A trial
245
+ * applies only with a place reserved for this task (slice 3). A rule or trial applies only to an
246
+ * eligible task — small or medium, not risky, default quality, no screenshot or manual-review evidence, not a
247
+ * merging mode, not a release or candidate, nothing protected declared — and only over the configured builder it was
248
+ * earned against. Its leg says how it was selected (`selection`), so the route seals as version 3 and every later
249
+ * check reads the sealed selection, never the current rules or the reason text.
250
+ */
251
+ export declare function selectBuildLeg(input: RouteInput, configured: {
252
+ spec: RouteCandidate;
253
+ tier: CandidateTier;
254
+ reasons: string[];
255
+ }): {
256
+ leg: RouteLeg;
257
+ moved: boolean;
258
+ };
259
+ /** The earned rule this task's build takes (pure): null when none could apply (no rule for its size, or the task
260
+ * isn't eligible), or a rule skipped for this filing with the words why. */
261
+ export declare function earnedRuleFor(input: RouteInput, configured: {
262
+ spec: RouteCandidate;
263
+ tier: CandidateTier;
264
+ }): {
265
+ kind: "rule";
266
+ rule: EarnedRule;
267
+ } | {
268
+ kind: "skipped";
269
+ reason: string;
270
+ } | null;
271
+ /** The trial this task's build takes (pure, results routing slice 3): null when none could apply (no live trial for
272
+ * its size, or the task isn't eligible: the same facts an earned rule needs), a trial with this task's place, or a
273
+ * trial skipped for this filing with the words why (another configured builder, no place left, no headroom). */
274
+ export declare function trialFor(input: RouteInput, configured: {
275
+ spec: RouteCandidate;
276
+ tier: CandidateTier;
277
+ }): {
278
+ kind: "trial";
279
+ trial: EarnedTrial & {
280
+ reservation: number;
281
+ };
282
+ } | {
283
+ kind: "skipped";
284
+ reason: string;
285
+ } | null;
206
286
  export declare function legOf(route: PhaseRoute, phase: Phase): RouteLeg;
207
287
  /** Every stated problem on the route, in phase order — a non-empty list
208
288
  * means the scope files unresolved with these words. */