toolroll 0.9.90 → 0.9.92
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/build-review.js +2 -0
- 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/contracts/chat-actions.d.ts +10 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +3 -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/dispose.js +6 -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/operate-remote-arguments.js +4 -4
- package/dist/operate-remote.js +1 -1
- package/dist/operate.js +53 -5
- 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 +12 -4
- package/dist/project-knowledge.d.ts +6 -6
- 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/release-adapters.d.ts +2 -0
- package/dist/release-adapters.js +30 -1
- package/dist/release-auto.js +12 -3
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +3 -0
- 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/render-chat.js +2 -2
- package/dist/server/render-tasks.d.ts +7 -0
- package/dist/server/render-tasks.js +13 -0
- package/dist/server/route-table.js +12 -0
- 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.js +3 -0
- package/dist/settings-proofs.js +2 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/spend-ui.d.ts +11 -0
- package/dist/spend-ui.js +49 -2
- package/dist/store.d.ts +38 -2
- package/dist/store.js +320 -35
- package/dist/surface.js +16 -4
- package/dist/task-outcome-cli.js +5 -2
- package/dist/toolroll-update.js +3 -2
- package/package.json +1 -1
|
@@ -34,7 +34,7 @@ import { chatMoney, oneLineOf, relativeAge, safePrUrl, screen, sentenceCase, str
|
|
|
34
34
|
import { chatResultHref, projectChatHref, taskChatHref, taskHref } from "./http.js";
|
|
35
35
|
import {} from "./render-pages.js";
|
|
36
36
|
import { completionReceiptCard } from "./render-results.js";
|
|
37
|
-
import { agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
|
|
37
|
+
import { routeEvidenceHtml, agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
|
|
38
38
|
/** The installation fact that the lead was turned on by default, once (its value: the provider). */
|
|
39
39
|
export const LEAD_BY_DEFAULT_FACT = "lead-on-by-default";
|
|
40
40
|
/** The installation fact that the phone card was put away. */
|
|
@@ -51,7 +51,7 @@ export function taskChatContext(focus) {
|
|
|
51
51
|
// conversation never hides which agent runs.
|
|
52
52
|
focus.route === null
|
|
53
53
|
? ""
|
|
54
|
-
: html `<div class="task-chat-agents-aside"><span class="eyebrow">agents</span><p class="agents-summary">${agentsSummaryWords(focus.route)}</p
|
|
54
|
+
: html `<div class="task-chat-agents-aside"><span class="eyebrow">agents</span><p class="agents-summary">${agentsSummaryWords(focus.route)}</p>${routeEvidenceHtml(focus.route)}<div class="agents-badges">${agentsBadgesHtml(focus.route)}</div>${focus.route.projection === null ? "" : html `${agentsAvailabilityHtml(focus.route.projection)}${agentsWhyHtml(focus.route.projection)}`}<p class="meta"><a href="${taskHref(focus.id)}#agents">Change agents on the task →</a></p></div>`}<div class="task-chat-overview-actions"><a class="task-chat-overview-link" href="${taskHref(focus.id)}">Open full overview →</a></div></aside>`;
|
|
55
55
|
}
|
|
56
56
|
export function taskChatApproval(focus, csrf) {
|
|
57
57
|
const approval = focus.approval;
|
|
@@ -220,7 +220,12 @@ export type RouteView = {
|
|
|
220
220
|
* every option an exact pair the operator named once in configuration.
|
|
221
221
|
* The form offers these and nothing else. */
|
|
222
222
|
choices: Record<Phase, AgentChoice[]>;
|
|
223
|
+
/** Results routing (slice 2): "Why this model: …" when an earned rule selected the build leg (route-rules.ts
|
|
224
|
+
* selectionEvidenceWords), else absent. The selection is sealed; the counts read "so far". */
|
|
225
|
+
evidence?: string | null;
|
|
223
226
|
};
|
|
227
|
+
/** The evidence line beside the agents, only when an earned rule chose the builder. */
|
|
228
|
+
export declare function routeEvidenceHtml(view: RouteView | null | undefined, className?: string): Html;
|
|
224
229
|
export declare const ROLE_NOUN: Record<RouteProjection["legs"][number]["phase"], string>;
|
|
225
230
|
export declare function agentsStandingWords(view: RouteView): string;
|
|
226
231
|
/** The size line an approval leads its agents with — "Small change: fast
|
|
@@ -609,6 +614,8 @@ export declare function taskBodyParts(data: {
|
|
|
609
614
|
qualityMode?: QualityMode | null;
|
|
610
615
|
/** The phase route (v47) and whether this session may edit it. */
|
|
611
616
|
route?: RouteView | null;
|
|
617
|
+
/** Results routing (slice 1): this task family's result record in words; null when it has none yet. */
|
|
618
|
+
resultRecord?: string | null;
|
|
612
619
|
canEditRoute?: boolean;
|
|
613
620
|
runs: Run[];
|
|
614
621
|
/** Proof produced by the newest finished attempt. The two booleans are
|
|
@@ -439,6 +439,7 @@ export function approvalSheetParts(input) {
|
|
|
439
439
|
// The size, said once and plainly beside who builds: "Small change: fast model, no plan".
|
|
440
440
|
projection === null ? "" : sizeLineHtml(projection),
|
|
441
441
|
who.length === 0 ? "" : html `<p class="approval-who">${who.join(" · ")}</p>`,
|
|
442
|
+
routeEvidenceHtml(route, "approval-evidence"),
|
|
442
443
|
consent,
|
|
443
444
|
]);
|
|
444
445
|
return { lead, rows, editor, editorForm, details, submit, label: rebuild !== null ? "Approve and build again" : "Approve and start", after, allowing, passwordNote };
|
|
@@ -565,6 +566,14 @@ export function profileWords(scope) {
|
|
|
565
566
|
const terms = sandboxTermsWords(profile);
|
|
566
567
|
return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>${profile.checkAccess === undefined ? "" : html `<p class="meta" data-scope-checks>Checks: ${checkAccessWords(profile.checkAccess)}.</p>`}<p class="meta" data-scope-reads>Reads: ${terms.reads}.</p>${terms.mach === null ? "" : html `<p class="meta" data-scope-mach>${terms.mach.charAt(0).toUpperCase()}${terms.mach.slice(1)}.</p>`}`;
|
|
567
568
|
}
|
|
569
|
+
/** The evidence line beside the agents, only when an earned rule chose the builder. */
|
|
570
|
+
export function routeEvidenceHtml(view, className = "meta") {
|
|
571
|
+
const words = view?.evidence ?? null;
|
|
572
|
+
if (words === null)
|
|
573
|
+
return html ``;
|
|
574
|
+
const split = words.indexOf(": ");
|
|
575
|
+
return html `<p class="${className} route-evidence" data-route-evidence><strong>${words.slice(0, split + 1)}</strong>${words.slice(split + 1)}</p>`;
|
|
576
|
+
}
|
|
568
577
|
export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
|
|
569
578
|
export function agentsStandingWords(view) {
|
|
570
579
|
return view.kind === "legacy"
|
|
@@ -658,6 +667,7 @@ export function agentsCeremonyHtml(view) {
|
|
|
658
667
|
html `<div class="agents-ceremony"><p class="approval-label">agents</p>`,
|
|
659
668
|
sizeLineHtml(view.projection),
|
|
660
669
|
html `<p class="agents-summary">${view.projection.summary}</p>`,
|
|
670
|
+
routeEvidenceHtml(view),
|
|
661
671
|
html `<div class="agents-badges">${riskBadgeHtml(view.projection)}<span class="badge">${sentenceCase(view.projection.postureWords)}</span></div>`,
|
|
662
672
|
agentsWhyHtml(view.projection),
|
|
663
673
|
html `<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`,
|
|
@@ -735,6 +745,7 @@ export function agentsCardHtml(taskId, view, canEdit) {
|
|
|
735
745
|
html `<section class="card agents-card" id="agents" aria-label="agents">`,
|
|
736
746
|
html `<div class="agents-head"><h3>Agents</h3><div class="agents-badges">${agentsBadgesHtml(view)}</div></div>`,
|
|
737
747
|
html `<p class="agents-summary">${agentsSummaryWords(view)}</p>`,
|
|
748
|
+
routeEvidenceHtml(view),
|
|
738
749
|
p === null
|
|
739
750
|
? view.kind === "unreadable"
|
|
740
751
|
? html `<p class="agents-halted">Nothing runs for this task until its scope is filed again and approved.</p>`
|
|
@@ -2280,6 +2291,8 @@ export function taskBodyParts(data) {
|
|
|
2280
2291
|
review.push({ ...approvedScope, label: approvedScope.label === "Approved scope" ? "Approvals" : "Scope" });
|
|
2281
2292
|
review.push({ label: "Who reviews", parts: [facts.find(one => one.label === "Approval rules")?.parts[0] ?? "Any approver in this project"] });
|
|
2282
2293
|
review.push({ label: "Publishes as", parts: [publishesAs] });
|
|
2294
|
+
if (data.resultRecord != null)
|
|
2295
|
+
review.push({ label: "Result record", parts: [data.resultRecord] });
|
|
2283
2296
|
const about = [];
|
|
2284
2297
|
const filedFact = facts.find(one => one.label === "Filed by");
|
|
2285
2298
|
about.push(filedFact ?? { label: "Filed by", parts: [filedBy ?? (data.origin != null ? "An automation" : data.filedVia == null ? "Not recorded" : `via ${data.filedVia}`)] });
|
|
@@ -91,6 +91,11 @@ const LOCAL_POSTS = Object.freeze({
|
|
|
91
91
|
"settings.lead-on": "choose-default-agent",
|
|
92
92
|
// Choosing which open project the pages show: a view filter, never authorization.
|
|
93
93
|
"projects.select": "read",
|
|
94
|
+
// Results routing: lowering only.
|
|
95
|
+
"settings.models-turn-off": "routing-turn-off",
|
|
96
|
+
"settings.models-not-now": "dismiss-suggestion",
|
|
97
|
+
"settings.models-suggest-off": "dismiss-suggestion",
|
|
98
|
+
"settings.models-stop": "trial-stop",
|
|
94
99
|
"agents.seen": "mark-seen",
|
|
95
100
|
"agents.watch": "watch-agent",
|
|
96
101
|
});
|
|
@@ -483,6 +488,13 @@ for (const one of ["check", "watch", "update"])
|
|
|
483
488
|
// Passwordless session; model-choices.ts re-proves the session and standing inside its one transaction).
|
|
484
489
|
for (const one of ["agent", "apply"])
|
|
485
490
|
post("settings", `settings.models-${one}`, `/settings/models/${one}`, { scope: "step-up", ...operator("An instance operator changes models.", "/settings/models") });
|
|
491
|
+
// Earned rules (results routing): accepting, keeping a paused rule and switching suggestions on are an instance
|
|
492
|
+
// operator's browser step-up; turning a rule off, Not now and switching suggestions off only take authority away (a
|
|
493
|
+
// signed-in operator, or the local owner in exactly that act: route-store.ts proveRuleDoor re-checks it in the write).
|
|
494
|
+
for (const one of ["accept", "keep", "suggest-on", "try"])
|
|
495
|
+
post("settings", `settings.models-${one}`, `/settings/models/${one}`, { scope: "step-up", ...operator("An instance operator changes which models run.", "/settings/models") });
|
|
496
|
+
for (const one of ["turn-off", "not-now", "suggest-off", "stop"])
|
|
497
|
+
post("settings", `settings.models-${one}`, `/settings/models/${one}`, operator("An instance operator changes which models run.", "/settings/models"));
|
|
486
498
|
// Any other models action answers the models area's own refusal.
|
|
487
499
|
post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/unknown", ...MODELS });
|
|
488
500
|
post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to refresh project context.", "/settings/knowledge") });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ServerRuntime } from './runtime.js';
|
|
2
|
-
import { type RoleView } from "../models-ui.js";
|
|
2
|
+
import { type RoleView, type SuggestedView } from "../models-ui.js";
|
|
3
3
|
export declare function createSettingsHandlers(runtime: ServerRuntime): {
|
|
4
4
|
registrations: import("./handler-registry.js").Registration[];
|
|
5
5
|
modelsView: (canManage: boolean, csrf: string, now: Date, said: string | null, problem: string | null, query?: URLSearchParams) => {
|
|
@@ -71,5 +71,6 @@ export declare function createSettingsHandlers(runtime: ServerRuntime): {
|
|
|
71
71
|
taskId: string;
|
|
72
72
|
title: string;
|
|
73
73
|
}[];
|
|
74
|
+
suggested: SuggestedView;
|
|
74
75
|
};
|
|
75
76
|
};
|
package/dist/server/settings.js
CHANGED
|
@@ -76,6 +76,7 @@ import { startConnect } from "../mcp-connect.js";
|
|
|
76
76
|
import { decideProposal, listProposals, memoryStatus } from "../memory-pass.js";
|
|
77
77
|
import { checkModels, isNewModel, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "../model-catalog.js";
|
|
78
78
|
import { modelsHtml, modelsScript } from "../models-ui.js";
|
|
79
|
+
import { acceptSuggestion, dismissSuggestion, grantWords, keepRule, modelName, ruleWords, rulesOf, setSuggestModels, startTrial, stopTrial, suggestionsFor, suggestionWords, suggestModelsOn, trialOffersFor, trialOfferWords, trialsOf, trialWords, turnOffRule } from "../route-rules.js";
|
|
79
80
|
import { ALL_SLOTS, applyModelChoices, browserWords, checkScope, DEFAULT_PRESET, defaultAgentOf, fillDefaultRouting, isPresetName, newerModels, PRESETS, presetChange, presetInUse, previewModelChange, recentRuns, rowAt, slotKey, SLOTS, slotWords, stateDigest, switchChange } from "../model-choices.js";
|
|
80
81
|
import { HEADROOM_FIVE_HOUR_LIMIT, HEADROOM_WEEKLY_LIMIT, exactModelId } from "../phase-routing.js";
|
|
81
82
|
import { RISKY_MEANS, SIZE_MEANS, SIZING_CLAUDE_MODEL } from "../task-sizing.js";
|
|
@@ -118,6 +119,7 @@ import { slackSettingsHtml } from "../slack-settings.js";
|
|
|
118
119
|
import { spendHtml, spendScript } from "../spend-ui.js";
|
|
119
120
|
import { approvalFactsNow } from "./kit.js";
|
|
120
121
|
import { spendReport, spendReportCsv, spendRows } from "../spend-report.js";
|
|
122
|
+
import { savingsReportOrError } from "../route-savings.js";
|
|
121
123
|
import { limitsView } from "../limits-ui.js";
|
|
122
124
|
import { budgetStates, monthNamed, spendItems, usd as spendUsd, subagentNames as subagentNamesOf } from "../spend.js";
|
|
123
125
|
import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "../sso-settings.js";
|
|
@@ -185,7 +187,9 @@ export function createSettingsHandlers(runtime) {
|
|
|
185
187
|
};
|
|
186
188
|
const windows = limitsView(store.providerLimits(), [], { project: projectName, subagent: id => subagentNames.get(id) ?? `Subagent ${id}` }, now);
|
|
187
189
|
const view = { month: month.name, previous: shift(-1), next: month.name === current.name ? null : shift(1), current: current.name, report: spendReport(rows),
|
|
188
|
-
budgets, unlimited, targets, windows, subagentNames, edit, facts: approvalFactsNow()
|
|
190
|
+
budgets, unlimited, targets, windows, subagentNames, edit, facts: approvalFactsNow(),
|
|
191
|
+
// Suggestions and rules (results-routing slice 4): read-only, for every project this console shows.
|
|
192
|
+
savings: savingsReportOrError(store, consoleProjects(), now) };
|
|
189
193
|
// A refusal for the open form shows in the form; anything else under the limits' heading.
|
|
190
194
|
const said = { said: url.searchParams.get("said"), problem: edit === null ? url.searchParams.get("problem") : null };
|
|
191
195
|
return sendScreen(response, 200, screen("Spend", spendHtml(view, said), { chrome: chromeFor(null, "spend"), functional: { script: spendScript() } }));
|
|
@@ -2565,6 +2569,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2565
2569
|
chosen: one.chosen === null ? "its default" : /^(opus|sonnet|haiku|fable)$/.test(one.chosen) ? `floating ${one.chosen}` : one.chosen,
|
|
2566
2570
|
ranOn: one.ranOn ?? "not reported" })),
|
|
2567
2571
|
waiting: canManage ? store.scopesAwaitingApproval(null, 10, admissionList()).map(one => ({ taskId: one.taskId, title: one.title })) : [],
|
|
2572
|
+
suggested: suggestedView(repo, projects, query.get("suggestion"), now),
|
|
2568
2573
|
};
|
|
2569
2574
|
}
|
|
2570
2575
|
/** Google's redirect back: the code becomes a saved refresh token, and a page sends the person back to Settings. */
|
|
@@ -2739,6 +2744,14 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2739
2744
|
"settings.models-agent": modelsSave,
|
|
2740
2745
|
"settings.models-apply": modelsApply,
|
|
2741
2746
|
"settings.models-act": modelsAct("other"),
|
|
2747
|
+
"settings.models-accept": modelsRuleAct("accept"),
|
|
2748
|
+
"settings.models-keep": modelsRuleAct("keep"),
|
|
2749
|
+
"settings.models-suggest-on": modelsRuleAct("suggest-on"),
|
|
2750
|
+
"settings.models-turn-off": modelsRuleAct("turn-off"),
|
|
2751
|
+
"settings.models-not-now": modelsRuleAct("not-now"),
|
|
2752
|
+
"settings.models-suggest-off": modelsRuleAct("suggest-off"),
|
|
2753
|
+
"settings.models-try": modelsRuleAct("try"),
|
|
2754
|
+
"settings.models-stop": modelsRuleAct("stop"),
|
|
2742
2755
|
"settings.knowledge-refresh": knowledgeRefresh,
|
|
2743
2756
|
"settings.knowledge-proposal": knowledgeProposal,
|
|
2744
2757
|
"settings.knowledge-decision": knowledgeDecision,
|
|
@@ -3005,5 +3018,78 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
3005
3018
|
return back("problem", turned.reason === "not-on" ? "Local mode is already off." : "Only an instance operator can turn off local mode.");
|
|
3006
3019
|
return back("said", "Local mode is off. This computer's browser signs in with your password now.");
|
|
3007
3020
|
}
|
|
3021
|
+
/** Settings → Models → Suggested: one project's offers, rules and switch; on the installation scope, every
|
|
3022
|
+
* project's offers and rules, each reviewed on its own project's page. */
|
|
3023
|
+
function suggestedView(repo, projects, asked, now) {
|
|
3024
|
+
const repos = repo === null ? projects : [repo];
|
|
3025
|
+
const offers = repos.flatMap(one => suggestionsFor(store, one, now));
|
|
3026
|
+
const sizeTitle = (size, name) => `${name} for ${size === "small" ? "small" : "routine"} tasks`;
|
|
3027
|
+
const reviewed = asked === null || repo === null ? null : offers.find(one => one.id === asked && one.repo === repo) ?? null;
|
|
3028
|
+
return {
|
|
3029
|
+
repo,
|
|
3030
|
+
offers: offers.map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo), title: sizeTitle(one.cohortSize, one.name), words: suggestionWords(one) })),
|
|
3031
|
+
review: asked === null || repo === null ? null : reviewed === null ? { stale: true, repo } : {
|
|
3032
|
+
id: reviewed.id, repo: reviewed.repo, title: `${sizeTitle(reviewed.cohortSize, reviewed.name)} instead of ${reviewed.configured.name}`,
|
|
3033
|
+
evidence: [suggestionWords(reviewed), reviewed.configuredEvidence.decided === 0 ? `${reviewed.configured.name}: no decided similar tasks here in the last 90 days.` : `${reviewed.configured.name}: ${reviewed.configuredEvidence.mature} of its last ${reviewed.configuredEvidence.decided} decided similar tasks accepted with checks passing.`],
|
|
3034
|
+
prices: `${reviewed.name} $${reviewed.prices.model.in} / $${reviewed.prices.model.out}, ${reviewed.configured.name} $${reviewed.prices.configured.in} / $${reviewed.prices.configured.out} per 1M tokens (listed now)`,
|
|
3035
|
+
terms: grantWords(reviewed.grant), save: `Use ${reviewed.name} for ${reviewed.cohortSize === "small" ? "small" : "routine"} tasks`,
|
|
3036
|
+
},
|
|
3037
|
+
rules: repos.flatMap(one => rulesOf(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo), title: sizeTitle(one.cohortSize, modelName(store, one.model)), words: ruleWords(store, one),
|
|
3038
|
+
state: one.state === "active" ? "active" : one.state === "paused" ? "paused" : "ended" })),
|
|
3039
|
+
suggest: repo === null ? null : { on: suggestModelsOn(store, repo) },
|
|
3040
|
+
trialOffers: repos.flatMap(one => trialOffersFor(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo),
|
|
3041
|
+
title: `Try ${sizeTitle(one.cohortSize, one.name)}`, words: trialOfferWords(one), save: `Try it on the next ${one.places}` })),
|
|
3042
|
+
trials: repos.flatMap(one => trialsOf(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo),
|
|
3043
|
+
title: `Trying ${sizeTitle(one.cohortSize, modelName(store, one.model))}`, words: trialWords(store, one), live: one.state === "trialling" || one.state === "paused" })),
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Settings → Models → Suggested (results routing, slice 2): the earned-rule doors (route-rules.ts), re-proved inside
|
|
3048
|
+
* their own transaction. Accepting, keeping a paused rule and switching suggestions on are an instance operator's
|
|
3049
|
+
* step-up (the password, or a Passwordless session); turning a rule off or dropping it, Not now, and switching
|
|
3050
|
+
* suggestions off only take authority away, so a signed-in session does — or this computer's local owner, whose
|
|
3051
|
+
* route row narrows it to exactly that act.
|
|
3052
|
+
*/
|
|
3053
|
+
function modelsRuleAct(kind) {
|
|
3054
|
+
return async (ctx) => {
|
|
3055
|
+
const { response, now, posted } = ctx;
|
|
3056
|
+
const who = browserCaller(ctx.who);
|
|
3057
|
+
const body = readForm(posted, CONSOLE_FORMS.modelsRules);
|
|
3058
|
+
const repo = body.get("scope") ?? "";
|
|
3059
|
+
const back = (key, words) => redirect(response, `/settings/models?scope=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#models-suggested`);
|
|
3060
|
+
if (["scope", "password", "id", "rule"].some(field => body.sent.getAll(field).length > 1))
|
|
3061
|
+
return back("problem", "That form was sent twice over. Nothing changed.");
|
|
3062
|
+
if (!consoleProjects().includes(repo) || !store.accountCanAccess(who.name, repo))
|
|
3063
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/models");
|
|
3064
|
+
let door;
|
|
3065
|
+
if (kind === "accept" || kind === "keep" || kind === "suggest-on" || kind === "try") {
|
|
3066
|
+
const proved = authenticateApprover(who, body.get("password") ?? "");
|
|
3067
|
+
if (!proved.ok)
|
|
3068
|
+
return back("problem", "Enter your Toolroll password to change which models run.");
|
|
3069
|
+
door = { kind: "person", person: modelsAuthority(who, proved.method) };
|
|
3070
|
+
}
|
|
3071
|
+
else {
|
|
3072
|
+
door = who.authority === "local-owner" ? { kind: "local-owner", actor: who.name } : { kind: "person", person: modelsAuthority(who, "session") };
|
|
3073
|
+
}
|
|
3074
|
+
const ruleId = /^[1-9][0-9]{0,9}$/.test(body.get("rule") ?? "") ? Number(body.get("rule")) : -1;
|
|
3075
|
+
const id = body.get("id") ?? "";
|
|
3076
|
+
const result = kind === "accept" ? acceptSuggestion(store, { repo, id }, door, now)
|
|
3077
|
+
: kind === "try" ? startTrial(store, { repo, id }, door, now)
|
|
3078
|
+
: kind === "stop" ? stopTrial(store, ruleId, door, now)
|
|
3079
|
+
: kind === "keep" ? keepRule(store, ruleId, door, now)
|
|
3080
|
+
: kind === "turn-off" ? turnOffRule(store, ruleId, door, now)
|
|
3081
|
+
: kind === "not-now" ? dismissSuggestion(store, { repo, id }, door, now)
|
|
3082
|
+
: setSuggestModels(store, repo, kind === "suggest-on", door);
|
|
3083
|
+
if (!result.ok)
|
|
3084
|
+
return back("problem", result.problem);
|
|
3085
|
+
return back("said", kind === "try" ? "Trial started. The next tasks of that size try the cheaper model; each waits for your approval."
|
|
3086
|
+
: kind === "stop" ? "Trial stopped. New tasks build on the configured model; tasks already approved keep their model."
|
|
3087
|
+
: kind === "accept" ? "Saved. New tasks of that size build on the suggested model; each is still approved by a person."
|
|
3088
|
+
: kind === "keep" ? "Kept. The rule is on again with its original terms."
|
|
3089
|
+
: kind === "turn-off" ? "Turned off. New tasks build on the configured model; tasks already approved keep their model."
|
|
3090
|
+
: kind === "not-now" ? "Not now. This suggestion stays quiet for 30 days."
|
|
3091
|
+
: kind === "suggest-on" ? "Suggestions are on for this project." : "Suggestions are off for this project.");
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3008
3094
|
return { registrations, modelsView };
|
|
3009
3095
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resultRecordWordsOf } from "../route-results.js";
|
|
2
|
+
import { selectionEvidenceWords } from "../route-rules.js";
|
|
1
3
|
import { feedbackImageProblem } from "../result-actions.js";
|
|
2
4
|
import { parseFeedbackImage } from "../result-review.js";
|
|
3
5
|
import { releaseViewOf } from "../release-view.js";
|
|
@@ -473,6 +475,8 @@ export function createTaskViews(runtime) {
|
|
|
473
475
|
// and the focused chat, with the readiness this task's runners report.
|
|
474
476
|
route: routeView,
|
|
475
477
|
canEditRoute: who.role === "approver",
|
|
478
|
+
// Results routing (slice 1): the family's saved record, or the one its rows give now; never a reason to fail the page.
|
|
479
|
+
resultRecord: resultRecordWordsOf(runtime.store, taskId, runtime.evidenceRoot, now),
|
|
476
480
|
spendDefaults: runtime.store.getSpendDefaults(),
|
|
477
481
|
permissionDefault: runtime.store.permissionDefault().mode,
|
|
478
482
|
permissionMode: ref?.permissionMode ?? null,
|
|
@@ -571,6 +575,8 @@ export function createTaskViews(runtime) {
|
|
|
571
575
|
replanOnPlanChange: ref.plan === "drafted" && scope !== null && !approvalOf(scope).approved,
|
|
572
576
|
digest: scope?.digest ?? null,
|
|
573
577
|
choices: agentChoicesFor(runtime.store, ref.repo, routed.kind === "route" ? routed.route : null),
|
|
578
|
+
// Results routing: why an earned rule's model builds it, from the records as they stand now.
|
|
579
|
+
evidence: routed.kind === "route" ? selectionEvidenceWords(runtime.store, routed.route, now) : null,
|
|
574
580
|
};
|
|
575
581
|
if (routed.kind === "route") {
|
|
576
582
|
return { kind: "route", source: routed.source, projection: projectRoute(routed.route, runtime.store.readinessLookupFor(ref.repo, ref.assignedRunner, now)), legacy: null, problem: null, ...shared };
|
package/dist/server/tasks.js
CHANGED
|
@@ -1444,6 +1444,8 @@ export function createTasksHandlers(runtime) {
|
|
|
1444
1444
|
const result = requestResultChanges(store, evidenceRoot, {
|
|
1445
1445
|
run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
|
|
1446
1446
|
repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
|
|
1447
|
+
// A browser session is a person's proof; a bearer credential is a machine's.
|
|
1448
|
+
origin: who.via === "cookie" ? "person" : "machine",
|
|
1447
1449
|
note: body.get("note") ?? "", path: body.get("path") ?? "", line: body.get("line") ?? "", request: body.get("request"), image,
|
|
1448
1450
|
}, now);
|
|
1449
1451
|
if (!result.ok) {
|
|
@@ -1556,6 +1558,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1556
1558
|
const result = createResultRevision(store, evidenceRoot, {
|
|
1557
1559
|
run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
|
|
1558
1560
|
repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
|
|
1561
|
+
origin: who.via === "cookie" ? "person" : "machine",
|
|
1559
1562
|
}, now);
|
|
1560
1563
|
if (!result.ok)
|
|
1561
1564
|
return refuse(response, who, result.status, result.message, back);
|
package/dist/settings-proofs.js
CHANGED
|
@@ -35,11 +35,13 @@ export const PROOF_INVENTORY = [
|
|
|
35
35
|
{ id: "google", action: "Connect or disconnect Google", routes: ["/settings/google", "/settings/google/disconnect"], form: "google", field: null, proof: "none", ...NONE, role: "any", secrets: ["clientSecret"], terms: [] },
|
|
36
36
|
{ id: "appearance", action: "Change the theme or accent", routes: ["/settings/appearance"], form: "appearance", field: null, proof: "none", ...NONE, role: "any", secrets: [], terms: [] },
|
|
37
37
|
{ id: "chat-approval-confirm", action: "Review approving from chat", routes: ["/settings/chat-approval/confirm", "/settings/chat-approval/off"], form: "chatApproval", field: null, proof: "none", ...NONE, role: "approver", secrets: [], terms: [] },
|
|
38
|
+
{ id: "models-rules-off", action: "Turn an earned rule off, stop a trial, say Not now, or turn suggestions off", routes: ["/settings/models/turn-off", "/settings/models/not-now", "/settings/models/suggest-off", "/settings/models/stop"], form: "modelsRules", field: null, proof: "none", ...NONE, role: "operator", secrets: [], terms: [] },
|
|
38
39
|
...CHAT_APPS.map(([app, name, form]) => ({ id: `${app}-alerts`, action: `Send alerts to ${name}`, routes: [`/settings/${app}/alerts`], form, field: null, proof: "none", ...NONE, role: "approver", secrets: [], terms: [] })),
|
|
39
40
|
// Step-up: a password typed now, the Passwordless session, or a one-use re-check.
|
|
40
41
|
{ id: "permission-default", action: "Start new tasks with Full access", routes: ["/settings/permission-default"], form: "permissionDefault", field: "password", proof: "step-up", when: "only when widening to Full access", ...ACCEPT, role: "any", secrets: [], terms: [] },
|
|
41
42
|
{ id: "spend-budget", action: "Set a monthly limit", routes: ["/spend/budget"], form: "spendBudget", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: [] },
|
|
42
43
|
{ id: "models-agent", action: "Change which model does each job", routes: ["/settings/models/agent"], form: "modelsChange", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["scope", "digest"] },
|
|
44
|
+
{ id: "models-rules", action: "Accept a suggested model, try a cheaper one, keep a paused rule, or turn suggestions on", routes: ["/settings/models/accept", "/settings/models/keep", "/settings/models/suggest-on", "/settings/models/try"], form: "modelsRules", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["scope", "id", "rule"], note: "the rule door re-proves the session and its generation in the write" },
|
|
43
45
|
{ id: "models-apply", action: "Apply a model preset or switch", routes: ["/settings/models/apply"], form: "modelsChange", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["scope", "digest", "change"], note: "plus the previewed preset, or switch and to" },
|
|
44
46
|
...[
|
|
45
47
|
["policy", "Change the policy", "policy", "operator"],
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/spend-ui.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type ApprovalFacts } from "./design/kit.js";
|
|
|
10
10
|
import { type BudgetState, type SpendItem } from "./spend.js";
|
|
11
11
|
import type { SpendReport } from "./spend-report.js";
|
|
12
12
|
import type { BrowserLimits } from "./browser-workspace.js";
|
|
13
|
+
import { type SavingsReport } from "./route-savings.js";
|
|
13
14
|
/** The page's own layout: the limit rows and meters, the summary figures and the quiet bars. Colour from tokens only. */
|
|
14
15
|
export declare const SPEND_CSS: string;
|
|
15
16
|
/** What this page doesn't record, in plain words (docs/plans/spend-dashboard.md §2.2). */
|
|
@@ -53,6 +54,10 @@ export type SpendView = {
|
|
|
53
54
|
facts?: ApprovalFacts;
|
|
54
55
|
/** This month's name (YYYY-MM): the limits are this month's. */
|
|
55
56
|
current?: string;
|
|
57
|
+
/** Suggestions and rules (route-savings.ts): what each rule, trial and suggestion observed, or that it can't be read. */
|
|
58
|
+
savings?: SavingsReport | {
|
|
59
|
+
error: string;
|
|
60
|
+
} | null;
|
|
56
61
|
};
|
|
57
62
|
/** Dollars for the page: a positive amount under a cent reads "<$0.01", never a false $0. */
|
|
58
63
|
export declare const money: (microusd: number) => string;
|
|
@@ -62,6 +67,12 @@ export declare function spendHtml(view: SpendView, said: {
|
|
|
62
67
|
said?: string | null;
|
|
63
68
|
problem?: string | null;
|
|
64
69
|
}): Html;
|
|
70
|
+
/** Suggestions and rules (docs/plans/results-routing.md, slice 4): per project and size, each live or recently ended
|
|
71
|
+
* rule or trial, and each standing suggestion, with what it observed over matched 90-day windows. Labelled "observed,
|
|
72
|
+
* not a promise"; a median shows only from 5 API-priced results a side; unknown is "—", never $0. Read-only. */
|
|
73
|
+
export declare function savingsSection(savings: SavingsReport | {
|
|
74
|
+
error: string;
|
|
75
|
+
} | null): Html | "";
|
|
65
76
|
/** Checks the amount next to its field before any password is asked or the form leaves the page; the server checks
|
|
66
77
|
* it again. Remove skips it. */
|
|
67
78
|
export declare function spendScript(): string;
|
package/dist/spend-ui.js
CHANGED
|
@@ -10,6 +10,7 @@ import { approvalAsk, termsOf } from "./design/kit.js";
|
|
|
10
10
|
import { acts, btn, chip, disclose, dock, empty, field, input, note, notice, segLinks, settingsShell } from "./server/kit.js";
|
|
11
11
|
import { budgetLabel, usd } from "./spend.js";
|
|
12
12
|
import { runHref, taskHref } from "./server/http.js";
|
|
13
|
+
import { notEnoughWords, pricesWords, sideWords, SAVINGS_LABEL } from "./route-savings.js";
|
|
13
14
|
const projectName = (repo) => repo.split("/").filter(Boolean).pop() ?? repo;
|
|
14
15
|
/** The page's own layout: the limit rows and meters, the summary figures and the quiet bars. Colour from tokens only. */
|
|
15
16
|
export const SPEND_CSS = `.spend{display:grid;gap:24px;min-width:0}.spend-sec{display:grid;gap:0}.spend-sec .k-secthead{margin-bottom:0}.spend-sec .k-secthead small{font-weight:400;color:var(--so-muted);font-size:12.5px}` +
|
|
@@ -29,7 +30,14 @@ export const SPEND_CSS = `.spend{display:grid;gap:24px;min-width:0}.spend-sec{di
|
|
|
29
30
|
`.spend-win.warning .spend-meter>i,.spend-win.danger .spend-meter>i{background:var(--so-danger)}.spend .k-disclose{padding-top:4px}.spend .k-disclose[open]>summary{margin-bottom:8px}` +
|
|
30
31
|
`@media(max-width:760px){.spend-lim-h{grid-template-columns:minmax(0,1fr) auto}.spend-lim-a{grid-column:1/-1}.spend-lim-a .k-btn{width:100%}.spend-sum{grid-template-columns:minmax(0,1fr)}` +
|
|
31
32
|
`.spend-sum>div+div{border-left:0;border-top:1px solid var(--so-line);padding-left:0}.spend-bar{grid-template-columns:minmax(0,120px) minmax(0,1fr) 68px;gap:8px;font-size:12.5px}` +
|
|
32
|
-
`.spend-edit-row .k-field{flex:1 1 100%}.spend .k-btn.k-btn-primary.k-docked{display:none}}
|
|
33
|
+
`.spend-edit-row .k-field{flex:1 1 100%}.spend .k-btn.k-btn-primary.k-docked{display:none}}` +
|
|
34
|
+
// Suggestions and rules (results-routing slice 4): each rule's two sides, side by side; stacked on a phone.
|
|
35
|
+
`.spend-rr{display:grid;gap:10px;padding:14px 0;border-bottom:1px solid var(--so-line);min-width:0}.spend-rr-h{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;min-width:0}` +
|
|
36
|
+
`.spend-rr-t{font-weight:500;overflow-wrap:anywhere;min-width:0}.spend-rr-x{font-size:12.5px;color:var(--so-muted);overflow-wrap:anywhere}` +
|
|
37
|
+
`.spend-rr-g{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.spend-rr-s{display:grid;gap:4px;align-content:start;min-width:0;padding:12px;border:1px solid var(--so-line);border-radius:8px}` +
|
|
38
|
+
`.spend-rr-k{font-size:12px;color:var(--so-muted);overflow-wrap:anywhere}.spend-rr-v{font-size:18px;font-weight:600;font-variant-numeric:tabular-nums}.spend-rr-v small{font-size:12px;font-weight:400;color:var(--so-muted)}` +
|
|
39
|
+
`.spend-rr-p{display:grid;gap:2px;margin:0;padding:0;list-style:none;font-size:12.5px;color:var(--so-muted)}.spend-rr .k-note{margin:0}` +
|
|
40
|
+
`@media(max-width:760px){.spend-rr-g{grid-template-columns:minmax(0,1fr)}}`;
|
|
33
41
|
const ROLE_WORDS = {
|
|
34
42
|
builder: "Builds", repair: "Repairs", planner: "Plans", reviewer: "Reviews", scout: "Scouting",
|
|
35
43
|
lead: "Lead chat", chat: "Chat", subagent: "Subagents", flow: "Flows", evals: "Evals",
|
|
@@ -148,10 +156,49 @@ export function spendHtml(view, said) {
|
|
|
148
156
|
const docked = edit !== null && (rows.some(row => row.target === edit.target) || adding !== null)
|
|
149
157
|
? dock(btn("Save limit", { variant: "primary", type: "submit", attrs: { form: `limit-${edit.target.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").slice(0, 60) || "new"}`, name: "action", value: "save" } }))
|
|
150
158
|
: "";
|
|
151
|
-
const body = html `<div class="spend">${limits}${monthBar}${figures}${breakdowns}${windows}${notCounted}</div>${docked}`;
|
|
159
|
+
const body = html `<div class="spend">${limits}${monthBar}${figures}${breakdowns}${windows}${savingsSection(view.savings ?? null)}${notCounted}</div>${docked}`;
|
|
152
160
|
return settingsShell({ group: "spend", title: "Spend", lead: "What agents cost, by project and by model, with the limits that stop them. Dates are UTC months.", body,
|
|
153
161
|
flags: { spend: saved.some(one => one.budget !== null && limitNeedsYou(one.budget)) } });
|
|
154
162
|
}
|
|
163
|
+
const sizeWords = (size) => size === "small" ? "Small tasks" : "Routine tasks";
|
|
164
|
+
const dateWords = (iso) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" }).format(new Date(iso));
|
|
165
|
+
const STATUS_TONE = { active: "live", trialling: "live", paused: "needs" };
|
|
166
|
+
/** One side of a rule's comparison: its model and window, the median API dollars a task (— when hidden), and the
|
|
167
|
+
* completeness, plan usage, unknown cost and remediation counts, each on its own line. */
|
|
168
|
+
function savingsSide(label, side) {
|
|
169
|
+
const words = sideWords(side);
|
|
170
|
+
const cap = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
171
|
+
return html `<div class="spend-rr-s" role="group" aria-label="${label}: ${side.name}" data-side="${label.toLowerCase()}"><span class="spend-rr-k">${label} · ${side.name} · ${dateWords(side.from)} to ${dateWords(side.to)}</span>\
|
|
172
|
+
<span class="spend-rr-v" data-amount-kind="api" data-microusd="${side.medianApiMicrousd ?? ""}">${words.median} <small>median API dollars a task</small></span>\
|
|
173
|
+
<ul class="spend-rr-p"><li data-priced="${side.apiPriced}" data-results="${side.results}">${words.parts[0]}</li><li data-amount-kind="subscription">${cap(words.parts[1])}</li><li data-amount-kind="unknown">${cap(words.parts[2])}</li><li>${cap(words.parts[3])}</li></ul></div>`;
|
|
174
|
+
}
|
|
175
|
+
function savingsRow(row) {
|
|
176
|
+
const suggestion = row.kind === "suggestion";
|
|
177
|
+
const what = `${row.cheaper.name} instead of ${row.configured.name}${row.kind === "trial" ? " (a trial)" : suggestion ? " (suggested, not in use)" : ""}`;
|
|
178
|
+
const href = `/settings/models?scope=${encodeURIComponent(row.repo)}#models-suggested`;
|
|
179
|
+
return html `<article class="spend-rr" data-savings-row data-kind="${row.kind}" data-state="${row.state}" data-enough="${row.enough ? "yes" : "no"}">\
|
|
180
|
+
<div class="spend-rr-h"><span class="spend-rr-t">${row.project} · ${sizeWords(row.size)}</span>${chip(row.status, STATUS_TONE[row.state] ?? "")}</div><span class="spend-rr-x">${what}</span>\
|
|
181
|
+
<div class="spend-rr-g">${savingsSide(suggestion ? "Configured" : "Before", row.configured)}${savingsSide(suggestion ? "Suggested" : "After", row.cheaper)}</div>\
|
|
182
|
+
${row.enough ? "" : html `<p class="k-note" data-not-enough>${notEnoughWords(row)}</p>`}<p class="k-note" data-prices>${pricesWords(row)}</p>\
|
|
183
|
+
<span><a class="k-link" href="${href}">${suggestion ? "Review in Models settings" : "Open Models settings"}</a></span></article>`;
|
|
184
|
+
}
|
|
185
|
+
/** Suggestions and rules (docs/plans/results-routing.md, slice 4): per project and size, each live or recently ended
|
|
186
|
+
* rule or trial, and each standing suggestion, with what it observed over matched 90-day windows. Labelled "observed,
|
|
187
|
+
* not a promise"; a median shows only from 5 API-priced results a side; unknown is "—", never $0. Read-only. */
|
|
188
|
+
export function savingsSection(savings) {
|
|
189
|
+
if (savings === null)
|
|
190
|
+
return "";
|
|
191
|
+
const state = `${SAVINGS_LABEL} · 90 days a side`;
|
|
192
|
+
if ("error" in savings)
|
|
193
|
+
return sec("savings", "Suggestions and rules", state, notice("Suggestions and rules can't be read right now. Nothing else on this page is affected."), html ` data-savings="error"`);
|
|
194
|
+
if (savings.rows.length === 0) {
|
|
195
|
+
return sec("savings", "Suggestions and rules", state, empty({ title: "No suggestions or rules yet.",
|
|
196
|
+
text: "When a cheaper model does as well on a project's small or routine tasks, its suggestion shows here, and once it's in use, what it cost before and after." }), html ` data-savings="empty"`);
|
|
197
|
+
}
|
|
198
|
+
const stale = savings.staleSince === null ? "" : html `<p class="k-note" data-savings-stale>Some results were last checked on ${dateWords(savings.staleSince)}; they update while Toolroll runs.</p>`;
|
|
199
|
+
const lead = html `<p class="k-note">Median API dollars a task, including revision and repair runs. Plan usage and unknown cost are counted apart, never added in. A median shows once each side has ${savings.minPriced} results priced in API dollars.</p>`;
|
|
200
|
+
return sec("savings", "Suggestions and rules", state, html `${lead}${stale}${savings.rows.map(savingsRow)}`, html ` data-savings="rows"`);
|
|
201
|
+
}
|
|
155
202
|
/** Checks the amount next to its field before any password is asked or the form leaves the page; the server checks
|
|
156
203
|
* it again. Remove skips it. */
|
|
157
204
|
export function spendScript() {
|
package/dist/store.d.ts
CHANGED
|
@@ -234,7 +234,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
234
234
|
hash: string;
|
|
235
235
|
} | null;
|
|
236
236
|
};
|
|
237
|
-
export declare const SCHEMA_VERSION =
|
|
237
|
+
export declare const SCHEMA_VERSION = 148;
|
|
238
238
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
239
239
|
export declare const V118_MERGED_TABLES: Readonly<Record<string, CredentialKind>>;
|
|
240
240
|
/**
|
|
@@ -426,7 +426,38 @@ export declare const V147_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record
|
|
|
426
426
|
}>>>>;
|
|
427
427
|
/** The v147 migration's steps after the epoch, in order: a test seam (OpenOptions.migrationStep) may stop after each. */
|
|
428
428
|
export declare const V147_STEPS: readonly ["epoch", "local-mode", "password-set", "web-session", "console-instance"];
|
|
429
|
-
export type MigrationStep = (typeof V147_STEPS)[number];
|
|
429
|
+
export type MigrationStep = (typeof V147_STEPS)[number] | (typeof V148_STEPS)[number];
|
|
430
|
+
/**
|
|
431
|
+
* v148 (docs/plans/results-routing.md): every table the results-routing arc uses, landed at once because a current
|
|
432
|
+
* store runs no DDL. Slice 1 writes run_cohort, route_record and models_generation; route_rule, route_exposure* and
|
|
433
|
+
* route_trial_reservation are written from later slices. src/route-store.ts is the only writer of all of them (the
|
|
434
|
+
* migration aside), which release-auto.ts's routing scan (route-hooks.ts) holds a release to.
|
|
435
|
+
*
|
|
436
|
+
* - run_cohort: one row per code-writing run (builder, repair), written in the run's admission transaction beside its
|
|
437
|
+
* route stamp and never updated: the sealed facts its eligibility was decided from. cohort_size NULL = not eligible,
|
|
438
|
+
* with the reason in `ineligible`.
|
|
439
|
+
* - route_record: a task family's result record (keyed by its root task_ref), recomputed by the reconcile pass.
|
|
440
|
+
* `settled` = its verdict (state, reason, result, acceptance, decision time) is final; its spending and counts keep
|
|
441
|
+
* accumulating through later revision and repair runs until `closed` (the family is quiet, or 90 days after its
|
|
442
|
+
* first run: route-results.ts ACCUMULATE_FOR_MS).
|
|
443
|
+
* - route_rule: an earned rule or a trial, with its sealed grant, price snapshot and the configuration generations it
|
|
444
|
+
* depends on; one live winner per project and size (route_rule_one_winner).
|
|
445
|
+
* - route_exposure / route_exposure_event: one exposure per task and rule, its events append-only.
|
|
446
|
+
* - route_trial_reservation: a trial's place, reserved, then consumed (once, at admission) or released.
|
|
447
|
+
* - models_generation: a counter per (scope, phase), moved by every Models row writer in the same transaction.
|
|
448
|
+
*/
|
|
449
|
+
export declare const V148_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_cohort (\n run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,\n task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,\n root_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,\n repo TEXT,\n role TEXT NOT NULL CHECK (role IN ('builder','repair')),\n size TEXT CHECK (size IS NULL OR size IN ('small','medium','large')),\n risky INTEGER CHECK (risky IS NULL OR risky IN (0,1)),\n quality_mode TEXT NOT NULL CHECK (quality_mode IN ('default','strict')),\n evidence_demands INTEGER NOT NULL CHECK (evidence_demands IN (0,1)),\n publication TEXT NOT NULL CHECK (publication IN ('none','notify','automerge')),\n release INTEGER NOT NULL CHECK (release IN (0,1)),\n protected INTEGER NOT NULL CHECK (protected IN (0,1)),\n selection_json TEXT NOT NULL,\n route_digest TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n cohort_size TEXT CHECK (cohort_size IS NULL OR cohort_size IN ('small','medium')),\n ineligible TEXT,\n started_at TEXT NOT NULL,\n CHECK ((cohort_size IS NULL) = (ineligible IS NOT NULL))\n);\nCREATE INDEX IF NOT EXISTS run_cohort_by_task ON run_cohort (task_ref);\nCREATE INDEX IF NOT EXISTS run_cohort_by_root ON run_cohort (repo, root_ref);\nCREATE TABLE IF NOT EXISTS route_record (\n task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id) ON DELETE CASCADE,\n repo TEXT,\n cohort_size TEXT CHECK (cohort_size IS NULL OR cohort_size IN ('small','medium')),\n provider TEXT,\n model TEXT,\n state TEXT NOT NULL CHECK (state IN ('pending','provisional','mature','rejected','not-counted')),\n reason TEXT,\n result_run INTEGER REFERENCES run(id) ON DELETE SET NULL,\n first_started_at TEXT,\n accepted_at TEXT,\n decided_at TEXT,\n model_failures INTEGER NOT NULL DEFAULT 0 CHECK (model_failures >= 0),\n infra_failures INTEGER NOT NULL DEFAULT 0 CHECK (infra_failures >= 0),\n api_microusd INTEGER NOT NULL DEFAULT 0 CHECK (api_microusd >= 0),\n subscription_runs INTEGER NOT NULL DEFAULT 0 CHECK (subscription_runs >= 0),\n subscription_tokens INTEGER NOT NULL DEFAULT 0 CHECK (subscription_tokens >= 0),\n unknown_runs INTEGER NOT NULL DEFAULT 0 CHECK (unknown_runs >= 0),\n remediation_runs INTEGER NOT NULL DEFAULT 0 CHECK (remediation_runs >= 0),\n person_revisions INTEGER NOT NULL DEFAULT 0 CHECK (person_revisions >= 0),\n first_check_passed INTEGER CHECK (first_check_passed IS NULL OR first_check_passed IN (0,1)),\n agent_turns INTEGER NOT NULL DEFAULT 0 CHECK (agent_turns >= 0),\n turns_unknown_runs INTEGER NOT NULL DEFAULT 0 CHECK (turns_unknown_runs >= 0),\n provenance_json TEXT NOT NULL DEFAULT '[]',\n settled INTEGER NOT NULL DEFAULT 0 CHECK (settled IN (0,1)),\n closed INTEGER NOT NULL DEFAULT 0 CHECK (closed IN (0,1)),\n evaluated_at TEXT NOT NULL,\n CHECK ((state IN ('rejected','not-counted')) = (reason IS NOT NULL)),\n CHECK (closed = 0 OR settled = 1)\n);\nCREATE INDEX IF NOT EXISTS route_record_by_cohort ON route_record (repo, cohort_size, state, first_started_at);\nCREATE INDEX IF NOT EXISTS route_record_open ON route_record (evaluated_at) WHERE closed = 0;\nCREATE TABLE IF NOT EXISTS route_rule (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n repo TEXT NOT NULL,\n cohort_size TEXT NOT NULL CHECK (cohort_size IN ('small','medium')),\n kind TEXT NOT NULL CHECK (kind IN ('rule','trial')),\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n configured_provider TEXT NOT NULL,\n configured_model TEXT,\n state TEXT NOT NULL CHECK (state IN ('active','trialling','paused','switched-off','turned-off','expired','dropped','finished')),\n generation INTEGER NOT NULL DEFAULT 1 CHECK (generation >= 1),\n grant_json TEXT NOT NULL,\n prices_json TEXT NOT NULL,\n depends_json TEXT NOT NULL,\n trial_limit INTEGER CHECK (trial_limit IS NULL OR trial_limit >= 1),\n accepted_by TEXT NOT NULL,\n accepted_at TEXT NOT NULL,\n ended_at TEXT,\n ended_by TEXT,\n end_reason TEXT,\n CHECK ((kind = 'trial') = (trial_limit IS NOT NULL))\n);\nCREATE UNIQUE INDEX IF NOT EXISTS route_rule_one_winner ON route_rule (repo, cohort_size) WHERE state IN ('active','trialling','paused');\nCREATE TABLE IF NOT EXISTS route_exposure (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,\n rule INTEGER NOT NULL REFERENCES route_rule(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n closed_at TEXT,\n UNIQUE (task_ref, rule)\n);\nCREATE TABLE IF NOT EXISTS route_exposure_event (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n exposure INTEGER NOT NULL REFERENCES route_exposure(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('proposed','refiled','approved-as-proposed','approved-with-other','cancelled')),\n model TEXT,\n at TEXT NOT NULL,\n CHECK ((kind = 'approved-with-other') = (model IS NOT NULL))\n);\nCREATE INDEX IF NOT EXISTS route_exposure_event_by_exposure ON route_exposure_event (exposure, id);\nCREATE TABLE IF NOT EXISTS route_trial_reservation (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n rule INTEGER NOT NULL REFERENCES route_rule(id) ON DELETE CASCADE,\n task_ref INTEGER NOT NULL REFERENCES task_ref(id) ON DELETE CASCADE,\n scope_digest TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('reserved','consumed','released')),\n consumed_run INTEGER REFERENCES run(id) ON DELETE SET NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE (rule, task_ref),\n CHECK (state = 'consumed' OR consumed_run IS NULL)\n);\nCREATE TABLE IF NOT EXISTS models_generation (\n scope TEXT NOT NULL,\n phase TEXT NOT NULL,\n generation INTEGER NOT NULL CHECK (generation >= 1),\n changed_at TEXT NOT NULL,\n PRIMARY KEY (scope, phase)\n);\n";
|
|
450
|
+
/** The tables V148_SCHEMA adds, in the order a test drops them to make the v147 shape. */
|
|
451
|
+
export declare const V148_TABLES: readonly ["route_trial_reservation", "route_exposure_event", "route_exposure", "route_rule", "route_record", "run_cohort", "models_generation"];
|
|
452
|
+
/** v148: a run's execution class (saved runs read NULL: none kept, which the records count as the model's), a
|
|
453
|
+
* revision's origin (saved revisions read NULL: not recorded, never a person), and a project's "Suggest cheaper models"
|
|
454
|
+
* switch (saved projects read 1: on). */
|
|
455
|
+
export declare const V148_ADDED_COLUMNS: Readonly<Record<string, Readonly<Record<string, {
|
|
456
|
+
definition: string;
|
|
457
|
+
saved: string;
|
|
458
|
+
}>>>>;
|
|
459
|
+
/** The v148 migration's steps after v147's, in order (OpenOptions.migrationStep may stop after each). */
|
|
460
|
+
export declare const V148_STEPS: readonly ["results-tables", "exec-class", "revision-origin", "suggest-models"];
|
|
430
461
|
/**
|
|
431
462
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
432
463
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -8364,3 +8395,8 @@ export declare const LAPTOP_PORT_FACT = "laptop-port";
|
|
|
8364
8395
|
export declare function laptopPortOf(store: Store): number | null;
|
|
8365
8396
|
/** Record the laptop port, once (installation facts are never updated). */
|
|
8366
8397
|
export declare function recordLaptopPort(store: Store, port: number, now: Date): void;
|
|
8398
|
+
/** The Models rows of a project being deleted (project-delete.ts), through the same counter as every other Models
|
|
8399
|
+
* writer: each phase whose rows go moves its generation (pausing what depended on it), then the project's
|
|
8400
|
+
* results-routing rows go (route-store.ts forgetProjectRoutes). One of the routing scan's named phase_* writers
|
|
8401
|
+
* (route-hooks.ts). How many rows went. */
|
|
8402
|
+
export declare function forgetProjectModels(store: Store, repo: string, now: Date): number;
|