toolroll 0.9.27 → 0.9.29
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 +28 -0
- package/dist/agentconfig.js +71 -3
- package/dist/browser/workspace.js +3 -3
- package/dist/build-review.js +3 -2
- package/dist/builder.js +10 -8
- package/dist/chat-actions.js +7 -4
- package/dist/chat-flow.js +10 -6
- package/dist/converse.js +3 -2
- package/dist/coordinator-proposals.js +2 -2
- package/dist/decision.d.ts +3 -3
- package/dist/decision.js +4 -3
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/flow-actions.d.ts +1 -0
- package/dist/flow-actions.js +6 -7
- package/dist/flow-code.d.ts +3 -2
- package/dist/flow-code.js +6 -4
- package/dist/flow-draft.d.ts +10 -4
- package/dist/flow-draft.js +19 -8
- package/dist/flow-engine.d.ts +0 -3
- package/dist/flow-engine.js +17 -10
- package/dist/flow-people.d.ts +0 -1
- package/dist/flow-people.js +9 -1
- package/dist/flow-send.js +3 -2
- package/dist/flow-steps.js +33 -10
- package/dist/flows.d.ts +23 -8
- package/dist/flows.js +106 -52
- package/dist/guides.js +1 -1
- package/dist/lead-context.d.ts +3 -9
- package/dist/lead-context.js +21 -5
- package/dist/lead-status.js +17 -12
- package/dist/mate-doors.js +8 -2
- package/dist/mate-tools.js +27 -10
- package/dist/mate.js +13 -2
- package/dist/needs-you.d.ts +2 -2
- package/dist/needs-you.js +3 -0
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +144 -9
- package/dist/phase-routing.d.ts +86 -5
- package/dist/phase-routing.js +229 -17
- package/dist/process-liveness.d.ts +1 -0
- package/dist/process-liveness.js +5 -0
- package/dist/project-skills.d.ts +10 -0
- package/dist/proposal.d.ts +10 -2
- package/dist/proposal.js +25 -10
- package/dist/recipe-ui.js +2 -1
- package/dist/scout-report.d.ts +10 -4
- package/dist/scout-report.js +23 -6
- package/dist/scout.d.ts +2 -0
- package/dist/scout.js +83 -10
- package/dist/serve.js +71 -15
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +72 -8
- package/dist/store.js +230 -29
- package/dist/task-sizing.d.ts +78 -0
- package/dist/task-sizing.js +228 -0
- package/dist/task-text.d.ts +4 -4
- package/dist/task-text.js +10 -6
- package/dist/teammate-work.js +31 -7
- package/dist/teammates.d.ts +20 -1
- package/dist/teammates.js +18 -5
- package/dist/teams-chat.d.ts +4 -1
- package/dist/teams-chat.js +17 -6
- package/dist/telegram-flow.js +6 -2
- package/dist/test-temp.js +1 -1
- package/dist/text-limits.d.ts +67 -0
- package/dist/text-limits.js +73 -0
- package/dist/tier-report.d.ts +44 -0
- package/dist/tier-report.js +71 -0
- package/package.json +1 -1
package/dist/mate-tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
1
2
|
import { connectionsOf, oneClickOf } from "./mcp-connect.js";
|
|
2
3
|
import { checkLevelFromWords, isCheckLevel } from "./check-levels.js";
|
|
3
4
|
import { policyParts } from "./policy.js";
|
|
@@ -42,7 +43,7 @@ import { parseAcceptanceCriteria, ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./sc
|
|
|
42
43
|
import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
|
|
43
44
|
import { agentChoicesFor, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
44
45
|
import { isNewModel, modelWords, priceWords, runtimeStates, seenModels } from "./model-catalog.js";
|
|
45
|
-
import { agentsSummary, chosenWords, isRiskLevel, PHASES, postureWords, RISK_CHOICES, riskConsequence, riskTitle, routeProblems, sameSpec, specWords } from "./phase-routing.js";
|
|
46
|
+
import { agentsSummary, chosenWords, isRiskLevel, isTaskSize, PHASES, postureWords, RISK_CHOICES, riskConsequence, riskTitle, routeProblems, sameSpec, sizeSourceWords, sizeWords, specWords } from "./phase-routing.js";
|
|
46
47
|
import { TOOL_CATALOG, discoverTools, projectToolsOf, secretsSetFor, toolCommandLine, toolStanding } from "./project-tools.js";
|
|
47
48
|
import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_STAGE_KINDS, FLOW_TEMPLATES, flowFromSteps, stepsFor } from "./flows.js";
|
|
48
49
|
import { flowDefinitionOf } from "./flow-engine.js";
|
|
@@ -359,6 +360,7 @@ export function agentsOver(store, taskId, now) {
|
|
|
359
360
|
const base = {
|
|
360
361
|
task: taskId,
|
|
361
362
|
risk: { level: risk, title: riskTitle(risk), consequence: riskConsequence(risk) },
|
|
363
|
+
size: ref.sizing == null ? null : { size: ref.sizing.size, risky: ref.sizing.risky, reason: ref.sizing.reason, source: sizeSourceWords(ref.sizing.source) },
|
|
362
364
|
riskChoices: RISK_CHOICES.map(one => ({ risk: one.risk, title: one.title, consequence: one.consequence })),
|
|
363
365
|
editable,
|
|
364
366
|
editableWhy,
|
|
@@ -378,6 +380,7 @@ export function agentsOver(store, taskId, now) {
|
|
|
378
380
|
standing: routed.source === "approved" ? "approved" : routed.source === "proposed" ? "awaiting approval" : "recommended",
|
|
379
381
|
summary: agentsSummary(route),
|
|
380
382
|
posture: postureWords(route),
|
|
383
|
+
sizeWords: sizeWords(route),
|
|
381
384
|
demands: route.demands.filter(reason => !/review/i.test(reason)),
|
|
382
385
|
agents: route.legs.filter(leg => leg.phase !== "review").map(leg => ({ role: ROLE_WORD[leg.phase], provider: leg.provider, model: leg.model, chosen: chosenWords(leg), reasons: leg.reasons.map(reason => reason.replace("builder and reviewer", "builder")), problem: leg.problem })),
|
|
383
386
|
problems: routeProblems(route),
|
|
@@ -433,7 +436,7 @@ export const MATE_TOOLS = [
|
|
|
433
436
|
{
|
|
434
437
|
name: "propose_action",
|
|
435
438
|
description: "Read get_actions and relevant skills/evidence first. Save an exact-state proposal only; protected or long terms require full secure review.",
|
|
436
|
-
inputSchema: schema({ operation: { type: 'string', enum: Object.keys(CHAT_ACTIONS).filter(one => !one.startsWith('flow_') && !one.startsWith('teammate_')) }, repo: { type: 'string' }, task: TASK_ARG, version: { type: 'string' }, restore: { type: 'integer', minimum: 1 }, sample: { type: 'string', maxLength: 800 }, content: { type: 'string', maxLength: 12000 }, instructions: { type: 'string', maxLength:
|
|
439
|
+
inputSchema: schema({ operation: { type: 'string', enum: Object.keys(CHAT_ACTIONS).filter(one => !one.startsWith('flow_') && !one.startsWith('teammate_')) }, repo: { type: 'string' }, task: TASK_ARG, version: { type: 'string' }, restore: { type: 'integer', minimum: 1 }, sample: { type: 'string', maxLength: 800 }, content: { type: 'string', maxLength: 12000 }, instructions: { type: 'string', maxLength: TEXT_LIMITS.flowInstructions }, title: { type: 'string', maxLength: 120 }, id: { type: 'string' }, run: { type: 'integer', minimum: 1 }, note: { type: 'string', maxLength: LIMITS.note, description: `At most ${LIMITS.note} characters; longer is refused with its length, not cut.` }, catalog: { type: 'string', maxLength: 40 }, name: { type: 'string', maxLength: 40 }, command: { type: 'string', maxLength: 400 }, args: { type: 'array', items: { type: 'string', maxLength: 400 }, maxItems: 40 }, url: { type: 'string', maxLength: 500 }, secrets: { type: 'array', items: { type: 'string', maxLength: 64 }, maxItems: 12 }, about: { type: 'string', maxLength: 240 } }, ['operation']),
|
|
437
440
|
handle: (ctx, args) => {
|
|
438
441
|
const operation = args['operation'];
|
|
439
442
|
if (!isChatAction(operation))
|
|
@@ -894,7 +897,7 @@ export const MATE_TOOLS = [
|
|
|
894
897
|
name: { type: "string", maxLength: 80 }, template: { type: "string", enum: FLOW_TEMPLATES.map(one => one.id) },
|
|
895
898
|
steps: { type: "array", minItems: 1, maxItems: 24, items: { type: "object", additionalProperties: false, properties: {
|
|
896
899
|
id: { type: "string", maxLength: 32 }, title: { type: "string", maxLength: 60 }, kind: { type: "string", enum: [...FLOW_STAGE_KINDS] },
|
|
897
|
-
instructions: { type: "string", maxLength:
|
|
900
|
+
instructions: { type: "string", maxLength: TEXT_LIMITS.flowInstructions }, planning: { type: "string", enum: ["auto", "required", "skip"] },
|
|
898
901
|
decider: { type: "string", maxLength: 64 }, message: { type: "string", maxLength: 1000 },
|
|
899
902
|
script: { type: "string", maxLength: 40 }, close: { type: "boolean" },
|
|
900
903
|
question: { type: "string", maxLength: 300 }, sureAt: { type: "integer", minimum: 50, maximum: 99 },
|
|
@@ -913,7 +916,7 @@ export const MATE_TOOLS = [
|
|
|
913
916
|
ifReplied: { type: "string", maxLength: 60 }, repo: REPO_ARG,
|
|
914
917
|
next: { type: "string", maxLength: 60 }, ifFails: { type: "string", maxLength: 60 }, ifNotSure: { type: "string", maxLength: 60 },
|
|
915
918
|
} } },
|
|
916
|
-
title: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 4000 }, zone: { type: "string", maxLength: 60 }, note: { type: "string", maxLength:
|
|
919
|
+
title: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 4000 }, zone: { type: "string", maxLength: 60 }, note: { type: "string", maxLength: LIMITS.note, description: `At most ${LIMITS.note} characters; longer is refused with its length, not cut.` },
|
|
917
920
|
choice: { type: "integer", minimum: 1, maximum: 4 },
|
|
918
921
|
trigger: { type: "integer", minimum: 1 }, owner: { type: "string", maxLength: 64 },
|
|
919
922
|
script: { type: "object", additionalProperties: false, properties: { name: { type: "string", maxLength: 40 }, about: { type: "string", maxLength: 160 }, body: { type: "string", maxLength: 1600 }, timeoutMinutes: { type: "integer", minimum: 1, maximum: 60 },
|
|
@@ -1829,15 +1832,17 @@ export const MATE_TOOLS = [
|
|
|
1829
1832
|
{
|
|
1830
1833
|
name: "propose_steer",
|
|
1831
1834
|
description: "Guide the next attempt within existing scope; never interrupt work.",
|
|
1832
|
-
inputSchema: schema({ task: TASK_ARG, note: { type: "string", maxLength:
|
|
1835
|
+
inputSchema: schema({ task: TASK_ARG, note: { type: "string", maxLength: LIMITS.note, description: `At most ${LIMITS.note} characters; longer is refused with its length, not cut.` } }, ["task", "note"]),
|
|
1833
1836
|
handle: (ctx, args) => {
|
|
1834
1837
|
const taskId = taskIdOf(args);
|
|
1835
1838
|
const ref = taskId === null ? null : admittedRef(ctx, taskId);
|
|
1836
1839
|
const task = taskId === null ? null : ctx.store.getTask(taskId);
|
|
1837
1840
|
if (taskId === null || ref === null || task === null)
|
|
1838
1841
|
return notFound();
|
|
1839
|
-
if (
|
|
1840
|
-
return { ok: false, message:
|
|
1842
|
+
if (typeof args["note"] === "string" && args["note"].length > LIMITS.note)
|
|
1843
|
+
return { ok: false, message: `guidance is ${args["note"].length} characters; the limit is ${LIMITS.note}. Shorten it and propose again.` };
|
|
1844
|
+
if (!honest(args["note"], LIMITS.note))
|
|
1845
|
+
return { ok: false, message: `guidance is plain text up to ${LIMITS.note} characters` };
|
|
1841
1846
|
if (task.state === "done" || task.state === "cancelled")
|
|
1842
1847
|
return { ok: false, message: "that task is finished, so guidance has no next attempt to reach" };
|
|
1843
1848
|
if (ctx.store.openContestFor(ref.id) !== null)
|
|
@@ -1945,10 +1950,12 @@ export const MATE_TOOLS = [
|
|
|
1945
1950
|
},
|
|
1946
1951
|
{
|
|
1947
1952
|
name: "propose_agents",
|
|
1948
|
-
description: "Read get_agents. Change risk
|
|
1953
|
+
description: "Read get_agents. Change risk, size (small: fast model, no plan; large or risky: strongest agents), configured role model, or clear override; stales approval, refuses running work.",
|
|
1949
1954
|
inputSchema: schema({
|
|
1950
1955
|
task: TASK_ARG,
|
|
1951
1956
|
risk: { type: "string", enum: ["routine", "elevated", "high"] },
|
|
1957
|
+
size: { type: "string", enum: ["small", "medium", "large"] },
|
|
1958
|
+
risky: { type: "boolean" },
|
|
1952
1959
|
role: { type: "string", enum: ["planner", "builder", "repair"] },
|
|
1953
1960
|
agent: schema({ provider: { type: "string", maxLength: 20 }, model: { type: "string", maxLength: 120 } }, ["provider", "model"]),
|
|
1954
1961
|
clear: { type: "boolean" },
|
|
@@ -1969,8 +1976,16 @@ export const MATE_TOOLS = [
|
|
|
1969
1976
|
return { ok: false, message: "role is planner, builder, or repair" };
|
|
1970
1977
|
const clear = args["clear"] === true;
|
|
1971
1978
|
const agent = args["agent"];
|
|
1972
|
-
|
|
1973
|
-
|
|
1979
|
+
const size = args["size"];
|
|
1980
|
+
const risky = args["risky"];
|
|
1981
|
+
if (size !== undefined && !isTaskSize(size))
|
|
1982
|
+
return { ok: false, message: "size is small, medium, or large" };
|
|
1983
|
+
if (risky !== undefined && typeof risky !== "boolean")
|
|
1984
|
+
return { ok: false, message: "risky is true or false" };
|
|
1985
|
+
const sized = ctx.store.refForId(ref.id)?.sizing ?? null;
|
|
1986
|
+
const sizeChange = size === undefined && risky === undefined ? null : { size: size ?? sized?.size ?? "medium", risky: risky ?? sized?.risky ?? false };
|
|
1987
|
+
if (risk === undefined && phase === null && sizeChange === null)
|
|
1988
|
+
return { ok: false, message: "say what changes: a risk, a size, or a role with an agent (or clear: true)" };
|
|
1974
1989
|
if (phase !== null && !clear && (agent === null || typeof agent !== "object"))
|
|
1975
1990
|
return { ok: false, message: "a role change names an agent from get_agents, or clear: true" };
|
|
1976
1991
|
if (phase === null && (clear || agent !== undefined))
|
|
@@ -2014,6 +2029,7 @@ export const MATE_TOOLS = [
|
|
|
2014
2029
|
taskTitle: task.title,
|
|
2015
2030
|
repoId: ref.repoId,
|
|
2016
2031
|
...(risk === undefined ? {} : { risk, riskConsequence: riskConsequence(risk) }),
|
|
2032
|
+
...(sizeChange === null ? {} : { size: sizeChange.size, risky: sizeChange.risky }),
|
|
2017
2033
|
...(phase === null ? {} : { phase, role: ROLE_WORD[phase] }),
|
|
2018
2034
|
...(chosen === null ? {} : { provider: chosen.provider, model: chosen.model }),
|
|
2019
2035
|
...(clear ? { clear: true } : {}),
|
|
@@ -2031,6 +2047,7 @@ export const MATE_TOOLS = [
|
|
|
2031
2047
|
kind: "agents",
|
|
2032
2048
|
task: taskId,
|
|
2033
2049
|
...(risk === undefined ? {} : { risk }),
|
|
2050
|
+
...(sizeChange === null ? {} : { size: sizeChange.size, risky: sizeChange.risky }),
|
|
2034
2051
|
...(phase === null ? {} : { role: ROLE_WORD[phase], ...(chosen === null ? { clear: true } : { agent: chosen }) }),
|
|
2035
2052
|
awaiting: view["approval"] === "approved" ? "the operator's confirmation — the current approval will then need renewing" : "the operator's confirmation",
|
|
2036
2053
|
},
|
package/dist/mate.js
CHANGED
|
@@ -9,7 +9,8 @@ import { redactSecretAssignments } from "./builder.js";
|
|
|
9
9
|
import { MATE_CONTRACT } from "./mate-contract.js";
|
|
10
10
|
import { MATE_MAX_PROPOSALS_PER_TURN, MATE_TOOL_SCHEMAS, executeMateTool, isMateTool, mateViewContextFor, projectLabelForMate, redactForMate, toolResultBytes } from "./mate-tools.js";
|
|
11
11
|
import { composeSubscriptionMatePrompt, performSubscriptionMateRequest } from "./subscription-chat.js";
|
|
12
|
-
import { leadContext } from './lead-context.js';
|
|
12
|
+
import { LEAD_REPLY_LIMITS, leadContext } from './lead-context.js';
|
|
13
|
+
import { shortenAsk } from './text-limits.js';
|
|
13
14
|
import { envValue } from "./names.js";
|
|
14
15
|
import { deliverableClaim, deliverableRepair, dropDeliverableClaims, replyCarriesDeliverable } from "./reply-shape.js";
|
|
15
16
|
export const MATE_MESSAGE_MAX_CHARS = 2_000;
|
|
@@ -317,6 +318,9 @@ export async function runMateTurn(input) {
|
|
|
317
318
|
let shownControl = false;
|
|
318
319
|
/** The turn's one repair step for a reply that claims an attachment it does not carry. */
|
|
319
320
|
let repaired = false;
|
|
321
|
+
/** The turn's one shorten step for a reply over its channel's message limit (LEAD_REPLY_LIMITS); still over, it is split. */
|
|
322
|
+
let shortened = false;
|
|
323
|
+
const replyLimit = input.channel === undefined ? null : LEAD_REPLY_LIMITS[input.channel];
|
|
320
324
|
const unbacked = (text) => {
|
|
321
325
|
const claim = deliverableClaim(text);
|
|
322
326
|
if (claim === null || shownControl || replyCarriesDeliverable(text) || store.listMateTurnEvidence(turnId).length > 0)
|
|
@@ -447,7 +451,14 @@ export async function runMateTurn(input) {
|
|
|
447
451
|
history.push({ role: "operator", text: deliverableRepair(claim) });
|
|
448
452
|
continue;
|
|
449
453
|
}
|
|
450
|
-
|
|
454
|
+
const candidate = claim === null ? answer.text : dropDeliverableClaims(answer.text);
|
|
455
|
+
if (replyLimit !== null && candidate.length > replyLimit && !shortened && steps < maxSteps) {
|
|
456
|
+
shortened = true;
|
|
457
|
+
history.push({ role: "assistant", text: answer.text, calls: [] });
|
|
458
|
+
history.push({ role: "operator", text: shortenAsk([{ field: "Your reply", limit: replyLimit, length: candidate.length }]) });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
reply = candidate;
|
|
451
462
|
break;
|
|
452
463
|
}
|
|
453
464
|
if (answer.calls.length > MATE_MAX_CALLS_PER_STEP)
|
package/dist/needs-you.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/** What a person is asked for. */
|
|
9
9
|
export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "confirm-stopped" | "check-stopped" | "hold" | "choose-project" | "define-task" | "fix-request" | "choose-agent" | "fix-dependency" | "add-requirement" | "connect-builder" | "start-builder" | "vanished" | "review-result" | "rebuild" | "earlier-version" | "other";
|
|
10
10
|
/** What the work waits for when no person can act. */
|
|
11
|
-
export type WaitKey = "build-stopping" | "other-computer" | "card-reply" | "card-time" | "card-ci";
|
|
11
|
+
export type WaitKey = "build-stopping" | "build-finishing" | "other-computer" | "card-reply" | "card-time" | "card-ci";
|
|
12
12
|
/** The action's code: the WorkAction that owns the act, or one of the acts a surface renders itself. */
|
|
13
13
|
export type NeedActionCode = "approve-scope" | "approve-card" | "answer-decision" | "inspect-decisions" | "sign-in" | "confirm-stopped" | "inspect-hold" | "place-task" | "write-scope" | "select-agent" | "repair-dependency" | "repair-capability" | "start-worker" | "reconcile-run" | "open-result" | "retry-task" | "inspect-task";
|
|
14
14
|
export type NeedAction = {
|
|
@@ -73,7 +73,7 @@ export declare function waitSentence(key: WaitKey | undefined, reason: string |
|
|
|
73
73
|
* `unknown`: Toolroll can't check, so the person confirms they checked. */
|
|
74
74
|
export declare function processNeedOf(fact: {
|
|
75
75
|
run: number;
|
|
76
|
-
kind: "open" | "alive" | "elsewhere" | "unprovable" | "unknown";
|
|
76
|
+
kind: "open" | "alive" | "elsewhere" | "unprovable" | "unknown" | "settling";
|
|
77
77
|
} | null): {
|
|
78
78
|
need: "confirm-stopped" | "check-stopped";
|
|
79
79
|
build: number;
|
package/dist/needs-you.js
CHANGED
|
@@ -79,6 +79,7 @@ export const NEED_ASK = {
|
|
|
79
79
|
"add-requirement": "unblock", "connect-builder": "unblock", "start-builder": "unblock", vanished: "unblock", "earlier-version": "unblock", other: "unblock",
|
|
80
80
|
};
|
|
81
81
|
export const WAITS = {
|
|
82
|
+
"build-finishing": () => "Finishing up; this clears on its own.",
|
|
82
83
|
"build-stopping": context => `Waiting for ${build(context)} to stop. Nothing is needed from you.`,
|
|
83
84
|
"other-computer": context => `Waiting for the computer that ran ${build(context)} to confirm it stopped.`,
|
|
84
85
|
"card-reply": () => "Waiting for a reply.",
|
|
@@ -110,6 +111,8 @@ export function waitSentence(key, reason, context = {}) {
|
|
|
110
111
|
export function processNeedOf(fact) {
|
|
111
112
|
if (fact === null)
|
|
112
113
|
return null;
|
|
114
|
+
if (fact.kind === "settling")
|
|
115
|
+
return { wait: "build-finishing", build: fact.run };
|
|
113
116
|
if (fact.kind === "unprovable")
|
|
114
117
|
return { need: "confirm-stopped", build: fact.run };
|
|
115
118
|
if (fact.kind === "unknown")
|
package/dist/operate.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type IntegrationIo } from "./integrations.js";
|
|
2
2
|
import { type OnboardIo } from "./agent-onboard.js";
|
|
3
|
+
import { type Sizer } from "./task-sizing.js";
|
|
3
4
|
import { type TriggerIo } from "./flow-triggers.js";
|
|
4
5
|
import type { StepIo } from "./flow-steps.js";
|
|
5
6
|
import type { FetchLike } from "./flow-share.js";
|
|
@@ -83,6 +84,8 @@ export type OperateOptions = {
|
|
|
83
84
|
env?: Record<string, string | undefined>;
|
|
84
85
|
openBrowser?: (url: string) => void;
|
|
85
86
|
};
|
|
87
|
+
/** Injected by tests: the classifier that sizes filed tasks (none by default under test). */
|
|
88
|
+
filingSizer?: Sizer;
|
|
86
89
|
};
|
|
87
90
|
export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
|
|
88
91
|
export declare const OPERATE_HELP: string;
|
package/dist/operate.js
CHANGED
|
@@ -26,6 +26,8 @@ import { checkIntegrations, integrationsBrokenLine, integrationsNow, renderInteg
|
|
|
26
26
|
import { buildHandoff, handoffLines, loginAccount, runOnboard } from "./agent-onboard.js";
|
|
27
27
|
import { backupFiles, backupFolderOf, backupOwner, backupNow, restoreDatabase, startBackups } from "./backup.js";
|
|
28
28
|
import { pushLimitSink } from "./provider-limits.js";
|
|
29
|
+
import { heuristicSizing, installFilingSizer, ownerSizer, refineFiledSizing, settleSizings } from "./task-sizing.js";
|
|
30
|
+
import { tierLines, tierReport } from "./tier-report.js";
|
|
29
31
|
import { limitsView } from "./limits-ui.js";
|
|
30
32
|
import { startCodexLimits } from "./codex-limits.js";
|
|
31
33
|
import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
|
|
@@ -176,7 +178,7 @@ import { TEMPLATES, templateByName } from "./templates.js";
|
|
|
176
178
|
import { planTournament, planComparison, contestNoun, jointApprovalDigest, admitContest, crossReadyBarrier, finalizeContestant, recoverContests, maybeAggregate as contestMaybeAggregate, sweepContestCleanup, escalateOverdueContests } from "./contest.js";
|
|
177
179
|
import { isDirectChatProvider, isSubscriptionChatProvider, priceOf, PRICED_MODELS } from "./converse.js";
|
|
178
180
|
import { resolvePhaseAgent, resolveScopeProfile, resolveScopeChain, resolveRouteCandidates, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
179
|
-
import { isRiskLevel, legOf, projectRoute, riskConsequence, routeDigestOf, routeWords, RISK_LEVELS, PHASES as ROUTE_PHASES } from "./phase-routing.js";
|
|
181
|
+
import { isRiskLevel, isTaskSize, legOf, projectRoute, riskConsequence, routeDigestOf, routeWords, RISK_LEVELS, TASK_SIZES, PHASES as ROUTE_PHASES } from "./phase-routing.js";
|
|
180
182
|
import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
|
|
181
183
|
import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
|
|
182
184
|
import { effectiveConcurrency, maySlotTake, parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, ProjectPasses, projectConcurrency, saveProjectConcurrency, savedProjectConcurrency } from "./project-concurrency.js";
|
|
@@ -423,6 +425,7 @@ Capabilities — what the work needs, recorded and probed, never valued
|
|
|
423
425
|
the repo, asks you questions, and
|
|
424
426
|
proposes a scope you approve
|
|
425
427
|
toolroll task route <id> [--risk routine|elevated|high]
|
|
428
|
+
[--size small|medium|large] [--risky yes|no]
|
|
426
429
|
[--phase plan|build|repair --provider <p> [--model <m>] | --clear-phase <phase>]
|
|
427
430
|
--as <you> --token <t> which agent plans, builds, and repairs
|
|
428
431
|
this task, with the reason for
|
|
@@ -500,6 +503,17 @@ Agents — which provider and model each phase runs on
|
|
|
500
503
|
the STRONG agent high-risk, strict,
|
|
501
504
|
screenshot-proof, and automerge
|
|
502
505
|
routes reach for; never inferred
|
|
506
|
+
toolroll config set build --tier light --provider <p> --model <m>
|
|
507
|
+
[--repo <path>] --as <you> --token <t>
|
|
508
|
+
the fast agent small changes build
|
|
509
|
+
on, with no plan
|
|
510
|
+
toolroll config set <plan|build> [--tier light|strong] --also
|
|
511
|
+
--provider <p> --model <m> [--repo <path>] --as <you> --token <t>
|
|
512
|
+
another provider's agent on the same
|
|
513
|
+
tier; each task runs the one whose
|
|
514
|
+
plan has more room
|
|
515
|
+
toolroll config clear <plan|build> [--tier light|strong] --also
|
|
516
|
+
[--provider <p>] [--repo <path>] --as <you> --token <t>
|
|
503
517
|
toolroll config clear <phase> [--repo <path>] --as <you> --token <t>
|
|
504
518
|
|
|
505
519
|
toolroll setup show --repo <path> what a fresh checkout runs first
|
|
@@ -632,7 +646,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
632
646
|
"provider", "plan-model", "plan-provider", "public-url", "editor",
|
|
633
647
|
"command", "timeout-seconds", "setup-digest", "stop-grace", "title", "name", "every", "lines",
|
|
634
648
|
"label", "reviewers", "limit", "role", "key-file", "weekly-usd", "daily-turns", "per-hour", "token-file", "race", "compare", "race-per-usd", "race-total-usd", "race-count", "race-agents", "budget-usd", "build-usd", "sync-max-age", "merge-method",
|
|
635
|
-
"phase", "risk", "tier", "clear-phase",
|
|
649
|
+
"phase", "risk", "tier", "clear-phase", "size", "risky",
|
|
636
650
|
"run", "containment", "agent",
|
|
637
651
|
// onboard: the starter flows to switch on.
|
|
638
652
|
"starter",
|
|
@@ -664,6 +678,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
664
678
|
"revoke",
|
|
665
679
|
// task merge: merge without waiting for the full check (Quick or Off results).
|
|
666
680
|
"anyway",
|
|
681
|
+
// config set/clear: another provider's candidate on the same tier.
|
|
682
|
+
"also",
|
|
667
683
|
]);
|
|
668
684
|
export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
669
685
|
const positional = [];
|
|
@@ -799,6 +815,9 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
799
815
|
}
|
|
800
816
|
// v105: a Claude turn anywhere in this command says its plan's usage windows; keep the latest.
|
|
801
817
|
const dropLimitSink = pushLimitSink(reading => store.recordProviderLimits(reading, new Date()));
|
|
818
|
+
// Filed tasks are sized by the owner's own fast classifier (never in tests, the demo, or with NO_TASK_CLASSIFIER).
|
|
819
|
+
const sizer = options.filingSizer ?? (process.env["VITEST"] !== undefined || envValue(process.env, "NO_TASK_CLASSIFIER") !== undefined || store.isDemo() ? null : ownerSizer());
|
|
820
|
+
const dropSizer = sizer === null ? () => { } : installFilingSizer(sizer);
|
|
802
821
|
// One `Date` per command is fine for a lookup and wrong for a pass that
|
|
803
822
|
// runs an agent for half an hour: leases granted, extended, and released
|
|
804
823
|
// with the same stale stamp. Injected time stays frozen — a test's clock
|
|
@@ -853,6 +872,9 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
853
872
|
return fail(write, json, command, "failed", describe(error), EXIT.failed);
|
|
854
873
|
}
|
|
855
874
|
finally {
|
|
875
|
+
// A classification still in flight (at most its five-second budget) lands before the database closes.
|
|
876
|
+
await settleSizings();
|
|
877
|
+
dropSizer();
|
|
856
878
|
dropLimitSink();
|
|
857
879
|
store.close();
|
|
858
880
|
}
|
|
@@ -5787,8 +5809,11 @@ async function configCommand(positional, flags, context) {
|
|
|
5787
5809
|
phase: one,
|
|
5788
5810
|
strong: candidates.ok ? candidates.candidates[one].strong : null,
|
|
5789
5811
|
}));
|
|
5812
|
+
const alsoRows = [...store.listPhaseTierAlternates(INSTALLATION_SCOPE), ...(scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierAlternates(scope))];
|
|
5813
|
+
// A light planner from before light became build-only is never used: said, with how to clear it.
|
|
5814
|
+
const unusedLight = [INSTALLATION_SCOPE, ...(scope === INSTALLATION_SCOPE ? [] : [scope])].filter(one => store.phaseTierConfig(one, "plan", "light") !== null);
|
|
5790
5815
|
if (json) {
|
|
5791
|
-
write(envelopeJson({ ok: true, command: "config show", installation, project, resolved, fallback, strong, installationStrong: store.listPhaseTierConfig(INSTALLATION_SCOPE), projectStrong: scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierConfig(scope) }));
|
|
5816
|
+
write(envelopeJson({ ok: true, command: "config show", installation, project, resolved, fallback, strong, installationStrong: store.listPhaseTierConfig(INSTALLATION_SCOPE), projectStrong: scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierConfig(scope), installationAlso: store.listPhaseTierAlternates(INSTALLATION_SCOPE), projectAlso: scope === INSTALLATION_SCOPE ? [] : store.listPhaseTierAlternates(scope) }));
|
|
5792
5817
|
return EXIT.ok;
|
|
5793
5818
|
}
|
|
5794
5819
|
write(`Effective phase agents${scope === INSTALLATION_SCOPE ? "" : ` for ${scope}`}:`);
|
|
@@ -5807,6 +5832,22 @@ async function configCommand(positional, flags, context) {
|
|
|
5807
5832
|
}
|
|
5808
5833
|
write(" set one with: toolroll config set <phase> --tier strong --provider <p> --model <m> --as <you> --token <t>");
|
|
5809
5834
|
write("");
|
|
5835
|
+
write(" light tier (small changes build on this, with no plan):");
|
|
5836
|
+
const light = candidates.ok ? candidates.candidates.build.light ?? null : null;
|
|
5837
|
+
write(` ${"build".padEnd(8)} ${light === null ? "none configured — small changes keep the default above and say so" : `${light.provider} · ${light.model} [${light.source}]`}`);
|
|
5838
|
+
for (const one of unusedLight)
|
|
5839
|
+
write(` ${"plan".padEnd(8)} not used — a small change makes no plan; \`config clear plan --tier light${one === INSTALLATION_SCOPE ? "" : ` --repo ${one}`}\` removes it`);
|
|
5840
|
+
write(" set one with: toolroll config set build --tier light --provider <p> --model <m> --as <you> --token <t>");
|
|
5841
|
+
write("");
|
|
5842
|
+
if (candidates.ok) {
|
|
5843
|
+
const also = ["plan", "build"].flatMap(one => Object.entries(candidates.candidates[one].alternates ?? {}).flatMap(([tier, list]) => (list ?? []).map(spec => ` ${one.padEnd(8)} ${tier.padEnd(8)} also ${spec.provider} · ${spec.model} [${spec.source}]`)));
|
|
5844
|
+
if (also.length > 0 || alsoRows.length > 0) {
|
|
5845
|
+
write(" other providers on a tier (each task runs the one whose plan has more room):");
|
|
5846
|
+
for (const line of also)
|
|
5847
|
+
write(line);
|
|
5848
|
+
write("");
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5810
5851
|
write(" repair note: the repair PROVIDER always inherits the build it mends — only its model is configurable.");
|
|
5811
5852
|
if (fallback.length > 0) {
|
|
5812
5853
|
write("");
|
|
@@ -6046,11 +6087,57 @@ async function configCommand(positional, flags, context) {
|
|
|
6046
6087
|
// reaches for when risk, quality, evidence, or publication demand it.
|
|
6047
6088
|
// Authenticated and audited exactly like the routine row; existing
|
|
6048
6089
|
// approvals are untouched — a sealed route never re-resolves.
|
|
6090
|
+
// The LIGHT tier (v2): the fast agent a small change builds on.
|
|
6049
6091
|
const tierGiven = text(flags, "tier");
|
|
6050
|
-
if (tierGiven !== undefined && tierGiven !== "strong") {
|
|
6051
|
-
return fail(write, json, `config ${action}`, "usage", "--tier is `strong` — the routine tier is the plain phase row", EXIT.usage);
|
|
6092
|
+
if (tierGiven !== undefined && tierGiven !== "strong" && tierGiven !== "light") {
|
|
6093
|
+
return fail(write, json, `config ${action}`, "usage", "--tier is `light` or `strong` — the routine tier is the plain phase row", EXIT.usage);
|
|
6094
|
+
}
|
|
6095
|
+
// A light planner would never run (a small change makes no plan): refused rather than kept and ignored.
|
|
6096
|
+
// Clearing one left from before stays possible.
|
|
6097
|
+
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also"))) {
|
|
6098
|
+
return fail(write, json, `config ${action}`, "usage", "--tier light is for build only — a small change makes no plan, and repairs resume the builder's session", EXIT.usage);
|
|
6099
|
+
}
|
|
6100
|
+
// Another provider's candidate on the same tier (`--also`): one per provider, and at filing the one whose plan
|
|
6101
|
+
// has more room runs.
|
|
6102
|
+
if (flag(flags, "also")) {
|
|
6103
|
+
if (phase !== "plan" && phase !== "build") {
|
|
6104
|
+
return fail(write, json, `config ${action}`, "usage", "--also is for plan or build — repairs follow the build's provider", EXIT.usage);
|
|
6105
|
+
}
|
|
6106
|
+
const tier = (tierGiven ?? "routine");
|
|
6107
|
+
const alsoProvider = text(flags, "provider");
|
|
6108
|
+
if (alsoProvider !== undefined && !isProviderId(alsoProvider)) {
|
|
6109
|
+
return fail(write, json, `config ${action}`, "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
6110
|
+
}
|
|
6111
|
+
if (action === "clear") {
|
|
6112
|
+
const removed = store.clearPhaseTierAlternates(scope, phase, tier, alsoProvider ?? null);
|
|
6113
|
+
return succeed(write, json, "config clear", { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
|
|
6114
|
+
removed > 0 ? `Removed ${removed} other-provider ${tier} ${phase} agent${removed === 1 ? "" : "s"} at ${scope}; sealed routes are untouched.` : `No other-provider ${tier} ${phase} agent${alsoProvider === undefined ? "" : ` on ${alsoProvider}`} was configured at ${scope}.`,
|
|
6115
|
+
]);
|
|
6116
|
+
}
|
|
6117
|
+
const alsoModel = text(flags, "model");
|
|
6118
|
+
if (alsoProvider === undefined || alsoModel === undefined) {
|
|
6119
|
+
return fail(write, json, "config set", "usage", "--also names an exact --provider and --model — approvals bind exact routing", EXIT.usage);
|
|
6120
|
+
}
|
|
6121
|
+
const alsoValid = validateSpec({ provider: alsoProvider, model: alsoModel });
|
|
6122
|
+
if (!alsoValid.ok)
|
|
6123
|
+
return fail(write, json, "config set", "invalid", alsoValid.problem, EXIT.usage);
|
|
6124
|
+
store.setPhaseTierAlternate(scope, phase, tier, alsoProvider, alsoModel, acting.name, clock());
|
|
6125
|
+
const resolvedNow = resolveRouteCandidates(store, scope === INSTALLATION_SCOPE ? null : scope);
|
|
6126
|
+
const own = !resolvedNow.ok ? null : tier === "routine" ? resolvedNow.candidates[phase].routine : tier === "light" ? resolvedNow.candidates[phase].light ?? null : resolvedNow.candidates[phase].strong;
|
|
6127
|
+
return succeed(write, json, "config set", { scope, phase, tier, also: true, provider: alsoProvider, model: alsoModel }, () => [
|
|
6128
|
+
`${tier} ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} can also run ${alsoProvider} · ${alsoModel}, set by ${acting.name}.`,
|
|
6129
|
+
own !== null && own.provider === alsoProvider && !own.source.includes("also")
|
|
6130
|
+
? ` ! the ${tier} ${phase} agent already runs ${alsoProvider} (${own.model}) — this one is not used while it does.`
|
|
6131
|
+
: " tasks filed from now on run on whichever provider's plan has more room; sealed routes are untouched.",
|
|
6132
|
+
]);
|
|
6052
6133
|
}
|
|
6053
6134
|
if (action === "clear") {
|
|
6135
|
+
if (tierGiven === "light") {
|
|
6136
|
+
const clearedLight = store.clearPhaseTierConfig(scope, phase, "light");
|
|
6137
|
+
return succeed(write, json, "config clear", { scope, phase, tier: "light", cleared: clearedLight }, () => [
|
|
6138
|
+
clearedLight ? `Cleared the light ${phase} agent at ${scope} — small changes keep the default and say so; sealed routes are untouched.` : `No light ${phase} agent was configured at ${scope}.`,
|
|
6139
|
+
]);
|
|
6140
|
+
}
|
|
6054
6141
|
if (tierGiven === "strong") {
|
|
6055
6142
|
const clearedStrong = store.clearPhaseTierConfig(scope, phase, "strong");
|
|
6056
6143
|
return succeed(write, json, "config clear", { scope, phase, tier: "strong", cleared: clearedStrong }, () => [
|
|
@@ -6071,6 +6158,16 @@ async function configCommand(positional, flags, context) {
|
|
|
6071
6158
|
if (!valid.ok) {
|
|
6072
6159
|
return fail(write, json, "config set", "invalid", valid.problem, EXIT.usage);
|
|
6073
6160
|
}
|
|
6161
|
+
if (tierGiven === "light") {
|
|
6162
|
+
if (modelGiven === null) {
|
|
6163
|
+
return fail(write, json, "config set", "usage", `a light ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
6164
|
+
}
|
|
6165
|
+
store.setPhaseTierConfig(scope, phase, "light", providerGiven, modelGiven, acting.name, clock());
|
|
6166
|
+
return succeed(write, json, "config set", { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
|
|
6167
|
+
`light ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven} · ${modelGiven}, set by ${acting.name}.`,
|
|
6168
|
+
" small changes filed from now on build on it with no plan; sealed routes are untouched.",
|
|
6169
|
+
]);
|
|
6170
|
+
}
|
|
6074
6171
|
if (tierGiven === "strong") {
|
|
6075
6172
|
if (phase === "review" && providerGiven === "gemini") {
|
|
6076
6173
|
return fail(write, json, "config set", "invalid", "gemini has no isolation posture for the review phase yet — name claude or codex as the strong reviewer", EXIT.usage);
|
|
@@ -11013,7 +11110,7 @@ async function syncCommand(flags, context) {
|
|
|
11013
11110
|
* separate acts by, usually, separate parties: an agent may draft a scope, and
|
|
11014
11111
|
* only a person may agree to it.
|
|
11015
11112
|
*/
|
|
11016
|
-
function scopeTask(positional, flags, context) {
|
|
11113
|
+
async function scopeTask(positional, flags, context) {
|
|
11017
11114
|
const { store, write, json, now } = context;
|
|
11018
11115
|
const id = positional[0];
|
|
11019
11116
|
const goal = text(flags, "goal");
|
|
@@ -11168,6 +11265,27 @@ function scopeTask(positional, flags, context) {
|
|
|
11168
11265
|
const actor = asGiven !== undefined && tokenGiven !== undefined && authenticateApprover(store, asGiven, tokenGiven).ok
|
|
11169
11266
|
? asGiven
|
|
11170
11267
|
: null;
|
|
11268
|
+
// Size the new goal before filing its route. A replay must not classify
|
|
11269
|
+
// again or change the task behind the original answer. This probe does
|
|
11270
|
+
// not record a result; the composite filing below still owns the receipt.
|
|
11271
|
+
let refinement = null;
|
|
11272
|
+
store.replay(mutationFrom(flags, now), "task-scope-filed", () => {
|
|
11273
|
+
const ref = store.lookupRef(id);
|
|
11274
|
+
const previous = store.getScope(id);
|
|
11275
|
+
if (ref === null || ref.deliverable === "report" || candidateGiven !== undefined ||
|
|
11276
|
+
ref.sizing?.source === "person" || approvalOf(previous).approved ||
|
|
11277
|
+
previous?.proposedVia === "coordinator" || store.filedViaOf(id)?.startsWith("mcp:") ||
|
|
11278
|
+
((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null))
|
|
11279
|
+
return null;
|
|
11280
|
+
const input = { title: store.getTask(id).title, goal, outOfScope: text(flags, "not") ?? null, touches };
|
|
11281
|
+
const sized = store.applySizing(id, heuristicSizing(input), { followPlanning: true }, now);
|
|
11282
|
+
if (sized.ok)
|
|
11283
|
+
refinement = refineFiledSizing(store, id, input, true, context.clock);
|
|
11284
|
+
return null;
|
|
11285
|
+
}, () => false);
|
|
11286
|
+
// Wait for bounded classification before filing so any approval in the
|
|
11287
|
+
// transaction below binds the final size and route.
|
|
11288
|
+
await refinement;
|
|
11171
11289
|
// ONE replayed composite (surfaces round 1, finding 3): the filing, any
|
|
11172
11290
|
// race/comparison terms, AND the mode seal record as a single operation —
|
|
11173
11291
|
// a replayed key returns the FIRST answer whole instead of re-sealing
|
|
@@ -11348,7 +11466,9 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11348
11466
|
const providerGiven = text(flags, "provider");
|
|
11349
11467
|
const modelGiven = text(flags, "model");
|
|
11350
11468
|
const digestGiven = text(flags, "digest");
|
|
11351
|
-
const
|
|
11469
|
+
const sizeGiven = text(flags, "size");
|
|
11470
|
+
const riskyGiven = text(flags, "risky");
|
|
11471
|
+
const editing = riskGiven !== undefined || phaseGiven !== undefined || clearGiven !== undefined || sizeGiven !== undefined || riskyGiven !== undefined;
|
|
11352
11472
|
const show = () => {
|
|
11353
11473
|
const scope = store.getScope(id);
|
|
11354
11474
|
const current = store.refFor(BUILT_IN, id);
|
|
@@ -11356,7 +11476,7 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11356
11476
|
const readiness = store.readinessLookupFor(ref.repo, ref.assignedRunner, clock());
|
|
11357
11477
|
const view = taskRouteView(routed, readiness);
|
|
11358
11478
|
const projection = routed !== null && routed.kind === "route" ? projectRoute(routed.route, readiness) : null;
|
|
11359
|
-
return succeed(write, json, "task route", { id, risk: current.riskLevel ?? scope?.riskLevel ?? "routine", riskConsequence: riskConsequence(current.riskLevel ?? scope?.riskLevel ?? "routine"), source: routed !== null && routed.kind === "route" ? routed.source : routed?.kind ?? null, route: view, overrides: current.routeOverrides ?? [], digest: scope?.digest ?? null, approval: approvalOf(scope) }, () => routed === null
|
|
11479
|
+
return succeed(write, json, "task route", { id, risk: current.riskLevel ?? scope?.riskLevel ?? "routine", riskConsequence: riskConsequence(current.riskLevel ?? scope?.riskLevel ?? "routine"), size: current.sizing ?? null, source: routed !== null && routed.kind === "route" ? routed.source : routed?.kind ?? null, route: view, overrides: current.routeOverrides ?? [], digest: scope?.digest ?? null, approval: approvalOf(scope) }, () => routed === null
|
|
11360
11480
|
? [`${id}: no route can be recommended yet — ${scope === null ? "place the task in a repository and file a scope" : scope.unresolvedReason ?? "the phase configuration cannot resolve"}`]
|
|
11361
11481
|
: [
|
|
11362
11482
|
...(projection === null
|
|
@@ -11393,6 +11513,17 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11393
11513
|
if (riskGiven !== undefined && !isRiskLevel(riskGiven)) {
|
|
11394
11514
|
return fail(write, json, "task route", "usage", `--risk is one of ${RISK_LEVELS.join(", ")}`, EXIT.usage);
|
|
11395
11515
|
}
|
|
11516
|
+
if (sizeGiven !== undefined && !isTaskSize(sizeGiven)) {
|
|
11517
|
+
return fail(write, json, "task route", "usage", `--size is one of ${TASK_SIZES.join(", ")}`, EXIT.usage);
|
|
11518
|
+
}
|
|
11519
|
+
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no") {
|
|
11520
|
+
return fail(write, json, "task route", "usage", "--risky is yes or no", EXIT.usage);
|
|
11521
|
+
}
|
|
11522
|
+
// A person's size outranks the classifier's; an unnamed half keeps what the task has.
|
|
11523
|
+
const sizeEdit = sizeGiven === undefined && riskyGiven === undefined ? undefined : {
|
|
11524
|
+
size: sizeGiven ?? ref.sizing?.size ?? "medium",
|
|
11525
|
+
risky: riskyGiven === undefined ? ref.sizing?.risky ?? false : riskyGiven === "yes",
|
|
11526
|
+
};
|
|
11396
11527
|
if (phaseGiven !== undefined && clearGiven !== undefined) {
|
|
11397
11528
|
return fail(write, json, "task route", "usage", "say --phase … --provider … --model … OR --clear-phase …, not both", EXIT.usage);
|
|
11398
11529
|
}
|
|
@@ -11420,6 +11551,7 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11420
11551
|
return authenticated.ok ? { ok: true } : { ok: false, reason: authenticated.reason };
|
|
11421
11552
|
},
|
|
11422
11553
|
...(riskGiven === undefined ? {} : { risk: riskGiven }),
|
|
11554
|
+
...(sizeEdit === undefined ? {} : { size: sizeEdit }),
|
|
11423
11555
|
...(phase === undefined
|
|
11424
11556
|
? {}
|
|
11425
11557
|
: { override: phaseGiven !== undefined ? { phase: phase, provider: providerGiven, model: modelGiven } : { phase: phase, clear: true } }),
|
|
@@ -12215,11 +12347,14 @@ function spendCommand(flags, context) {
|
|
|
12215
12347
|
// v105: subscription work is $0; what binds it is its plan's windows, as the provider last said.
|
|
12216
12348
|
const limits = context.store.providerLimits();
|
|
12217
12349
|
const windows = limitsView(limits, [], { project: repo => basename(repo), teammate: id => String(id) }, context.clock())?.tiles ?? [];
|
|
12218
|
-
|
|
12350
|
+
// Sized routing: each tier's tasks, time to a result and plan use, and what the tiers saved.
|
|
12351
|
+
const tiers = tierReport(context.store.handle, month.from, month.to, items);
|
|
12352
|
+
return succeed(context.write, context.json, command, { month: month.name, totalMicrousd: total, unpriced, items: items.length, budgets, limits, tiers: { rows: tiers.rows, saved: tiers.saved } }, () => [
|
|
12219
12353
|
`${month.name}: ${spendUsd(total)} (${items.length} pieces of work${unpriced > 0 ? `, ${unpriced} unpriced` : ""}; subscription work is $0)`,
|
|
12220
12354
|
...windows.map(tile => ` ${`${tile.name} ${tile.window}`.padEnd(24)} ${tile.value}% · ${tile.detail}`),
|
|
12221
12355
|
...[...byProject.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([repo, microusd]) => ` ${basename(repo).padEnd(24)} ${spendUsd(microusd)}`),
|
|
12222
12356
|
...budgets.map(one => ` budget ${budgetLabel(one, names.get(Number(one.key))).replace(/'s$/, "").padEnd(24)} ${spendUsd(one.spentMicrousd)} of ${spendUsd(one.limitMicrousd)} (${one.percent}%)${one.hardStop ? "" : " alerts only"}`),
|
|
12357
|
+
...tierLines(tiers, spendUsd),
|
|
12223
12358
|
]);
|
|
12224
12359
|
}
|
|
12225
12360
|
/** `backup now | list` (sprint 8): back the database up now, or list backups and how the latest ones went. */
|