toolroll 0.9.91 → 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 +22 -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-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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.92 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
- **Toolroll learns which cheaper models do the job.** Every task now gets a
|
|
6
|
+
result record (see it on the task page, or run `toolroll models results`).
|
|
7
|
+
Once a cheaper model has done well on a project's small or routine tasks,
|
|
8
|
+
Settings → Models suggests it, with the evidence. Accepting needs your
|
|
9
|
+
password and only affects that project's eligible tasks; if the cheaper
|
|
10
|
+
model then does badly, Toolroll switches back on its own and tells you.
|
|
11
|
+
Approvals show a line saying why a model was picked. Also in the terminal:
|
|
12
|
+
`toolroll models suggestions`, `accept`, `dismiss`, `turn-off`.
|
|
13
|
+
- **Try a cheaper model on the next 10 tasks.** When a cheaper model has no
|
|
14
|
+
track record yet, Settings → Models offers a trial. Each trial task says
|
|
15
|
+
"trial 3 of 10" and must be approved by a person; Stop ends it any time
|
|
16
|
+
(`toolroll models try` / `stop`).
|
|
17
|
+
- **See what it saved.** The Spend page and `toolroll spend` show cost per
|
|
18
|
+
task before and after each rule or trial — observed, not a promise, and
|
|
19
|
+
only once there are enough priced results.
|
|
20
|
+
- **Project memory is changed by a person only.** A personal API token used
|
|
21
|
+
from the remote CLI could record or retire project decisions without a
|
|
22
|
+
password; now it can't, and neither can the lead. Reading memory works as
|
|
23
|
+
before.
|
|
24
|
+
|
|
3
25
|
## 0.9.91 — 2026-11-04
|
|
4
26
|
|
|
5
27
|
- **A deploy checks your HTTPS console before it changes anything.** If the
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/** SQL over a `task_scope` alias and a repo expression: whether that row's approval authority stands at `now` (an SQL
|
|
2
2
|
* expression, used exactly three times: the mode and grant clauses, and the release's expiry). It never asks whether the row is approved.
|
|
3
3
|
* COALESCE: a NULL basis would make the OR chain NULL, and NOT NULL reads as "not dead"; NULL stands on nothing. */
|
|
4
|
-
export declare
|
|
4
|
+
export declare function authorityStandsSql(scope: string, repo: string, now: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* SQL, conjunctive with every alternative above (results routing slice 3): a task whose sealed route tries a cheaper
|
|
7
|
+
* model (its build leg's selection is a trial) stands only on a PERSON's own yes to these exact bytes — the ceremony
|
|
8
|
+
* (`password`) or their paired-chat tap (`chat`), with that person's proof at the seal's instant, and no machine's.
|
|
9
|
+
* A mode, a grant, a schedule, a carry, an automatic seal or the lead never stand for one, whatever else allows them.
|
|
10
|
+
* The route is matched as text, so a route that can't be read as JSON is held to it too (fail closed).
|
|
11
|
+
*/
|
|
12
|
+
export declare function trialSealStandsSql(scope: string): string;
|
|
5
13
|
/**
|
|
6
14
|
* SQL, conjunctive with every alternative above (easy-start plan C5): a seal a local owner gave (its proof row at the
|
|
7
15
|
* seal's instant says `local-owner`) stands only while local mode is on, for that owner, at the local generation its
|
|
@@ -20,7 +20,8 @@ const PERSON_PROOFS = "('password-now', 'sso-now', 'fresh-password', 'fresh-sso'
|
|
|
20
20
|
/** SQL over a `task_scope` alias and a repo expression: whether that row's approval authority stands at `now` (an SQL
|
|
21
21
|
* expression, used exactly three times: the mode and grant clauses, and the release's expiry). It never asks whether the row is approved.
|
|
22
22
|
* COALESCE: a NULL basis would make the OR chain NULL, and NOT NULL reads as "not dead"; NULL stands on nothing. */
|
|
23
|
-
export
|
|
23
|
+
export function authorityStandsSql(scope, repo, now) {
|
|
24
|
+
return `COALESCE((((COALESCE(${scope}.approval_basis, '') IN ('password', 'chat') AND NOT ${machineSealSql(scope)}
|
|
24
25
|
AND (${personSealSql(scope)} OR NOT ${automationSql(scope)} OR ${approverStandsSql(scope, repo)}))
|
|
25
26
|
OR (${scope}.approval_basis = 'carried' AND ${carrySealSql(scope)})
|
|
26
27
|
OR (${scope}.approval_basis = 'mode' AND EXISTS (SELECT 1 FROM operating_mode om
|
|
@@ -30,7 +31,19 @@ export const authorityStandsSql = (scope, repo, now) => `COALESCE((((COALESCE(${
|
|
|
30
31
|
OR (${scope}.approval_basis = 'grant' AND ${grantStandsSql(`${scope}.mode_digest`, now, repo)})
|
|
31
32
|
OR (${scope}.approval_basis = 'schedule' AND ${approverStandsSql(scope, repo)})
|
|
32
33
|
OR (${scope}.approval_basis IN ('auto-check', 'auto-green') AND ${autoSealSql(scope, repo)} AND ${approverStandsSql(scope, repo)}))
|
|
33
|
-
AND ${releaseStandsSql(scope, now)} AND ${localSealStandsSql(scope, repo)}
|
|
34
|
+
AND ${releaseStandsSql(scope, now)} AND ${localSealStandsSql(scope, repo)}
|
|
35
|
+
AND (instr(COALESCE(${scope}.approved_route_json, ''), '"trial"') = 0 OR ${trialSealStandsSql(scope)})), 0)`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* SQL, conjunctive with every alternative above (results routing slice 3): a task whose sealed route tries a cheaper
|
|
39
|
+
* model (its build leg's selection is a trial) stands only on a PERSON's own yes to these exact bytes — the ceremony
|
|
40
|
+
* (`password`) or their paired-chat tap (`chat`), with that person's proof at the seal's instant, and no machine's.
|
|
41
|
+
* A mode, a grant, a schedule, a carry, an automatic seal or the lead never stand for one, whatever else allows them.
|
|
42
|
+
* The route is matched as text, so a route that can't be read as JSON is held to it too (fail closed).
|
|
43
|
+
*/
|
|
44
|
+
export function trialSealStandsSql(scope) {
|
|
45
|
+
return `(COALESCE(${scope}.approval_basis, '') IN ('password', 'chat') AND NOT ${machineSealSql(scope)} AND ${personSealSql(scope)})`;
|
|
46
|
+
}
|
|
34
47
|
/**
|
|
35
48
|
* SQL, conjunctive with every alternative above (easy-start plan C5): a seal a local owner gave (its proof row at the
|
|
36
49
|
* seal's instant says `local-owner`) stands only while local mode is on, for that owner, at the local generation its
|
package/dist/build-review.js
CHANGED
|
@@ -126,6 +126,8 @@ function sendBack(store, root, runId, taskId, repo, high, now) {
|
|
|
126
126
|
run: runId, source: revisionSourceOf(store.getScope(taskId)?.digest ?? null), actor, repos: [repo], batch: "",
|
|
127
127
|
note: revisionNoteOf(high), path: "", line: "",
|
|
128
128
|
request: createHash("sha256").update(`build-review:${runId}`).digest("hex").slice(0, 32), allowMode: true,
|
|
129
|
+
// The automatic review's send-back, even when the mode's signer is its actor: never a person's act.
|
|
130
|
+
origin: "automatic-review",
|
|
129
131
|
}, now);
|
|
130
132
|
return revised.ok ? { ok: true, id: revised.id } : { ok: false, problem: revised.message };
|
|
131
133
|
}
|
package/dist/chat-actions.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Shared, exact-state actions. Models may prepare; only the existing human
|
|
2
|
-
* confirmation door executes. Protected actions use a one-use review receipt. */
|
|
3
1
|
import { type Authority } from "./local-owner.js";
|
|
4
2
|
import { type Store } from "./store.js";
|
|
5
3
|
import { type VerifiedApprover } from "./principal.js";
|
|
@@ -231,6 +229,16 @@ export declare const CHAT_ACTIONS: {
|
|
|
231
229
|
readonly protected: false;
|
|
232
230
|
readonly password: false;
|
|
233
231
|
};
|
|
232
|
+
readonly suggestion_not_now: {
|
|
233
|
+
readonly label: "Not now";
|
|
234
|
+
readonly protected: false;
|
|
235
|
+
readonly password: false;
|
|
236
|
+
};
|
|
237
|
+
readonly trial_stop: {
|
|
238
|
+
readonly label: "Stop trial";
|
|
239
|
+
readonly protected: false;
|
|
240
|
+
readonly password: false;
|
|
241
|
+
};
|
|
234
242
|
readonly lead_about_you: {
|
|
235
243
|
readonly label: "Remember";
|
|
236
244
|
readonly protected: false;
|
|
@@ -279,7 +287,7 @@ export declare function sharedActionReviewPath(id: number): string;
|
|
|
279
287
|
export declare function mintSharedActionReview(store: Store, who: VerifiedApprover, id: number, root: string, now: Date): {
|
|
280
288
|
payload: {
|
|
281
289
|
version: 1;
|
|
282
|
-
operation: "decision_record" | "decision_retire" | "flow_browser" | "flow_browser_off" | "flow_card_add" | "flow_card_approve" | "flow_card_assign" | "flow_card_cancel" | "flow_card_choose" | "flow_card_comment" | "flow_card_move" | "flow_card_send_back" | "flow_card_watch" | "flow_create" | "flow_edit" | "flow_script_save" | "flow_starter" | "flow_trigger_add" | "flow_trigger_pause" | "flow_trigger_remove" | "flow_trigger_resume" | "kit_setup" | "knowledge_instructions" | "knowledge_remove" | "knowledge_restore" | "knowledge_save" | "lead_about_you" | "result_accept" | "scope_approve" | "skill_disable" | "skill_enable" | "skill_import" | "skill_restore" | "skill_test" | "subagent_answer" | "subagent_ask" | "subagent_create" | "subagent_memory" | "subagent_note" | "subagent_routine" | "subagent_soul" | "subagent_state" | "subagent_tools" | "subagent_undo" | "sweep_cancel_rest" | "sweep_file" | "task_cancel" | "task_resume" | "tool_add" | "tool_remove";
|
|
290
|
+
operation: "decision_record" | "decision_retire" | "flow_browser" | "flow_browser_off" | "flow_card_add" | "flow_card_approve" | "flow_card_assign" | "flow_card_cancel" | "flow_card_choose" | "flow_card_comment" | "flow_card_move" | "flow_card_send_back" | "flow_card_watch" | "flow_create" | "flow_edit" | "flow_script_save" | "flow_starter" | "flow_trigger_add" | "flow_trigger_pause" | "flow_trigger_remove" | "flow_trigger_resume" | "kit_setup" | "knowledge_instructions" | "knowledge_remove" | "knowledge_restore" | "knowledge_save" | "lead_about_you" | "result_accept" | "scope_approve" | "skill_disable" | "skill_enable" | "skill_import" | "skill_restore" | "skill_test" | "subagent_answer" | "subagent_ask" | "subagent_create" | "subagent_memory" | "subagent_note" | "subagent_routine" | "subagent_soul" | "subagent_state" | "subagent_tools" | "subagent_undo" | "suggestion_not_now" | "sweep_cancel_rest" | "sweep_file" | "task_cancel" | "task_resume" | "tool_add" | "tool_remove" | "trial_stop";
|
|
283
291
|
request: {
|
|
284
292
|
[x: string]: unknown;
|
|
285
293
|
};
|
package/dist/chat-actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared, exact-state actions. Models may prepare; only the existing human
|
|
2
2
|
* confirmation door executes. Protected actions use a one-use review receipt. */
|
|
3
|
+
import { dismissSuggestion, ruleById, stopTrial, suggestionsFor, suggestionWords, taskEvidenceWords, trialWords } from "./route-rules.js";
|
|
3
4
|
import { CHAT_ACTION_LOCAL_ACTS, currentLocalOwner } from "./local-owner.js";
|
|
4
5
|
import { fileTaskProposal } from "./proposal.js";
|
|
5
6
|
import { heuristicSizing } from "./task-sizing.js";
|
|
@@ -128,6 +129,10 @@ export const CHAT_ACTIONS = {
|
|
|
128
129
|
kit_setup: { label: "Set up kit", protected: false, password: false },
|
|
129
130
|
decision_record: { label: "Record decision", protected: false, password: false },
|
|
130
131
|
decision_retire: { label: "Retire decision", protected: false, password: false },
|
|
132
|
+
// Not now only takes a suggestion off the list for 30 days: nothing about which models run changes.
|
|
133
|
+
suggestion_not_now: { label: "Not now", protected: false, password: false },
|
|
134
|
+
// Stopping a trial only lowers: new tasks build on the configured model; approved ones keep theirs.
|
|
135
|
+
trial_stop: { label: "Stop trial", protected: false, password: false },
|
|
131
136
|
// What the owner's own lead knows about them: theirs alone, in no project (lead-about.ts).
|
|
132
137
|
lead_about_you: { label: "Remember", protected: false, password: false },
|
|
133
138
|
scope_approve: { label: "Approve work", protected: true, password: true },
|
|
@@ -279,7 +284,7 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
279
284
|
const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
|
|
280
285
|
return { version: SHARED_ACTION_VERSION, operation, request, repo: "", title, terms, stamp, state };
|
|
281
286
|
}
|
|
282
|
-
const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_") || operation.startsWith("sweep_")
|
|
287
|
+
const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("subagent_") || operation.startsWith("kit_") || operation.startsWith("sweep_") || operation.startsWith("suggestion_") || operation.startsWith("trial_")
|
|
283
288
|
? null
|
|
284
289
|
: text(input, "task", 64);
|
|
285
290
|
const flowTarget = operation.startsWith("flow_") && operation !== "flow_create" && operation !== "flow_script_save" && operation !== "flow_starter" ? flowTargetOf(store, input) : null;
|
|
@@ -866,6 +871,26 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
866
871
|
}
|
|
867
872
|
}
|
|
868
873
|
}
|
|
874
|
+
else if (operation === "suggestion_not_now") {
|
|
875
|
+
// Only a suggestion on offer now, by its exact id (which binds its evidence, prices and Models generations).
|
|
876
|
+
const asked = text(input, "suggestion", 200);
|
|
877
|
+
const offer = suggestionsFor(store, repo, now).find(one => one.id === asked);
|
|
878
|
+
if (offer === undefined)
|
|
879
|
+
throw Error("That suggestion isn't on offer any more. See Settings → Models.");
|
|
880
|
+
state = { suggestion: offer.id };
|
|
881
|
+
title = `Not now: ${offer.name} for ${offer.cohortSize === "small" ? "small" : "routine"} tasks`;
|
|
882
|
+
terms.push(suggestionWords(offer), "Keeps this suggestion quiet for 30 days. Nothing about which models run changes. To accept it instead, review it in Settings → Models.");
|
|
883
|
+
}
|
|
884
|
+
else if (operation === "trial_stop") {
|
|
885
|
+
// Only a live trial of this project, by its number; the card is stale once the trial moves.
|
|
886
|
+
const asked = integer(input, "trial");
|
|
887
|
+
const trial = Number.isSafeInteger(asked) ? ruleById(store, asked) : null;
|
|
888
|
+
if (trial === null || trial.kind !== "trial" || trial.repo !== repo || (trial.state !== "trialling" && trial.state !== "paused"))
|
|
889
|
+
throw Error("That trial isn't running any more. See Settings → Models.");
|
|
890
|
+
state = { trial: trial.id, generation: trial.generation };
|
|
891
|
+
title = "Stop trying a cheaper model";
|
|
892
|
+
terms.push(trialWords(store, trial), "New tasks build on the configured model again. Tasks a person already approved on the trial keep that model.");
|
|
893
|
+
}
|
|
869
894
|
else if (operation.startsWith("decision_")) {
|
|
870
895
|
// The staleness fence: the newest decision id and the active count, so a
|
|
871
896
|
// card drafted before a subagent recorded or retired one is refused.
|
|
@@ -947,10 +972,11 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
|
|
|
947
972
|
if (!authority.ok)
|
|
948
973
|
throw Error(authority.problem);
|
|
949
974
|
state["authMode"] = authority.authMode;
|
|
950
|
-
terms.push(...describeScope(scope)
|
|
975
|
+
terms.push(...describeScope(scope, undefined, taskEvidenceWords(store, scope.taskId, now))
|
|
951
976
|
.filter((line) => !/^\s*(reference|approved)\s/.test(line))
|
|
952
977
|
.map((line) => line
|
|
953
978
|
.trimStart()
|
|
979
|
+
.replace(/^why model\s+/, "Why this model: ")
|
|
954
980
|
.replace(/^goal\s+/, "Work: ")
|
|
955
981
|
.replace(/^not this\s+/, "Excluded: ")
|
|
956
982
|
.replace(/^touches\s+/, "Allowed files: ")), `Agent permissions and limits\n${runtimeTerms(authority.profile)}\n${authority.authMode === "api-key" ? "Uses your API key; spend is charged to that account." : authority.authMode === "subscription" ? "Uses your subscription login." : ""}`);
|
|
@@ -1246,6 +1272,25 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1246
1272
|
if (!removeToolFrom(store, repo, String(req["name"]), actor, now))
|
|
1247
1273
|
throw Error("That tool was already removed.");
|
|
1248
1274
|
}
|
|
1275
|
+
else if (payload.operation === "trial_stop") {
|
|
1276
|
+
const door = currentLocalOwner() !== null ? { kind: "local-owner", actor } : { kind: "person", person: { actor: who.name, generation: who.generation, method: "session", live: () => true } };
|
|
1277
|
+
const trial = ruleById(store, Number(payload.state["trial"]));
|
|
1278
|
+
if (trial === null || trial.generation !== Number(payload.state["generation"]))
|
|
1279
|
+
throw Error("That trial changed since this card was made. See Settings → Models.");
|
|
1280
|
+
const done = stopTrial(store, trial.id, door, now);
|
|
1281
|
+
if (!done.ok)
|
|
1282
|
+
throw Error(done.problem);
|
|
1283
|
+
return { ok: true, taskId: null, said: "Trial stopped. New tasks build on the configured model.", href: `/settings/models?scope=${encodeURIComponent(repo)}#models-suggested` };
|
|
1284
|
+
}
|
|
1285
|
+
else if (payload.operation === "suggestion_not_now") {
|
|
1286
|
+
// The rule door (route-store.ts proveRuleDoor) re-proves who says it: this computer's local owner only inside
|
|
1287
|
+
// its `dismiss-suggestion` act, or the confirming person.
|
|
1288
|
+
const door = currentLocalOwner() !== null ? { kind: "local-owner", actor } : { kind: "person", person: { actor: who.name, generation: who.generation, method: "session", live: () => true } };
|
|
1289
|
+
const done = dismissSuggestion(store, { repo, id: String(payload.state["suggestion"]) }, door, now);
|
|
1290
|
+
if (!done.ok)
|
|
1291
|
+
throw Error(done.problem);
|
|
1292
|
+
return { ok: true, taskId: null, said: "Not now: this suggestion stays quiet for 30 days.", href: `/settings/models?scope=${encodeURIComponent(repo)}#models-suggested` };
|
|
1293
|
+
}
|
|
1249
1294
|
else if (payload.operation === "decision_record")
|
|
1250
1295
|
recordDecision(store, { repo, actor, draft: { claim: String(req["claim"]), why: String(req["why"]), sourceKind: "conversation",
|
|
1251
1296
|
...(req["source"] === undefined ? {} : { sourceRef: String(req["source"]) }), ...(payload.state["supersedes"] === undefined ? {} : { supersedes: Number(payload.state["supersedes"]) }) } }, now);
|
package/dist/chat-decide.js
CHANGED
|
@@ -972,6 +972,8 @@ export function applyDecideFeedback(store, seat, prompt, text, repos, root, now)
|
|
|
972
972
|
const revised = requestResultChanges(store, root, {
|
|
973
973
|
run: prompt.run, batch: "", source: prompt.digest, actor: verified.who.name, repos: verified.who.repos,
|
|
974
974
|
note: text, path: "", line: "", request: sha(`chat-decide:${seat.channel}:${prompt.id}`).slice(0, 32), allowMode: true,
|
|
975
|
+
// The chat's own verified person, answering "What should change?" in their own words.
|
|
976
|
+
origin: "person",
|
|
975
977
|
}, now);
|
|
976
978
|
if (!revised.ok)
|
|
977
979
|
return { said: `Your feedback wasn't sent: ${revised.message}`, link: null };
|
package/dist/chat-review.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RevisionOrigin } from "./contracts/route-results.js";
|
|
1
2
|
import type { Store } from "./store.js";
|
|
2
3
|
import type { VerifiedApprover } from "./principal.js";
|
|
3
4
|
import { type FeedbackImage } from "./result-review.js";
|
|
@@ -41,7 +42,7 @@ export declare function reviewInputProblem(note: string | null, path: string | n
|
|
|
41
42
|
/** Chat and the result form write the same comments and call the same revision service.
|
|
42
43
|
* A failed combined note+revision rolls back the note too; no half-completed action.
|
|
43
44
|
*/
|
|
44
|
-
export declare function applyChatReview(store: Store, who: VerifiedApprover, evidenceRoot: string, request: ReviewRequest, now: Date, allowMode: boolean): {
|
|
45
|
+
export declare function applyChatReview(store: Store, who: VerifiedApprover, evidenceRoot: string, request: ReviewRequest, now: Date, allowMode: boolean, origin: RevisionOrigin): {
|
|
45
46
|
ok: true;
|
|
46
47
|
taskId: string;
|
|
47
48
|
run: number;
|
package/dist/chat-review.js
CHANGED
|
@@ -48,7 +48,7 @@ export function reviewInputProblem(note, path, line) {
|
|
|
48
48
|
/** Chat and the result form write the same comments and call the same revision service.
|
|
49
49
|
* A failed combined note+revision rolls back the note too; no half-completed action.
|
|
50
50
|
*/
|
|
51
|
-
export function applyChatReview(store, who, evidenceRoot, request, now, allowMode) {
|
|
51
|
+
export function applyChatReview(store, who, evidenceRoot, request, now, allowMode, origin) {
|
|
52
52
|
try {
|
|
53
53
|
return store.transact(() => store.savepoint(() => {
|
|
54
54
|
const fail = (message) => { throw new ReviewRefusal(message); };
|
|
@@ -86,7 +86,7 @@ export function applyChatReview(store, who, evidenceRoot, request, now, allowMod
|
|
|
86
86
|
if (batch.length === 0)
|
|
87
87
|
return fail("Add feedback or select saved notes before requesting changes.");
|
|
88
88
|
const result = createResultRevision(store, evidenceRoot, {
|
|
89
|
-
run: live.run, batch: batch.join(","), source: live.source, actor: who.name, repos: who.repos, allowMode,
|
|
89
|
+
run: live.run, batch: batch.join(","), source: live.source, actor: who.name, repos: who.repos, allowMode, origin,
|
|
90
90
|
}, now);
|
|
91
91
|
if (!result.ok)
|
|
92
92
|
return fail(result.message);
|
|
@@ -252,6 +252,14 @@ export declare const chatActionRequestSchemas: {
|
|
|
252
252
|
decision: z.ZodInt;
|
|
253
253
|
reason: z.ZodString;
|
|
254
254
|
}, z.core.$strict>;
|
|
255
|
+
readonly suggestion_not_now: z.ZodObject<{
|
|
256
|
+
repo: z.ZodString;
|
|
257
|
+
suggestion: z.ZodString;
|
|
258
|
+
}, z.core.$strict>;
|
|
259
|
+
readonly trial_stop: z.ZodObject<{
|
|
260
|
+
repo: z.ZodString;
|
|
261
|
+
trial: z.ZodInt;
|
|
262
|
+
}, z.core.$strict>;
|
|
255
263
|
/** The line, and (from 1) the line it replaces; 0 or none adds it. */
|
|
256
264
|
readonly lead_about_you: z.ZodObject<{
|
|
257
265
|
line: z.ZodString;
|
|
@@ -331,12 +339,14 @@ export declare const sharedActionSchema: z.ZodObject<{
|
|
|
331
339
|
subagent_state: "subagent_state";
|
|
332
340
|
subagent_tools: "subagent_tools";
|
|
333
341
|
subagent_undo: "subagent_undo";
|
|
342
|
+
suggestion_not_now: "suggestion_not_now";
|
|
334
343
|
sweep_cancel_rest: "sweep_cancel_rest";
|
|
335
344
|
sweep_file: "sweep_file";
|
|
336
345
|
task_cancel: "task_cancel";
|
|
337
346
|
task_resume: "task_resume";
|
|
338
347
|
tool_add: "tool_add";
|
|
339
348
|
tool_remove: "tool_remove";
|
|
349
|
+
trial_stop: "trial_stop";
|
|
340
350
|
}>;
|
|
341
351
|
request: z.ZodObject<{}, z.core.$catchall<z.ZodUnknown>>;
|
|
342
352
|
repo: z.ZodString;
|
|
@@ -81,6 +81,10 @@ export const chatActionRequestSchemas = {
|
|
|
81
81
|
kit_setup: z.strictObject({ repo: words, kit: words }),
|
|
82
82
|
decision_record: z.strictObject({ repo: words, claim: words, why: words, supersedes: id.optional(), source: words.optional() }),
|
|
83
83
|
decision_retire: z.strictObject({ repo: words, decision: id, reason: words }),
|
|
84
|
+
// Results routing: Not now on a suggested cheaper model (accepting stays in Settings → Models, behind a step-up).
|
|
85
|
+
suggestion_not_now: z.strictObject({ repo: words, suggestion: words }),
|
|
86
|
+
// Slice 3: stopping a trial (starting one stays in Settings → Models, behind a step-up).
|
|
87
|
+
trial_stop: z.strictObject({ repo: words, trial: id }),
|
|
84
88
|
/** The line, and (from 1) the line it replaces; 0 or none adds it. */
|
|
85
89
|
lead_about_you: z.strictObject({ line: words, replaces: z.int().min(0).optional() }),
|
|
86
90
|
scope_approve: z.strictObject({ task: words }),
|
|
@@ -112,6 +112,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
112
112
|
readonly appearance: FormContract<"accent" | "quiet" | "return" | "theme">;
|
|
113
113
|
readonly models: FormContract<"enabled" | "tool">;
|
|
114
114
|
readonly modelsChange: FormContract<"build" | "build-light" | "build-strong" | "change" | "digest" | "password" | "plan" | "plan-strong" | "preset" | "repair" | "review" | "review-strong" | "scope" | "switch" | "to">;
|
|
115
|
+
readonly modelsRules: FormContract<"id" | "password" | "rule" | "scope">;
|
|
115
116
|
readonly knowledgeRefresh: FormContract<"repo">;
|
|
116
117
|
readonly knowledgeProposal: FormContract<"decision" | "proposal" | "repo">;
|
|
117
118
|
readonly knowledgeDecision: FormContract<"action" | "claim" | "decision" | "reason" | "repo" | "why">;
|
|
@@ -124,6 +124,9 @@ export const CONSOLE_FORMS = {
|
|
|
124
124
|
// preset or switch; the digest is what the page showed, and the change digest what the preview showed.
|
|
125
125
|
modelsChange: formContract("POST /settings/models/agent, /settings/models/apply", ["password", "scope", "digest", "change", "preset", "switch", "to",
|
|
126
126
|
"plan", "plan-strong", "build-light", "build", "build-strong", "repair", "review", "review-strong"]),
|
|
127
|
+
// Settings → Models → Suggested (results routing): the project, a suggestion's id or a rule's number, and the step-up
|
|
128
|
+
// password for accepting, keeping and switching suggestions on.
|
|
129
|
+
modelsRules: formContract("POST /settings/models/accept, /settings/models/keep, /settings/models/suggest-on, /settings/models/turn-off, /settings/models/not-now, /settings/models/suggest-off, /settings/models/try, /settings/models/stop", ["password", "scope", "id", "rule"]),
|
|
127
130
|
knowledgeRefresh: formContract("POST /settings/knowledge/refresh", ["repo"]),
|
|
128
131
|
knowledgeProposal: formContract("POST /settings/knowledge/proposal", ["repo", "decision", "proposal"]),
|
|
129
132
|
knowledgeDecision: formContract("POST /settings/knowledge/decision", ["repo", "action", "claim", "why", "decision", "reason"]),
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result records (docs/plans/results-routing.md, slice 1): the words and shapes a task family's result record is kept
|
|
3
|
+
* and shown in. A policy path (release-auto.ts EVAL_POLICY): the states, reasons and the execution-class table decide
|
|
4
|
+
* what a cheaper model's evidence is, so a change to them needs a person.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
/** A record's lifecycle. Only mature and rejected records are decided; pending and provisional never count anywhere. */
|
|
8
|
+
export declare const RECORD_STATES: readonly ["pending", "provisional", "mature", "rejected", "not-counted"];
|
|
9
|
+
export type RecordState = (typeof RECORD_STATES)[number];
|
|
10
|
+
/** Why a record was rejected, in the order they are checked. */
|
|
11
|
+
export declare const REJECT_REASONS: readonly ["model-failures", "task-failed", "cancelled-after-result", "high-finding", "person-revision", "proof-refuted", "check-failed"];
|
|
12
|
+
/** Why a record is not counted. Never counted as good, never as 0. */
|
|
13
|
+
export declare const NOT_COUNTED_REASONS: readonly ["before-records", "not-eligible", "mixed-cohort", "mixed-models", "model-not-reported", "infrastructure-only", "cancelled-before-result", "no-check", "check-unavailable", "touched-protected", "changed-files-unknown", "unanswered"];
|
|
14
|
+
export type RejectReason = (typeof REJECT_REASONS)[number];
|
|
15
|
+
export type NotCountedReason = (typeof NOT_COUNTED_REASONS)[number];
|
|
16
|
+
export type RecordReason = RejectReason | NotCountedReason;
|
|
17
|
+
/** The plain words for each reason. */
|
|
18
|
+
export declare const REASON_WORDS: Readonly<Record<RecordReason, string>>;
|
|
19
|
+
/** Why a run was refused a cohort before dispatch: any of these makes its record not counted ("not eligible"). */
|
|
20
|
+
export declare const INELIGIBLE_REASONS: readonly ["no-sealed-route", "not-built-in", "large", "risky", "unsized", "strict", "evidence-demands", "automerge", "release", "protected"];
|
|
21
|
+
export type IneligibleReason = (typeof INELIGIBLE_REASONS)[number];
|
|
22
|
+
/** Who a revision came from (task_ref.revision_origin). Only `person` rejects a record. */
|
|
23
|
+
export declare const REVISION_ORIGINS: readonly ["person", "automatic-review", "flow", "lead", "machine"];
|
|
24
|
+
export type RevisionOrigin = (typeof REVISION_ORIGINS)[number];
|
|
25
|
+
/** A failed run's execution class, kept before the tick normalizes its reason (run.exec_class). */
|
|
26
|
+
export declare const EXEC_CLASSES: readonly ["model", "infrastructure"];
|
|
27
|
+
export type ExecClass = (typeof EXEC_CLASSES)[number];
|
|
28
|
+
/** The build leg's sealed selection, as a run's cohort keeps it (route v3 `selection`; earlier routes by `chosen`). */
|
|
29
|
+
export declare const selectionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
30
|
+
kind: z.ZodLiteral<"rule">;
|
|
31
|
+
rule: z.ZodNumber;
|
|
32
|
+
ruleGeneration: z.ZodNumber;
|
|
33
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
34
|
+
kind: z.ZodLiteral<"trial">;
|
|
35
|
+
rule: z.ZodNumber;
|
|
36
|
+
reservation: z.ZodNumber;
|
|
37
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
38
|
+
kind: z.ZodEnum<{
|
|
39
|
+
configured: "configured";
|
|
40
|
+
override: "override";
|
|
41
|
+
pin: "pin";
|
|
42
|
+
}>;
|
|
43
|
+
}, z.core.$strict>], "kind">;
|
|
44
|
+
export type Selection = z.infer<typeof selectionSchema>;
|
|
45
|
+
/** One record as `toolroll models results --json` returns it. */
|
|
46
|
+
export declare const resultRecordSchema: z.ZodObject<{
|
|
47
|
+
task: z.ZodString;
|
|
48
|
+
repo: z.ZodNullable<z.ZodString>;
|
|
49
|
+
size: z.ZodNullable<z.ZodEnum<{
|
|
50
|
+
medium: "medium";
|
|
51
|
+
small: "small";
|
|
52
|
+
}>>;
|
|
53
|
+
provider: z.ZodNullable<z.ZodString>;
|
|
54
|
+
model: z.ZodNullable<z.ZodString>;
|
|
55
|
+
state: z.ZodEnum<{
|
|
56
|
+
mature: "mature";
|
|
57
|
+
"not-counted": "not-counted";
|
|
58
|
+
pending: "pending";
|
|
59
|
+
provisional: "provisional";
|
|
60
|
+
rejected: "rejected";
|
|
61
|
+
}>;
|
|
62
|
+
reason: z.ZodNullable<z.ZodEnum<{
|
|
63
|
+
"before-records": "before-records";
|
|
64
|
+
"cancelled-after-result": "cancelled-after-result";
|
|
65
|
+
"cancelled-before-result": "cancelled-before-result";
|
|
66
|
+
"changed-files-unknown": "changed-files-unknown";
|
|
67
|
+
"check-failed": "check-failed";
|
|
68
|
+
"check-unavailable": "check-unavailable";
|
|
69
|
+
"high-finding": "high-finding";
|
|
70
|
+
"infrastructure-only": "infrastructure-only";
|
|
71
|
+
"mixed-cohort": "mixed-cohort";
|
|
72
|
+
"mixed-models": "mixed-models";
|
|
73
|
+
"model-failures": "model-failures";
|
|
74
|
+
"model-not-reported": "model-not-reported";
|
|
75
|
+
"no-check": "no-check";
|
|
76
|
+
"not-eligible": "not-eligible";
|
|
77
|
+
"person-revision": "person-revision";
|
|
78
|
+
"proof-refuted": "proof-refuted";
|
|
79
|
+
"task-failed": "task-failed";
|
|
80
|
+
"touched-protected": "touched-protected";
|
|
81
|
+
unanswered: "unanswered";
|
|
82
|
+
}>>;
|
|
83
|
+
words: z.ZodString;
|
|
84
|
+
resultRun: z.ZodNullable<z.ZodNumber>;
|
|
85
|
+
acceptedAt: z.ZodNullable<z.ZodString>;
|
|
86
|
+
decidedAt: z.ZodNullable<z.ZodString>;
|
|
87
|
+
failures: z.ZodObject<{
|
|
88
|
+
model: z.ZodNumber;
|
|
89
|
+
infrastructure: z.ZodNumber;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
spend: z.ZodObject<{
|
|
92
|
+
apiMicrousd: z.ZodNumber;
|
|
93
|
+
subscriptionRuns: z.ZodNumber;
|
|
94
|
+
subscriptionTokens: z.ZodNumber;
|
|
95
|
+
unknownRuns: z.ZodNumber;
|
|
96
|
+
remediationRuns: z.ZodNumber;
|
|
97
|
+
}, z.core.$strict>;
|
|
98
|
+
personRevisions: z.ZodNumber;
|
|
99
|
+
firstCheckPassed: z.ZodNullable<z.ZodBoolean>;
|
|
100
|
+
turns: z.ZodObject<{
|
|
101
|
+
known: z.ZodNumber;
|
|
102
|
+
unknownRuns: z.ZodNumber;
|
|
103
|
+
}, z.core.$strict>;
|
|
104
|
+
closed: z.ZodBoolean;
|
|
105
|
+
selections: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
106
|
+
kind: z.ZodLiteral<"rule">;
|
|
107
|
+
rule: z.ZodNumber;
|
|
108
|
+
ruleGeneration: z.ZodNumber;
|
|
109
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
110
|
+
kind: z.ZodLiteral<"trial">;
|
|
111
|
+
rule: z.ZodNumber;
|
|
112
|
+
reservation: z.ZodNumber;
|
|
113
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
114
|
+
kind: z.ZodEnum<{
|
|
115
|
+
configured: "configured";
|
|
116
|
+
override: "override";
|
|
117
|
+
pin: "pin";
|
|
118
|
+
}>;
|
|
119
|
+
}, z.core.$strict>], "kind">>;
|
|
120
|
+
}, z.core.$strict>;
|
|
121
|
+
export type ResultRecordView = z.infer<typeof resultRecordSchema>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result records (docs/plans/results-routing.md, slice 1): the words and shapes a task family's result record is kept
|
|
3
|
+
* and shown in. A policy path (release-auto.ts EVAL_POLICY): the states, reasons and the execution-class table decide
|
|
4
|
+
* what a cheaper model's evidence is, so a change to them needs a person.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
/** A record's lifecycle. Only mature and rejected records are decided; pending and provisional never count anywhere. */
|
|
8
|
+
export const RECORD_STATES = ["pending", "provisional", "mature", "rejected", "not-counted"];
|
|
9
|
+
/** Why a record was rejected, in the order they are checked. */
|
|
10
|
+
export const REJECT_REASONS = ["model-failures", "task-failed", "cancelled-after-result", "high-finding", "person-revision", "proof-refuted", "check-failed"];
|
|
11
|
+
/** Why a record is not counted. Never counted as good, never as 0. */
|
|
12
|
+
export const NOT_COUNTED_REASONS = [
|
|
13
|
+
"before-records", "not-eligible", "mixed-cohort", "mixed-models", "model-not-reported", "infrastructure-only", "cancelled-before-result",
|
|
14
|
+
"no-check", "check-unavailable", "touched-protected", "changed-files-unknown", "unanswered",
|
|
15
|
+
];
|
|
16
|
+
/** The plain words for each reason. */
|
|
17
|
+
export const REASON_WORDS = Object.freeze({
|
|
18
|
+
"model-failures": "two or more attempts failed on the model's own account",
|
|
19
|
+
"task-failed": "the task failed",
|
|
20
|
+
"cancelled-after-result": "it was cancelled after a result",
|
|
21
|
+
"high-finding": "the automatic review found a serious problem",
|
|
22
|
+
"person-revision": "a person asked for changes",
|
|
23
|
+
"proof-refuted": "its evidence didn't match the changes",
|
|
24
|
+
"check-failed": "its checks failed",
|
|
25
|
+
"before-records": "started before results were recorded",
|
|
26
|
+
"not-eligible": "not a small or routine task with default checks",
|
|
27
|
+
"mixed-cohort": "its attempts ran under different task sizes or settings",
|
|
28
|
+
"mixed-models": "its attempts ran on different models",
|
|
29
|
+
"model-not-reported": "the provider didn't report the model",
|
|
30
|
+
"infrastructure-only": "it failed only for reasons outside the model",
|
|
31
|
+
"cancelled-before-result": "it was cancelled before a result",
|
|
32
|
+
"no-check": "no checks configured",
|
|
33
|
+
"check-unavailable": "its check result can't be read",
|
|
34
|
+
"touched-protected": "touched protected files",
|
|
35
|
+
"changed-files-unknown": "changed files unknown",
|
|
36
|
+
"unanswered": "no answer in 30 days",
|
|
37
|
+
});
|
|
38
|
+
/** Why a run was refused a cohort before dispatch: any of these makes its record not counted ("not eligible"). */
|
|
39
|
+
export const INELIGIBLE_REASONS = ["no-sealed-route", "not-built-in", "large", "risky", "unsized", "strict", "evidence-demands", "automerge", "release", "protected"];
|
|
40
|
+
/** Who a revision came from (task_ref.revision_origin). Only `person` rejects a record. */
|
|
41
|
+
export const REVISION_ORIGINS = ["person", "automatic-review", "flow", "lead", "machine"];
|
|
42
|
+
/** A failed run's execution class, kept before the tick normalizes its reason (run.exec_class). */
|
|
43
|
+
export const EXEC_CLASSES = ["model", "infrastructure"];
|
|
44
|
+
/** The build leg's sealed selection, as a run's cohort keeps it (route v3 `selection`; earlier routes by `chosen`). */
|
|
45
|
+
export const selectionSchema = z.discriminatedUnion("kind", [
|
|
46
|
+
z.strictObject({ kind: z.literal("rule"), rule: z.number().int().positive(), ruleGeneration: z.number().int().positive() }),
|
|
47
|
+
z.strictObject({ kind: z.literal("trial"), rule: z.number().int().positive(), reservation: z.number().int().positive() }),
|
|
48
|
+
z.strictObject({ kind: z.enum(["configured", "override", "pin"]) }),
|
|
49
|
+
]);
|
|
50
|
+
/** One record as `toolroll models results --json` returns it. */
|
|
51
|
+
export const resultRecordSchema = z.strictObject({
|
|
52
|
+
task: z.string(),
|
|
53
|
+
repo: z.string().nullable(),
|
|
54
|
+
size: z.enum(["small", "medium"]).nullable(),
|
|
55
|
+
provider: z.string().nullable(),
|
|
56
|
+
model: z.string().nullable(),
|
|
57
|
+
state: z.enum(RECORD_STATES),
|
|
58
|
+
reason: z.enum([...REJECT_REASONS, ...NOT_COUNTED_REASONS]).nullable(),
|
|
59
|
+
words: z.string(),
|
|
60
|
+
resultRun: z.number().int().nullable(),
|
|
61
|
+
acceptedAt: z.string().nullable(),
|
|
62
|
+
decidedAt: z.string().nullable(),
|
|
63
|
+
failures: z.strictObject({ model: z.number().int(), infrastructure: z.number().int() }),
|
|
64
|
+
spend: z.strictObject({ apiMicrousd: z.number().int(), subscriptionRuns: z.number().int(), subscriptionTokens: z.number().int(), unknownRuns: z.number().int(), remediationRuns: z.number().int() }),
|
|
65
|
+
/** Counts that keep accumulating after the verdict is final, until the record closes. */
|
|
66
|
+
personRevisions: z.number().int(),
|
|
67
|
+
firstCheckPassed: z.boolean().nullable(),
|
|
68
|
+
turns: z.strictObject({ known: z.number().int(), unknownRuns: z.number().int() }),
|
|
69
|
+
closed: z.boolean(),
|
|
70
|
+
selections: z.array(selectionSchema),
|
|
71
|
+
});
|