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/model-choices.js
CHANGED
|
@@ -14,6 +14,7 @@ import { INSTALLATION_SCOPE, resolvePhaseAgent, resolveRouteCandidates } from ".
|
|
|
14
14
|
import { seenModels } from "./model-catalog.js";
|
|
15
15
|
import { passwordlessOn } from "./passwordless.js";
|
|
16
16
|
import { exactModelId, recommendRoute, routeProblems } from "./phase-routing.js";
|
|
17
|
+
import { liveEarnedRules } from "./route-store.js";
|
|
17
18
|
import { isProviderId, validateSpec } from "./provider.js";
|
|
18
19
|
/** Every row the editor owns, in the order the page and the diff show them. Alternates (`--also`) and the strong
|
|
19
20
|
* repair row stay CLI-only; presets clear the strong repair row so repairs follow the builder. */
|
|
@@ -120,24 +121,29 @@ export const ROUTE_CASES = Object.freeze([
|
|
|
120
121
|
{ label: "large", size: "large", risky: false, strict: false }, { label: "risky", size: "small", risky: true, strict: false },
|
|
121
122
|
{ label: "strict", size: "medium", risky: false, strict: true },
|
|
122
123
|
]);
|
|
123
|
-
export function effectiveRoutes(store, repo) {
|
|
124
|
+
export function effectiveRoutes(store, repo, options = {}) {
|
|
124
125
|
const candidates = resolveRouteCandidates(store, repo);
|
|
125
126
|
if (!candidates.ok)
|
|
126
127
|
return { ok: false, problem: candidates.problem };
|
|
127
|
-
|
|
128
|
+
// A project's live earned rules route its eligible small and routine tasks (the one precedence order,
|
|
129
|
+
// phase-routing.ts selectBuildLeg); `rules: false` asks for the configured rows alone.
|
|
130
|
+
const rules = options.rules === false ? [] : options.rules ?? liveEarnedRules(store, repo);
|
|
131
|
+
return { ok: true, routes: routesOf(candidates.candidates, rules) };
|
|
128
132
|
}
|
|
129
|
-
/** Every size's and demand's route over these candidates
|
|
130
|
-
* its repairs with it, so that move can't break a repair; a tier
|
|
131
|
-
* primary here, and checked.) */
|
|
132
|
-
function routesOf(candidates) {
|
|
133
|
+
/** Every size's and demand's route over these candidates, with a project's earned rules where they apply. (A build
|
|
134
|
+
* moved to another provider for plan headroom takes its repairs with it, so that move can't break a repair; a tier
|
|
135
|
+
* whose only candidate is another provider's is its primary here, and checked.) */
|
|
136
|
+
function routesOf(candidates, rules = []) {
|
|
133
137
|
const routes = ROUTE_CASES.map(one => ({ label: one.label, route: recommendRoute({ qualityMode: one.strict ? "strict" : "default", evidence: [], publication: "none",
|
|
134
|
-
candidates, overrides: [], size: { size: one.size, risky: one.risky, source: "heuristic", reason: "" }
|
|
138
|
+
candidates, overrides: [], size: { size: one.size, risky: one.risky, source: "heuristic", reason: "" },
|
|
139
|
+
earned: rules.length === 0 ? null : { rules, release: false, protected: false } }) }));
|
|
135
140
|
return routes;
|
|
136
141
|
}
|
|
137
142
|
/** Which preset a scope's EFFECTIVE configuration matches (what each size would really run, inherited rows
|
|
138
143
|
* included), or null ("a custom set"). */
|
|
139
144
|
export function presetInUse(store, scope) {
|
|
140
|
-
|
|
145
|
+
// The configured rows, not a project's earned rules: a rule never edits the owner's Models rows.
|
|
146
|
+
const effective = effectiveRoutes(store, scope === INSTALLATION_SCOPE ? null : scope, { rules: false });
|
|
141
147
|
if (!effective.ok)
|
|
142
148
|
return null;
|
|
143
149
|
const legs = (label) => effective.routes.find(one => one.label === label).route.legs;
|
|
@@ -224,13 +230,22 @@ function projectsWithRows(store) {
|
|
|
224
230
|
* or a route of any size or demand (small, routine, large, risky, strict) whose repair can't resume its builder,
|
|
225
231
|
* inherited rows included. An invalid result is never saved.
|
|
226
232
|
*/
|
|
227
|
-
export function checkScope(store, repo) {
|
|
233
|
+
export function checkScope(store, repo, options = {}) {
|
|
228
234
|
const build = resolvePhaseAgent(store, "build", repo, {});
|
|
229
235
|
if (!build.ok)
|
|
230
236
|
return { kind: "invalid", problem: build.problem };
|
|
231
237
|
const plan = resolvePhaseAgent(store, "plan", repo, {});
|
|
232
238
|
if (!plan.ok)
|
|
233
239
|
return { kind: "invalid", problem: plan.problem };
|
|
240
|
+
// Every live earned rule's route is checked with the configured ones (results routing): a rule's builder must
|
|
241
|
+
// share any configured repair row's provider, since repairs resume the builder's session. `rules` is the set to
|
|
242
|
+
// check (an acceptance adds its candidate rule); by default the project's live rules.
|
|
243
|
+
const rules = options.rules ?? liveEarnedRules(store, repo);
|
|
244
|
+
const repairRow = (repo === null ? null : store.phaseConfig(repo, "repair")) ?? store.phaseConfig(INSTALLATION_SCOPE, "repair");
|
|
245
|
+
for (const rule of rules) {
|
|
246
|
+
if (repairRow !== null && repairRow.provider !== rule.provider)
|
|
247
|
+
return { kind: "invalid", problem: `Repairs must use the builder's provider (${rule.provider}): an earned rule builds ${rule.cohortSize === "small" ? "small" : "routine"} tasks on it.` };
|
|
248
|
+
}
|
|
234
249
|
// The reviewer is not checked here: an unset reviewer follows the planner, and routing deliberately never lets a
|
|
235
250
|
// reviewer setting stop a task from planning or building (agentconfig.ts resolveRouteCandidates; that leg never
|
|
236
251
|
// runs). So a Gemini planner with no reviewer of its own stays a valid configuration. A reviewer row written here is
|
|
@@ -244,12 +259,12 @@ export function checkScope(store, repo) {
|
|
|
244
259
|
const configured = resolveRouteCandidates(store, repo, { plan: standIn(plan.spec), build: standIn(build.spec) });
|
|
245
260
|
if (!configured.ok)
|
|
246
261
|
return { kind: /has no exact model/.test(configured.problem) ? "incomplete" : "invalid", problem: configured.problem };
|
|
247
|
-
for (const route of routesOf(configured.candidates)) {
|
|
262
|
+
for (const route of routesOf(configured.candidates, rules)) {
|
|
248
263
|
const problems = routeProblems(route.route);
|
|
249
264
|
if (problems.length > 0)
|
|
250
265
|
return { kind: "invalid", problem: `${route.label} tasks: ${problems[0]}` };
|
|
251
266
|
}
|
|
252
|
-
const effective = effectiveRoutes(store, repo);
|
|
267
|
+
const effective = effectiveRoutes(store, repo, { rules });
|
|
253
268
|
if (!effective.ok)
|
|
254
269
|
return { kind: /has no exact model/.test(effective.problem) ? "incomplete" : "invalid", problem: effective.problem };
|
|
255
270
|
return { kind: "ok" };
|
package/dist/models-cli.d.ts
CHANGED
|
@@ -90,7 +90,207 @@ export declare const MODELS_DESCRIPTORS: readonly [{
|
|
|
90
90
|
readonly takesValue: true;
|
|
91
91
|
readonly meaning: "local Toolroll database";
|
|
92
92
|
}];
|
|
93
|
+
}, {
|
|
94
|
+
readonly action: "results";
|
|
95
|
+
readonly synopsis: "each finished task's result record: which model built it, and whether it counts";
|
|
96
|
+
readonly mutation: "none";
|
|
97
|
+
readonly takesQuery: false;
|
|
98
|
+
readonly flags: readonly [{
|
|
99
|
+
readonly name: "repo";
|
|
100
|
+
readonly takesValue: true;
|
|
101
|
+
readonly meaning: "one project's records (default: every project you can open)";
|
|
102
|
+
}, {
|
|
103
|
+
readonly name: "json";
|
|
104
|
+
readonly takesValue: false;
|
|
105
|
+
readonly meaning: "versioned structured response";
|
|
106
|
+
}, {
|
|
107
|
+
readonly name: "db";
|
|
108
|
+
readonly takesValue: true;
|
|
109
|
+
readonly meaning: "local Toolroll database";
|
|
110
|
+
}];
|
|
111
|
+
}, {
|
|
112
|
+
readonly action: "suggestions";
|
|
113
|
+
readonly synopsis: "cheaper models a project's own results suggest, and its earned rules";
|
|
114
|
+
readonly mutation: "none";
|
|
115
|
+
readonly takesQuery: false;
|
|
116
|
+
readonly flags: readonly [{
|
|
117
|
+
readonly name: "repo";
|
|
118
|
+
readonly takesValue: true;
|
|
119
|
+
readonly meaning: "one project's records (default: every project you can open)";
|
|
120
|
+
}, {
|
|
121
|
+
readonly name: "json";
|
|
122
|
+
readonly takesValue: false;
|
|
123
|
+
readonly meaning: "versioned structured response";
|
|
124
|
+
}, {
|
|
125
|
+
readonly name: "db";
|
|
126
|
+
readonly takesValue: true;
|
|
127
|
+
readonly meaning: "local Toolroll database";
|
|
128
|
+
}];
|
|
129
|
+
}, {
|
|
130
|
+
readonly action: "accept";
|
|
131
|
+
readonly synopsis: "accept a suggestion as an earned rule, with its switch-back terms: models accept <id> --repo <p>";
|
|
132
|
+
readonly mutation: "unkeyed";
|
|
133
|
+
readonly takesQuery: true;
|
|
134
|
+
readonly positional: "the suggestion's id, as models suggestions lists it";
|
|
135
|
+
readonly flags: readonly [{
|
|
136
|
+
readonly name: "repo";
|
|
137
|
+
readonly takesValue: true;
|
|
138
|
+
readonly meaning: "the project (default: the one project you can open)";
|
|
139
|
+
}, {
|
|
140
|
+
readonly name: "as";
|
|
141
|
+
readonly takesValue: true;
|
|
142
|
+
readonly meaning: "your approver name";
|
|
143
|
+
}, {
|
|
144
|
+
readonly name: "token";
|
|
145
|
+
readonly takesValue: true;
|
|
146
|
+
readonly meaning: "your password (accepting and keeping take one typed now)";
|
|
147
|
+
}, {
|
|
148
|
+
readonly name: "json";
|
|
149
|
+
readonly takesValue: false;
|
|
150
|
+
readonly meaning: "versioned structured response";
|
|
151
|
+
}, {
|
|
152
|
+
readonly name: "db";
|
|
153
|
+
readonly takesValue: true;
|
|
154
|
+
readonly meaning: "local Toolroll database";
|
|
155
|
+
}];
|
|
156
|
+
}, {
|
|
157
|
+
readonly action: "dismiss";
|
|
158
|
+
readonly synopsis: "Not now: keep a suggestion quiet for 30 days: models dismiss <id> --repo <p>";
|
|
159
|
+
readonly mutation: "unkeyed";
|
|
160
|
+
readonly takesQuery: true;
|
|
161
|
+
readonly positional: "the suggestion's id, as models suggestions lists it";
|
|
162
|
+
readonly flags: readonly [{
|
|
163
|
+
readonly name: "repo";
|
|
164
|
+
readonly takesValue: true;
|
|
165
|
+
readonly meaning: "the project (default: the one project you can open)";
|
|
166
|
+
}, {
|
|
167
|
+
readonly name: "as";
|
|
168
|
+
readonly takesValue: true;
|
|
169
|
+
readonly meaning: "your approver name";
|
|
170
|
+
}, {
|
|
171
|
+
readonly name: "token";
|
|
172
|
+
readonly takesValue: true;
|
|
173
|
+
readonly meaning: "your password (accepting and keeping take one typed now)";
|
|
174
|
+
}, {
|
|
175
|
+
readonly name: "json";
|
|
176
|
+
readonly takesValue: false;
|
|
177
|
+
readonly meaning: "versioned structured response";
|
|
178
|
+
}, {
|
|
179
|
+
readonly name: "db";
|
|
180
|
+
readonly takesValue: true;
|
|
181
|
+
readonly meaning: "local Toolroll database";
|
|
182
|
+
}];
|
|
183
|
+
}, {
|
|
184
|
+
readonly action: "turn-off";
|
|
185
|
+
readonly synopsis: "turn an earned rule off, or drop a paused one: models turn-off <rule>";
|
|
186
|
+
readonly mutation: "unkeyed";
|
|
187
|
+
readonly takesQuery: true;
|
|
188
|
+
readonly positional: "the rule's number, as models suggestions lists it";
|
|
189
|
+
readonly flags: readonly [{
|
|
190
|
+
readonly name: "repo";
|
|
191
|
+
readonly takesValue: true;
|
|
192
|
+
readonly meaning: "the project (default: the one project you can open)";
|
|
193
|
+
}, {
|
|
194
|
+
readonly name: "as";
|
|
195
|
+
readonly takesValue: true;
|
|
196
|
+
readonly meaning: "your approver name";
|
|
197
|
+
}, {
|
|
198
|
+
readonly name: "token";
|
|
199
|
+
readonly takesValue: true;
|
|
200
|
+
readonly meaning: "your password (accepting and keeping take one typed now)";
|
|
201
|
+
}, {
|
|
202
|
+
readonly name: "json";
|
|
203
|
+
readonly takesValue: false;
|
|
204
|
+
readonly meaning: "versioned structured response";
|
|
205
|
+
}, {
|
|
206
|
+
readonly name: "db";
|
|
207
|
+
readonly takesValue: true;
|
|
208
|
+
readonly meaning: "local Toolroll database";
|
|
209
|
+
}];
|
|
210
|
+
}, {
|
|
211
|
+
readonly action: "keep";
|
|
212
|
+
readonly synopsis: "keep a paused earned rule: models keep <rule>";
|
|
213
|
+
readonly mutation: "unkeyed";
|
|
214
|
+
readonly takesQuery: true;
|
|
215
|
+
readonly positional: "the rule's number, as models suggestions lists it";
|
|
216
|
+
readonly flags: readonly [{
|
|
217
|
+
readonly name: "repo";
|
|
218
|
+
readonly takesValue: true;
|
|
219
|
+
readonly meaning: "the project (default: the one project you can open)";
|
|
220
|
+
}, {
|
|
221
|
+
readonly name: "as";
|
|
222
|
+
readonly takesValue: true;
|
|
223
|
+
readonly meaning: "your approver name";
|
|
224
|
+
}, {
|
|
225
|
+
readonly name: "token";
|
|
226
|
+
readonly takesValue: true;
|
|
227
|
+
readonly meaning: "your password (accepting and keeping take one typed now)";
|
|
228
|
+
}, {
|
|
229
|
+
readonly name: "json";
|
|
230
|
+
readonly takesValue: false;
|
|
231
|
+
readonly meaning: "versioned structured response";
|
|
232
|
+
}, {
|
|
233
|
+
readonly name: "db";
|
|
234
|
+
readonly takesValue: true;
|
|
235
|
+
readonly meaning: "local Toolroll database";
|
|
236
|
+
}];
|
|
237
|
+
}, {
|
|
238
|
+
readonly action: "try";
|
|
239
|
+
readonly synopsis: "try a cheaper model on the next 10 small or routine tasks, each approved by a person: models try <id> --repo <p>";
|
|
240
|
+
readonly mutation: "unkeyed";
|
|
241
|
+
readonly takesQuery: true;
|
|
242
|
+
readonly positional: "the trial offer's id, as models suggestions lists it";
|
|
243
|
+
readonly flags: readonly [{
|
|
244
|
+
readonly name: "repo";
|
|
245
|
+
readonly takesValue: true;
|
|
246
|
+
readonly meaning: "the project (default: the one project you can open)";
|
|
247
|
+
}, {
|
|
248
|
+
readonly name: "as";
|
|
249
|
+
readonly takesValue: true;
|
|
250
|
+
readonly meaning: "your approver name";
|
|
251
|
+
}, {
|
|
252
|
+
readonly name: "token";
|
|
253
|
+
readonly takesValue: true;
|
|
254
|
+
readonly meaning: "your password (accepting and keeping take one typed now)";
|
|
255
|
+
}, {
|
|
256
|
+
readonly name: "json";
|
|
257
|
+
readonly takesValue: false;
|
|
258
|
+
readonly meaning: "versioned structured response";
|
|
259
|
+
}, {
|
|
260
|
+
readonly name: "db";
|
|
261
|
+
readonly takesValue: true;
|
|
262
|
+
readonly meaning: "local Toolroll database";
|
|
263
|
+
}];
|
|
264
|
+
}, {
|
|
265
|
+
readonly action: "stop";
|
|
266
|
+
readonly synopsis: "stop a trial; tasks already approved on it keep their model: models stop <trial>";
|
|
267
|
+
readonly mutation: "unkeyed";
|
|
268
|
+
readonly takesQuery: true;
|
|
269
|
+
readonly positional: "the trial's number, as models suggestions lists it";
|
|
270
|
+
readonly flags: readonly [{
|
|
271
|
+
readonly name: "repo";
|
|
272
|
+
readonly takesValue: true;
|
|
273
|
+
readonly meaning: "the project (default: the one project you can open)";
|
|
274
|
+
}, {
|
|
275
|
+
readonly name: "as";
|
|
276
|
+
readonly takesValue: true;
|
|
277
|
+
readonly meaning: "your approver name";
|
|
278
|
+
}, {
|
|
279
|
+
readonly name: "token";
|
|
280
|
+
readonly takesValue: true;
|
|
281
|
+
readonly meaning: "your password (accepting and keeping take one typed now)";
|
|
282
|
+
}, {
|
|
283
|
+
readonly name: "json";
|
|
284
|
+
readonly takesValue: false;
|
|
285
|
+
readonly meaning: "versioned structured response";
|
|
286
|
+
}, {
|
|
287
|
+
readonly name: "db";
|
|
288
|
+
readonly takesValue: true;
|
|
289
|
+
readonly meaning: "local Toolroll database";
|
|
290
|
+
}];
|
|
93
291
|
}];
|
|
292
|
+
/** `proof`: the person who proved themselves for this command — at the credential generation read before the proof,
|
|
293
|
+
* and how — or null. The rule doors re-prove it inside their own transaction. */
|
|
94
294
|
export type ModelsCliContext = {
|
|
95
295
|
store: Store;
|
|
96
296
|
write: (line: string) => void;
|
|
@@ -98,5 +298,10 @@ export type ModelsCliContext = {
|
|
|
98
298
|
now: Date;
|
|
99
299
|
actor: string | null;
|
|
100
300
|
seams?: CatalogSeams;
|
|
301
|
+
proof?: {
|
|
302
|
+
actor: string;
|
|
303
|
+
generation: number;
|
|
304
|
+
method: string;
|
|
305
|
+
} | null;
|
|
101
306
|
};
|
|
102
307
|
export declare function runModelsCommand(positional: readonly string[], options: Map<string, string | true>, context: ModelsCliContext): Promise<number>;
|
package/dist/models-cli.js
CHANGED
|
@@ -3,17 +3,44 @@
|
|
|
3
3
|
import { envelopeJson } from "./envelope.js";
|
|
4
4
|
import { checkModels, isNewModel, modelOptions, priceWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "./model-catalog.js";
|
|
5
5
|
import { isProviderId } from "./provider.js";
|
|
6
|
+
import { canonicalProject } from "./project.js";
|
|
7
|
+
import { resultRecords } from "./route-results.js";
|
|
8
|
+
import { acceptSuggestion, dismissSuggestion, grantWords, keepRule, ruleById, ruleWords, rulesOf, startTrial, stopTrial, suggestionsFor, suggestionWords, trialOffersFor, trialOfferWords, trialsOf, trialWords, turnOffRule } from "./route-rules.js";
|
|
9
|
+
import { trialPlacesOf } from "./route-store.js";
|
|
10
|
+
import { currentLocalOwner } from "./local-owner.js";
|
|
11
|
+
import { REASON_WORDS } from "./contracts/route-results.js";
|
|
12
|
+
import { resolve } from "node:path";
|
|
6
13
|
const flags = [
|
|
7
14
|
{ name: "provider", takesValue: true, meaning: "claude, codex, gemini or openrouter, for models list" },
|
|
8
15
|
{ name: "json", takesValue: false, meaning: "versioned structured response" },
|
|
9
16
|
{ name: "db", takesValue: true, meaning: "local Toolroll database" },
|
|
10
17
|
];
|
|
18
|
+
const resultFlags = [
|
|
19
|
+
{ name: "repo", takesValue: true, meaning: "one project's records (default: every project you can open)" },
|
|
20
|
+
{ name: "json", takesValue: false, meaning: "versioned structured response" },
|
|
21
|
+
{ name: "db", takesValue: true, meaning: "local Toolroll database" },
|
|
22
|
+
];
|
|
23
|
+
const ruleFlags = [
|
|
24
|
+
{ name: "repo", takesValue: true, meaning: "the project (default: the one project you can open)" },
|
|
25
|
+
{ name: "as", takesValue: true, meaning: "your approver name" },
|
|
26
|
+
{ name: "token", takesValue: true, meaning: "your password (accepting and keeping take one typed now)" },
|
|
27
|
+
{ name: "json", takesValue: false, meaning: "versioned structured response" },
|
|
28
|
+
{ name: "db", takesValue: true, meaning: "local Toolroll database" },
|
|
29
|
+
];
|
|
11
30
|
export const MODELS_DESCRIPTORS = [
|
|
12
31
|
{ action: "status", synopsis: "installed CLI versions, available updates, new models and the automatic check", mutation: "none", takesQuery: false, flags },
|
|
13
32
|
{ action: "list", synopsis: "models a provider can run, newest first, with prices (--provider claude|codex|gemini|openrouter)", mutation: "none", takesQuery: false, flags },
|
|
14
33
|
{ action: "check", synopsis: "fetch the public model lists and check CLI versions now", mutation: "unkeyed", takesQuery: false, flags },
|
|
15
34
|
{ action: "update", synopsis: "update one CLI while no work is running: models update claude|codex|gemini", mutation: "unkeyed", takesQuery: true, flags },
|
|
16
35
|
{ action: "watch", synopsis: "turn the 6-hourly check and its messages on or off: models watch on|off", mutation: "unkeyed", takesQuery: true, flags },
|
|
36
|
+
{ action: "results", synopsis: "each finished task's result record: which model built it, and whether it counts", mutation: "none", takesQuery: false, flags: resultFlags },
|
|
37
|
+
{ action: "suggestions", synopsis: "cheaper models a project's own results suggest, and its earned rules", mutation: "none", takesQuery: false, flags: resultFlags },
|
|
38
|
+
{ action: "accept", synopsis: "accept a suggestion as an earned rule, with its switch-back terms: models accept <id> --repo <p>", mutation: "unkeyed", takesQuery: true, positional: "the suggestion's id, as models suggestions lists it", flags: ruleFlags },
|
|
39
|
+
{ action: "dismiss", synopsis: "Not now: keep a suggestion quiet for 30 days: models dismiss <id> --repo <p>", mutation: "unkeyed", takesQuery: true, positional: "the suggestion's id, as models suggestions lists it", flags: ruleFlags },
|
|
40
|
+
{ action: "turn-off", synopsis: "turn an earned rule off, or drop a paused one: models turn-off <rule>", mutation: "unkeyed", takesQuery: true, positional: "the rule's number, as models suggestions lists it", flags: ruleFlags },
|
|
41
|
+
{ action: "keep", synopsis: "keep a paused earned rule: models keep <rule>", mutation: "unkeyed", takesQuery: true, positional: "the rule's number, as models suggestions lists it", flags: ruleFlags },
|
|
42
|
+
{ action: "try", synopsis: "try a cheaper model on the next 10 small or routine tasks, each approved by a person: models try <id> --repo <p>", mutation: "unkeyed", takesQuery: true, positional: "the trial offer's id, as models suggestions lists it", flags: ruleFlags },
|
|
43
|
+
{ action: "stop", synopsis: "stop a trial; tasks already approved on it keep their model: models stop <trial>", mutation: "unkeyed", takesQuery: true, positional: "the trial's number, as models suggestions lists it", flags: ruleFlags },
|
|
17
44
|
];
|
|
18
45
|
export async function runModelsCommand(positional, options, context) {
|
|
19
46
|
const action = positional[0] ?? "status";
|
|
@@ -21,10 +48,20 @@ export async function runModelsCommand(positional, options, context) {
|
|
|
21
48
|
const fail = (reason, message) => { context.write(context.json ? envelopeJson({ ok: false, command, reason, message }) : message); return reason === "usage" ? 2 : 1; };
|
|
22
49
|
const ok = (result, lines) => { context.write(context.json ? envelopeJson({ ok: true, command, ...result }) : lines.join("\n")); return 0; };
|
|
23
50
|
if (!MODELS_DESCRIPTORS.some(one => one.action === action))
|
|
24
|
-
return fail("usage", "Use models status | list --provider <p> | check | update <claude|codex|gemini> | watch on|off
|
|
25
|
-
|
|
51
|
+
return fail("usage", "Use models status | list --provider <p> | check | update <claude|codex|gemini> | watch on|off | results | suggestions | accept <id> | dismiss <id> | turn-off <rule> | keep <rule> | try <id> | stop <trial>.");
|
|
52
|
+
// This computer's local owner (local mode, through the terminal UI) may turn a rule off and say Not now: the door
|
|
53
|
+
// re-checks that its in-flight act is exactly that one.
|
|
54
|
+
const local = currentLocalOwner();
|
|
55
|
+
if (context.actor === null && !(local !== null && (action === "turn-off" || action === "dismiss" || action === "suggestions" || action === "stop")))
|
|
26
56
|
return fail("unauthenticated", "Sign in first: the remembered local login drives models commands.");
|
|
27
57
|
const { store, now } = context;
|
|
58
|
+
if (action === "results")
|
|
59
|
+
return modelsResults(options, context, ok, fail);
|
|
60
|
+
if (action === "suggestions" || action === "accept" || action === "dismiss" || action === "turn-off" || action === "keep" || action === "try" || action === "stop")
|
|
61
|
+
return modelsRules(action, positional, options, { ...context, actor: context.actor ?? local.account }, ok, fail, local !== null && context.proof == null);
|
|
62
|
+
const actor = context.actor;
|
|
63
|
+
if (actor === null)
|
|
64
|
+
return fail("unauthenticated", "Sign in first: the remembered local login drives models commands.");
|
|
28
65
|
if (action === "status") {
|
|
29
66
|
const runtimes = runtimeStates(store), watch = watchState(store);
|
|
30
67
|
const fresh = ["claude", "codex", "gemini"].flatMap(source => seenModels(store, source)).filter(model => isNewModel(model, now));
|
|
@@ -55,12 +92,114 @@ export async function runModelsCommand(positional, options, context) {
|
|
|
55
92
|
const tool = positional[1] ?? "";
|
|
56
93
|
if (!(tool in RUNTIME_TOOLS))
|
|
57
94
|
return fail("usage", "Name one CLI: models update claude|codex|gemini.");
|
|
58
|
-
const updated = await updateRuntime(store, tool,
|
|
95
|
+
const updated = await updateRuntime(store, tool, actor, now, context.seams);
|
|
59
96
|
return updated.ok ? ok({ tool, message: updated.message }, [updated.message]) : fail("refused", updated.message);
|
|
60
97
|
}
|
|
61
98
|
const wanted = positional[1];
|
|
62
99
|
if (wanted !== "on" && wanted !== "off")
|
|
63
100
|
return fail("usage", "Use models watch on or models watch off.");
|
|
64
|
-
setWatch(store, wanted === "on",
|
|
101
|
+
setWatch(store, wanted === "on", actor, now);
|
|
65
102
|
return ok({ enabled: wanted === "on" }, [wanted === "on" ? "Automatic checks are on: every 6 hours, with a message for each new model or CLI update." : "Automatic checks are off."]);
|
|
66
103
|
}
|
|
104
|
+
/** `models results [--repo <p>]`: the saved result records of the projects this person can open, newest first — the
|
|
105
|
+
* same records the task page's "Result record" line reads. Read-only. */
|
|
106
|
+
function modelsResults(options, context, ok, fail) {
|
|
107
|
+
const { store } = context;
|
|
108
|
+
for (const key of options.keys())
|
|
109
|
+
if (!["repo", "json", "db", "as", "token", "token-file", "token-env"].includes(key))
|
|
110
|
+
return fail("usage", `--${key} is not a models results option.`);
|
|
111
|
+
const open = store.knownRepos().filter(repo => store.accountCanAccess(context.actor, repo));
|
|
112
|
+
const asked = options.get("repo");
|
|
113
|
+
if (asked === true)
|
|
114
|
+
return fail("usage", "--repo takes a project path.");
|
|
115
|
+
const wanted = asked === undefined ? null : canonicalProject(asked) ?? resolve(asked);
|
|
116
|
+
if (wanted !== null && !open.includes(wanted))
|
|
117
|
+
return fail("refused", "That project isn't one you can open here.");
|
|
118
|
+
const records = resultRecords(store, wanted === null ? open : [wanted]);
|
|
119
|
+
const counted = (state) => records.filter(one => one.state === state).length;
|
|
120
|
+
const summary = `${records.length} record${records.length === 1 ? "" : "s"}: ${counted("mature")} counted, ${counted("rejected")} counted against, ${counted("provisional")} accepted and waiting, ${counted("pending")} pending, ${counted("not-counted")} not counted`;
|
|
121
|
+
return ok({ records, reasons: REASON_WORDS }, records.length === 0
|
|
122
|
+
? ["No result records yet. A record starts when a task's first build runs."]
|
|
123
|
+
: [summary, ...records.map(one => `${one.task} · ${one.model ?? "unknown model"}${one.size === null ? "" : ` · ${one.size}`} · ${one.words}`)]);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Earned rules on the command line (docs/plans/results-routing.md, slice 2), the same doors Settings → Models uses:
|
|
127
|
+
* `suggestions` reads; `accept` and `keep` take the person's own proof, re-proved inside the write (a lead token or a
|
|
128
|
+
* remote token is refused there); `turn-off` and `dismiss` take the person, or this computer's local owner inside
|
|
129
|
+
* exactly that act.
|
|
130
|
+
*/
|
|
131
|
+
function modelsRules(action, positional, options, context, ok, fail, asLocal) {
|
|
132
|
+
const { store, now } = context;
|
|
133
|
+
const allowed = action === "suggestions" ? ["repo", "json", "db", "as", "token", "token-file", "token-env"] : ["repo", "json", "db", "as", "token", "token-file", "token-env"];
|
|
134
|
+
for (const key of options.keys())
|
|
135
|
+
if (!allowed.includes(key))
|
|
136
|
+
return fail("usage", `--${key} is not a models ${action} option.`);
|
|
137
|
+
const open = store.knownRepos().filter(repo => store.accountCanAccess(context.actor, repo));
|
|
138
|
+
const asked = options.get("repo");
|
|
139
|
+
if (asked === true)
|
|
140
|
+
return fail("usage", "--repo takes a project path.");
|
|
141
|
+
const wanted = asked === undefined ? null : canonicalProject(asked) ?? resolve(asked);
|
|
142
|
+
if (wanted !== null && !open.includes(wanted))
|
|
143
|
+
return fail("refused", "That project isn't one you can open here.");
|
|
144
|
+
if (action === "suggestions") {
|
|
145
|
+
const repos = wanted === null ? open : [wanted];
|
|
146
|
+
const suggestions = repos.flatMap(repo => suggestionsFor(store, repo, now));
|
|
147
|
+
const rules = repos.flatMap(repo => rulesOf(store, repo, now));
|
|
148
|
+
const trialOffers = repos.flatMap(repo => trialOffersFor(store, repo, now));
|
|
149
|
+
const trials = repos.flatMap(repo => trialsOf(store, repo, now));
|
|
150
|
+
return ok({
|
|
151
|
+
suggestions: suggestions.map(one => ({ id: one.id, repo: one.repo, size: one.cohortSize, model: one.model, name: one.name, configured: one.configured,
|
|
152
|
+
evidence: one.evidence, configuredEvidence: one.configuredEvidence, prices: one.prices, terms: grantWords(one.grant), words: suggestionWords(one) })),
|
|
153
|
+
rules: rules.map(one => ({ id: one.id, repo: one.repo, size: one.cohortSize, model: one.model, configured: one.configured, state: one.state, acceptedAt: one.acceptedAt,
|
|
154
|
+
acceptedBy: one.acceptedBy, expires: one.grant?.expires ?? null, endedAt: one.endedAt, endReason: one.endReason, words: ruleWords(store, one) })),
|
|
155
|
+
// Slice 3: cheaper models to try where results don't yet suggest one, and the trials in use.
|
|
156
|
+
trialOffers: trialOffers.map(one => ({ id: one.id, repo: one.repo, size: one.cohortSize, model: one.model, name: one.name, configured: one.configured,
|
|
157
|
+
evidence: one.evidence, configuredEvidence: one.configuredEvidence, prices: one.prices, places: one.places, words: trialOfferWords(one) })),
|
|
158
|
+
trials: trials.map(one => ({ id: one.id, repo: one.repo, size: one.cohortSize, model: one.model, configured: one.configured, state: one.state, startedAt: one.acceptedAt,
|
|
159
|
+
startedBy: one.acceptedBy, endedAt: one.endedAt, endReason: one.endReason, places: trialPlacesOf(store, one.id), words: trialWords(store, one) })),
|
|
160
|
+
}, suggestions.length === 0 && rules.length === 0 && trialOffers.length === 0 && trials.length === 0 ? ["No suggestions yet. A cheaper model is suggested once its results here match the configured one's."] : [
|
|
161
|
+
...suggestions.flatMap(one => [`Suggested · ${one.repo} · ${suggestionWords(one)}`, ` accept: toolroll models accept ${one.id} --repo ${one.repo}`, ` not now: toolroll models dismiss ${one.id} --repo ${one.repo}`]),
|
|
162
|
+
...rules.map(one => `Rule ${one.id} · ${one.repo} · ${ruleWords(store, one)}`),
|
|
163
|
+
...trialOffers.flatMap(one => [`Try · ${one.repo} · ${trialOfferWords(one)}`, ` try: toolroll models try ${one.id} --repo ${one.repo}`, ` not now: toolroll models dismiss ${one.id} --repo ${one.repo}`]),
|
|
164
|
+
...trials.map(one => `Trial ${one.id} · ${one.repo} · ${trialWords(store, one)}`),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
const target = positional[1];
|
|
168
|
+
if (target === undefined || positional.length !== 2)
|
|
169
|
+
return fail("usage", action === "accept" || action === "dismiss" || action === "try" ? `Name one offer: models ${action} <id> --repo <p>.` : action === "stop" ? "Name one trial: models stop <trial>." : `Name one rule: models ${action} <rule>.`);
|
|
170
|
+
const proof = context.proof ?? null;
|
|
171
|
+
const door = proof !== null ? { kind: "person", person: { ...proof, live: () => true } } : asLocal ? { kind: "local-owner", actor: context.actor } : null;
|
|
172
|
+
if (door === null)
|
|
173
|
+
return fail("unauthenticated", "This takes your own sign-in: --as <you> --token <password>.");
|
|
174
|
+
const said = (result, payload, line) => result.ok ? ok(payload, [line]) : fail("refused", result.problem);
|
|
175
|
+
if (action === "accept" || action === "dismiss" || action === "try") {
|
|
176
|
+
const repo = wanted ?? (open.length === 1 ? open[0] : null);
|
|
177
|
+
if (repo === null)
|
|
178
|
+
return fail("usage", "Name the project: --repo <p>.");
|
|
179
|
+
if (action === "try") {
|
|
180
|
+
const started = startTrial(store, { repo, id: target }, door, now);
|
|
181
|
+
return started.ok ? ok({ trial: started.trial, repo, model: started.offer.model, size: started.offer.cohortSize, places: started.offer.places }, [`Trial ${started.trial}: the next ${started.offer.places} ${started.offer.cohortSize === "small" ? "small" : "routine"} tasks try ${started.offer.name}. Each waits for a person's approval.`]) : fail("refused", started.problem);
|
|
182
|
+
}
|
|
183
|
+
if (action === "accept") {
|
|
184
|
+
const accepted = acceptSuggestion(store, { repo, id: target }, door, now);
|
|
185
|
+
return accepted.ok ? ok({ rule: accepted.rule, repo, model: accepted.suggestion.model, size: accepted.suggestion.cohortSize, terms: grantWords(accepted.suggestion.grant) }, [`Rule ${accepted.rule}: ${suggestionWords(accepted.suggestion)}`, ...grantWords(accepted.suggestion.grant).map(line => ` ${line}`)]) : fail("refused", accepted.problem);
|
|
186
|
+
}
|
|
187
|
+
return said(dismissSuggestion(store, { repo, id: target }, door, now), { repo, id: target }, "Not now: this suggestion stays quiet for 30 days.");
|
|
188
|
+
}
|
|
189
|
+
const id = /^[1-9][0-9]{0,9}$/.test(target) ? Number(target) : null;
|
|
190
|
+
const rule = id === null ? null : ruleById(store, id);
|
|
191
|
+
if (action === "stop") {
|
|
192
|
+
if (rule === null || rule.kind !== "trial" || !open.includes(rule.repo))
|
|
193
|
+
return fail("refused", "That trial isn't one you can see here.");
|
|
194
|
+
const stopped = stopTrial(store, rule.id, door, now);
|
|
195
|
+
return stopped.ok ? ok({ trial: stopped.trial.id, state: stopped.trial.state }, [`Trial ${stopped.trial.id} stopped. Tasks already approved on it keep their model.`]) : fail("refused", stopped.problem);
|
|
196
|
+
}
|
|
197
|
+
if (rule === null || rule.kind !== "rule" || !open.includes(rule.repo))
|
|
198
|
+
return fail("refused", "That rule isn't one you can see here.");
|
|
199
|
+
if (action === "turn-off") {
|
|
200
|
+
const turned = turnOffRule(store, rule.id, door, now);
|
|
201
|
+
return turned.ok ? ok({ rule: turned.rule.id, state: turned.rule.state }, [`Rule ${turned.rule.id} is off. Tasks already approved keep the model they were approved with.`]) : fail("refused", turned.problem);
|
|
202
|
+
}
|
|
203
|
+
const kept = keepRule(store, rule.id, door, now);
|
|
204
|
+
return kept.ok ? ok({ rule: kept.rule.id, state: kept.rule.state }, [`Rule ${kept.rule.id} is on again: ${ruleWords(store, kept.rule)}`]) : fail("refused", kept.problem);
|
|
205
|
+
}
|
package/dist/models-ui.d.ts
CHANGED
|
@@ -98,6 +98,63 @@ export type ModelsView = {
|
|
|
98
98
|
}[];
|
|
99
99
|
/** What this request's approval shows (approvalFactsNow): a step-up asks for the password unless Passwordless is on. */
|
|
100
100
|
facts?: ApprovalFacts;
|
|
101
|
+
/** Results routing (slice 2): cheaper models this project's own results suggest, its earned rules, its switch. */
|
|
102
|
+
suggested?: SuggestedView;
|
|
103
|
+
};
|
|
104
|
+
/** The Suggested group: offers, a reviewed offer, the project's rules, and its "Suggest cheaper models" switch. */
|
|
105
|
+
export type SuggestedView = {
|
|
106
|
+
/** The project shown, or null on the installation scope (offers from every project, each reviewed on its own). */
|
|
107
|
+
repo: string | null;
|
|
108
|
+
offers: {
|
|
109
|
+
id: string;
|
|
110
|
+
repo: string;
|
|
111
|
+
project: string;
|
|
112
|
+
title: string;
|
|
113
|
+
words: string;
|
|
114
|
+
}[];
|
|
115
|
+
/** "Review change": the evidence, the switch-back terms and the step-up save; `stale` when the offer reviewed is
|
|
116
|
+
* no longer the one on offer. */
|
|
117
|
+
review: {
|
|
118
|
+
id: string;
|
|
119
|
+
repo: string;
|
|
120
|
+
title: string;
|
|
121
|
+
evidence: string[];
|
|
122
|
+
prices: string;
|
|
123
|
+
terms: string[];
|
|
124
|
+
save: string;
|
|
125
|
+
} | {
|
|
126
|
+
stale: true;
|
|
127
|
+
repo: string;
|
|
128
|
+
} | null;
|
|
129
|
+
rules: {
|
|
130
|
+
id: number;
|
|
131
|
+
repo: string;
|
|
132
|
+
project: string;
|
|
133
|
+
title: string;
|
|
134
|
+
words: string;
|
|
135
|
+
state: "active" | "paused" | "ended";
|
|
136
|
+
}[];
|
|
137
|
+
/** The project's switch (null on the installation scope). */
|
|
138
|
+
suggest: {
|
|
139
|
+
on: boolean;
|
|
140
|
+
} | null;
|
|
141
|
+
/** Slice 3: cheaper models to try on the next few tasks (a step-up starts one), and the trials in use (Stop). */
|
|
142
|
+
trialOffers?: {
|
|
143
|
+
id: string;
|
|
144
|
+
repo: string;
|
|
145
|
+
project: string;
|
|
146
|
+
title: string;
|
|
147
|
+
words: string;
|
|
148
|
+
save: string;
|
|
149
|
+
}[];
|
|
150
|
+
trials?: {
|
|
151
|
+
id: number;
|
|
152
|
+
repo: string;
|
|
153
|
+
project: string;
|
|
154
|
+
title: string;
|
|
155
|
+
words: string;
|
|
156
|
+
live: boolean;
|
|
157
|
+
}[];
|
|
101
158
|
};
|
|
102
159
|
export declare function modelsHtml(view: ModelsView): Html;
|
|
103
160
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
package/dist/models-ui.js
CHANGED
|
@@ -59,7 +59,7 @@ ${formRow("Automatic checks", `${view.watch.enabled ? "On" : "Off"}. Every 6 hou
|
|
|
59
59
|
: note(`Checked ${when(view.watch.checkedAt)}.`);
|
|
60
60
|
const fresh = view.fresh.length === 0 ? "" : sec("new", "New models", "", view.fresh.map(model => formRow(model.name, `${priceWords(model)}${model.releasedAt === null ? "" : ` · released ${model.releasedAt.slice(0, 10)}`}`, chip("New", "", { dot: false }), "inline")));
|
|
61
61
|
const chat = view.chat === null ? "" : formRow("Chat", view.chat.words, btn("Change", { sm: true, variant: "ghost", href: "/settings/lead" }), "inline");
|
|
62
|
-
const body = html `<div class="models">${outcome}${preview}${choiceSection(view, manage, previewing)}${recentSection(view)}${sec("tools", "AI tools on this computer", "", html `${tools}${checks}`)}${fresh}${chat}</div>`;
|
|
62
|
+
const body = html `<div class="models">${outcome}${preview}${choiceSection(view, manage, previewing)}${suggestedSection(view)}${recentSection(view)}${sec("tools", "AI tools on this computer", "", html `${tools}${checks}`)}${fresh}${chat}</div>`;
|
|
63
63
|
return settingsShell({ group: "agents", sub: true, title: "Models", lead: "Which model plans, builds and reviews each task. Changes apply to tasks filed from now on.", body });
|
|
64
64
|
}
|
|
65
65
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
|
@@ -123,3 +123,46 @@ function recentSection(view) {
|
|
|
123
123
|
return "";
|
|
124
124
|
return sec("recent", "Recent work", "", view.recent.map(one => formRow(one.role, `Chosen ${one.chosen} → ran on ${one.ranOn}`, html `<a class="k-link k-mono" href="/t/${encodeURIComponent(one.taskId)}">${one.taskId}</a>`, "inline")));
|
|
125
125
|
}
|
|
126
|
+
/** Settings → Models → Suggested (docs/plans/results-routing.md slice 2): what a project's own results suggest, each
|
|
127
|
+
* reviewed before it's saved with a step-up; the rules in force with Turn off (Keep or Drop when paused); and the
|
|
128
|
+
* project's "Suggest cheaper models" switch. Turning off and Not now only take authority away: no password. */
|
|
129
|
+
function suggestedSection(view) {
|
|
130
|
+
const s = view.suggested;
|
|
131
|
+
if (s === undefined)
|
|
132
|
+
return "";
|
|
133
|
+
const manage = view.csrf !== "";
|
|
134
|
+
const scoped = (repo) => `/settings/models?scope=${encodeURIComponent(repo)}`;
|
|
135
|
+
const review = s.review === null ? ""
|
|
136
|
+
: "stale" in s.review
|
|
137
|
+
? html `${notice("This suggestion is out of date: its results, a price or Models changed since. Nothing was saved.")}${acts(btn("Back", { sm: true, href: `${scoped(s.review.repo)}#models-suggested` }))}`
|
|
138
|
+
: html `<div class="models-diff">${factList([
|
|
139
|
+
["Change", s.review.title],
|
|
140
|
+
["Evidence", html `${s.review.evidence.map(line => html `<span>${line}</span><br>`)}`],
|
|
141
|
+
["Prices", s.review.prices],
|
|
142
|
+
["Switches back", html `${s.review.terms.map(line => html `<span>${line}</span><br>`)}`],
|
|
143
|
+
])}${manage ? postForm("/settings/models/accept", html `${note(`Every task is still approved, and keeps the model it was approved with. ${termsOf("step-up", factsOf(view))}`)}${password(view, "models-accept-pw")}${acts(btn(s.review.save, { variant: "primary", type: "submit" }), btn("Cancel", { variant: "ghost", href: `${scoped(s.review.repo)}#models-suggested` }))}`, { hidden: { scope: s.review.repo, id: s.review.id }, attrs: { class: "models-editor" } }) : ""}</div>`;
|
|
144
|
+
const offers = s.offers.map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, manage
|
|
145
|
+
? acts(btn("Review change", { sm: true, href: `${scoped(one.repo)}&suggestion=${encodeURIComponent(one.id)}#models-suggested` }), postForm("/settings/models/not-now", btn("Not now", { sm: true, variant: "ghost", type: "submit" }), { hidden: { scope: one.repo, id: one.id } }))
|
|
146
|
+
: html ``, "top"));
|
|
147
|
+
const rules = s.rules.map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, !manage || one.state === "ended" ? html ``
|
|
148
|
+
: one.state === "paused"
|
|
149
|
+
? acts(postForm("/settings/models/keep", html `${password(view, `models-keep-${one.id}`)}${btn("Keep", { sm: true, type: "submit" })}`, { hidden: { scope: one.repo, rule: String(one.id) } }), postForm("/settings/models/turn-off", btn("Drop", { sm: true, variant: "ghost", type: "submit" }), { hidden: { scope: one.repo, rule: String(one.id) } }))
|
|
150
|
+
: postForm("/settings/models/turn-off", btn("Turn off", { sm: true, type: "submit" }), { hidden: { scope: one.repo, rule: String(one.id) } }), "top"));
|
|
151
|
+
// Trials (slice 3): an offer to try a cheaper model on the next few tasks — started with a step-up, each task still
|
|
152
|
+
// approved by a person — and the trials in use, each with its progress and Stop (stopping only lowers: no password).
|
|
153
|
+
const trialOffers = (s.trialOffers ?? []).map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, manage
|
|
154
|
+
? acts(postForm("/settings/models/try", html `${password(view, `models-try-${one.id.replace(/[^a-z0-9]/gi, "-")}`)}${btn(one.save, { sm: true, type: "submit" })}`, { hidden: { scope: one.repo, id: one.id } }), postForm("/settings/models/not-now", btn("Not now", { sm: true, variant: "ghost", type: "submit" }), { hidden: { scope: one.repo, id: one.id } }))
|
|
155
|
+
: html ``, "top"));
|
|
156
|
+
const trials = (s.trials ?? []).map(one => formRow(s.repo === null ? `${one.title} · ${one.project}` : one.title, one.words, !manage || !one.live ? html ``
|
|
157
|
+
: postForm("/settings/models/stop", btn("Stop", { sm: true, type: "submit" }), { hidden: { scope: one.repo, rule: String(one.id) } }), "top"));
|
|
158
|
+
const toggle = s.suggest === null || s.repo === null ? "" : formRow("Suggest cheaper models", s.suggest.on
|
|
159
|
+
? "On. When a cheaper model does as well on this project's small or routine tasks, it's suggested here. Nothing changes until you accept it."
|
|
160
|
+
: "Off. No new suggestions for this project.", !manage ? html `` : s.suggest.on
|
|
161
|
+
? postForm("/settings/models/suggest-off", btn("Turn off", { sm: true, type: "submit" }), { hidden: { scope: s.repo } })
|
|
162
|
+
: postForm("/settings/models/suggest-on", html `${password(view, "models-suggest-pw")}${btn("Turn on", { sm: true, type: "submit" })}`, { hidden: { scope: s.repo } }), "inline");
|
|
163
|
+
const emptyWords = s.repo === null ? "No suggestions yet. Choose a project above to see its own." : "No suggestions yet. When a cheaper model's results on this project's small or routine tasks match the configured model's, it's suggested here.";
|
|
164
|
+
const nothing = offers.length === 0 && rules.length === 0 && trialOffers.length === 0 && trials.length === 0 && s.review === null;
|
|
165
|
+
const body = html `${review}${nothing ? note(emptyWords) : ""}${offers}${trialOffers}${rules.length === 0 ? "" : html `${group("Rules in use")}${rules}`}${trials.length === 0 ? "" : html `${group("Trials")}${trials}`}${toggle}`;
|
|
166
|
+
const waiting = s.offers.length + trialOffers.length;
|
|
167
|
+
return sec("suggested", "Suggested", waiting === 0 ? "" : `${waiting} to review`, body);
|
|
168
|
+
}
|