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.
- package/CHANGELOG.md +41 -0
- package/dist/approval-authority.d.ts +9 -1
- package/dist/approval-authority.js +15 -2
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -2
- package/dist/assignment.js +11 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-crew.js +2 -2
- package/dist/browser-workspace.d.ts +3 -5
- package/dist/build-review.js +2 -0
- package/dist/builder.js +21 -2
- package/dist/chat-actions.d.ts +11 -3
- package/dist/chat-actions.js +47 -2
- package/dist/chat-decide.js +2 -0
- package/dist/chat-review.d.ts +2 -1
- package/dist/chat-review.js +2 -2
- package/dist/check-facts.d.ts +124 -0
- package/dist/check-facts.js +272 -0
- package/dist/check-report.d.ts +5 -0
- package/dist/check-report.js +4 -3
- package/dist/cli.js +5 -0
- package/dist/contracts/chat-actions.d.ts +10 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/console-api.d.ts +3 -2
- package/dist/contracts/console-api.js +5 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/route-results.d.ts +121 -0
- package/dist/contracts/route-results.js +71 -0
- package/dist/contracts/route.d.ts +343 -5
- package/dist/contracts/route.js +36 -7
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispose.js +6 -0
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/flaky.d.ts +17 -0
- package/dist/flaky.js +9 -0
- package/dist/flow-engine.js +1 -1
- package/dist/flow-pull-request.js +1 -1
- package/dist/lead-doors.js +4 -2
- package/dist/lead-tools.js +10 -2
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/memory-cli.d.ts +3 -0
- package/dist/memory-cli.js +3 -3
- package/dist/memory-pass.js +5 -1
- package/dist/model-choices.d.ts +7 -3
- package/dist/model-choices.js +26 -11
- package/dist/models-cli.d.ts +205 -0
- package/dist/models-cli.js +143 -4
- package/dist/models-ui.d.ts +57 -0
- package/dist/models-ui.js +44 -1
- package/dist/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate-remote-arguments.js +4 -4
- package/dist/operate-remote.js +1 -1
- package/dist/operate.js +191 -23
- package/dist/phase-routing.d.ts +82 -2
- package/dist/phase-routing.js +132 -18
- package/dist/project-delete.d.ts +4 -0
- package/dist/project-delete.js +16 -4
- package/dist/project-memory.d.ts +8 -0
- package/dist/project-memory.js +20 -1
- package/dist/pull-request-flow.js +1 -1
- package/dist/push.d.ts +10 -3
- package/dist/push.js +22 -6
- package/dist/release-adapters.js +11 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +22 -4
- package/dist/release-card.d.ts +73 -4
- package/dist/release-card.js +168 -23
- package/dist/release-cli.js +9 -1
- package/dist/release.d.ts +30 -0
- package/dist/release.js +42 -3
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +3 -0
- package/dist/result-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/route-hooks.d.ts +54 -0
- package/dist/route-hooks.js +179 -0
- package/dist/route-results.d.ts +70 -0
- package/dist/route-results.js +340 -0
- package/dist/route-rules.d.ts +324 -0
- package/dist/route-rules.js +781 -0
- package/dist/route-savings.d.ts +136 -0
- package/dist/route-savings.js +193 -0
- package/dist/route-store.d.ts +300 -0
- package/dist/route-store.js +598 -0
- package/dist/scope.d.ts +3 -1
- package/dist/scope.js +4 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-release.d.ts +17 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +0 -2
- package/dist/server/render-results.js +4 -6
- package/dist/server/render-tasks.d.ts +7 -0
- package/dist/server/render-tasks.js +16 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +19 -1
- package/dist/server/settings.d.ts +2 -1
- package/dist/server/settings.js +87 -1
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +105 -20
- package/dist/settings-proofs.js +2 -0
- package/dist/spend-ui.d.ts +11 -0
- package/dist/spend-ui.js +49 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +106 -31
- package/dist/store.js +653 -128
- package/dist/surface.js +17 -5
- package/dist/task-outcome-cli.js +24 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.js +7 -9
- package/dist/work-summary.d.ts +2 -2
- package/package.json +1 -1
package/dist/needs-you.d.ts
CHANGED
|
@@ -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" | "
|
|
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" | "
|
|
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
|
-
*
|
|
81
|
-
*
|
|
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: "
|
|
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
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
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 === "
|
|
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", "
|
|
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
|
-
*
|
|
135
|
-
*
|
|
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 {
|
|
144
|
+
return { wait: "build-checking", build: fact.run };
|
|
143
145
|
if (fact.kind === "unknown")
|
|
144
|
-
return { need: "
|
|
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
|
|
61
|
-
//
|
|
62
|
-
...['search', 'decisions', 'show', '
|
|
63
|
-
args(
|
|
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' })],
|
package/dist/operate-remote.js
CHANGED
|
@@ -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", "
|
|
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
|
-
|
|
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
|
-
|
|
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> --
|
|
10756
|
-
*
|
|
10757
|
-
*
|
|
10758
|
-
*
|
|
10759
|
-
*
|
|
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
|
|
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", "
|
|
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
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/phase-routing.d.ts
CHANGED
|
@@ -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. */
|