toolroll 0.9.27 → 0.9.28
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 +14 -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/mate-doors.js +8 -2
- package/dist/mate-tools.js +27 -10
- package/dist/mate.js +13 -2
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +122 -8
- package/dist/phase-routing.d.ts +86 -5
- package/dist/phase-routing.js +228 -16
- 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 +60 -10
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +64 -5
- package/dist/store.js +192 -7
- 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.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/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 { installFilingSizer, ownerSizer, 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);
|
|
@@ -11348,7 +11445,9 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11348
11445
|
const providerGiven = text(flags, "provider");
|
|
11349
11446
|
const modelGiven = text(flags, "model");
|
|
11350
11447
|
const digestGiven = text(flags, "digest");
|
|
11351
|
-
const
|
|
11448
|
+
const sizeGiven = text(flags, "size");
|
|
11449
|
+
const riskyGiven = text(flags, "risky");
|
|
11450
|
+
const editing = riskGiven !== undefined || phaseGiven !== undefined || clearGiven !== undefined || sizeGiven !== undefined || riskyGiven !== undefined;
|
|
11352
11451
|
const show = () => {
|
|
11353
11452
|
const scope = store.getScope(id);
|
|
11354
11453
|
const current = store.refFor(BUILT_IN, id);
|
|
@@ -11356,7 +11455,7 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11356
11455
|
const readiness = store.readinessLookupFor(ref.repo, ref.assignedRunner, clock());
|
|
11357
11456
|
const view = taskRouteView(routed, readiness);
|
|
11358
11457
|
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
|
|
11458
|
+
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
11459
|
? [`${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
11460
|
: [
|
|
11362
11461
|
...(projection === null
|
|
@@ -11393,6 +11492,17 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11393
11492
|
if (riskGiven !== undefined && !isRiskLevel(riskGiven)) {
|
|
11394
11493
|
return fail(write, json, "task route", "usage", `--risk is one of ${RISK_LEVELS.join(", ")}`, EXIT.usage);
|
|
11395
11494
|
}
|
|
11495
|
+
if (sizeGiven !== undefined && !isTaskSize(sizeGiven)) {
|
|
11496
|
+
return fail(write, json, "task route", "usage", `--size is one of ${TASK_SIZES.join(", ")}`, EXIT.usage);
|
|
11497
|
+
}
|
|
11498
|
+
if (riskyGiven !== undefined && riskyGiven !== "yes" && riskyGiven !== "no") {
|
|
11499
|
+
return fail(write, json, "task route", "usage", "--risky is yes or no", EXIT.usage);
|
|
11500
|
+
}
|
|
11501
|
+
// A person's size outranks the classifier's; an unnamed half keeps what the task has.
|
|
11502
|
+
const sizeEdit = sizeGiven === undefined && riskyGiven === undefined ? undefined : {
|
|
11503
|
+
size: sizeGiven ?? ref.sizing?.size ?? "medium",
|
|
11504
|
+
risky: riskyGiven === undefined ? ref.sizing?.risky ?? false : riskyGiven === "yes",
|
|
11505
|
+
};
|
|
11396
11506
|
if (phaseGiven !== undefined && clearGiven !== undefined) {
|
|
11397
11507
|
return fail(write, json, "task route", "usage", "say --phase … --provider … --model … OR --clear-phase …, not both", EXIT.usage);
|
|
11398
11508
|
}
|
|
@@ -11420,6 +11530,7 @@ async function routeTaskCommand(positional, flags, context) {
|
|
|
11420
11530
|
return authenticated.ok ? { ok: true } : { ok: false, reason: authenticated.reason };
|
|
11421
11531
|
},
|
|
11422
11532
|
...(riskGiven === undefined ? {} : { risk: riskGiven }),
|
|
11533
|
+
...(sizeEdit === undefined ? {} : { size: sizeEdit }),
|
|
11423
11534
|
...(phase === undefined
|
|
11424
11535
|
? {}
|
|
11425
11536
|
: { override: phaseGiven !== undefined ? { phase: phase, provider: providerGiven, model: modelGiven } : { phase: phase, clear: true } }),
|
|
@@ -12215,11 +12326,14 @@ function spendCommand(flags, context) {
|
|
|
12215
12326
|
// v105: subscription work is $0; what binds it is its plan's windows, as the provider last said.
|
|
12216
12327
|
const limits = context.store.providerLimits();
|
|
12217
12328
|
const windows = limitsView(limits, [], { project: repo => basename(repo), teammate: id => String(id) }, context.clock())?.tiles ?? [];
|
|
12218
|
-
|
|
12329
|
+
// Sized routing: each tier's tasks, time to a result and plan use, and what the tiers saved.
|
|
12330
|
+
const tiers = tierReport(context.store.handle, month.from, month.to, items);
|
|
12331
|
+
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
12332
|
`${month.name}: ${spendUsd(total)} (${items.length} pieces of work${unpriced > 0 ? `, ${unpriced} unpriced` : ""}; subscription work is $0)`,
|
|
12220
12333
|
...windows.map(tile => ` ${`${tile.name} ${tile.window}`.padEnd(24)} ${tile.value}% · ${tile.detail}`),
|
|
12221
12334
|
...[...byProject.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([repo, microusd]) => ` ${basename(repo).padEnd(24)} ${spendUsd(microusd)}`),
|
|
12222
12335
|
...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"}`),
|
|
12336
|
+
...tierLines(tiers, spendUsd),
|
|
12223
12337
|
]);
|
|
12224
12338
|
}
|
|
12225
12339
|
/** `backup now | list` (sprint 8): back the database up now, or list backups and how the latest ones went. */
|
package/dist/phase-routing.d.ts
CHANGED
|
@@ -56,11 +56,16 @@ export declare const MODEL_ID_SHAPE: RegExp;
|
|
|
56
56
|
/** The same exactness for callers outside this policy (the profile
|
|
57
57
|
* rehydrator): one shape, one definition. */
|
|
58
58
|
export declare function exactModelId(v: unknown): v is string;
|
|
59
|
-
|
|
59
|
+
/** v2 adds the task's size (small / medium / large, risky or not), the
|
|
60
|
+
* light tier, and plan headroom. A v1 route — sealed before sizing — still
|
|
61
|
+
* reads back exactly and keeps its own digest domain. */
|
|
62
|
+
export declare const ROUTE_VERSION = 2;
|
|
63
|
+
export type RouteVersion = 1 | 2;
|
|
60
64
|
/** The durable route ERA a scope row carries once it was filed under this
|
|
61
65
|
* policy: NULL on a row proven to predate v47 (legacy — the sealed profile
|
|
62
66
|
* alone governs), this value on every row filed since. A row with an era
|
|
63
|
-
* and no readable route is corrupt and fails closed.
|
|
67
|
+
* and no readable route is corrupt and fails closed. The era marks a ROUTED
|
|
68
|
+
* row; which route version it holds rides in the snapshot itself. */
|
|
64
69
|
export declare const ROUTE_ERA = 1;
|
|
65
70
|
export declare const PHASES: readonly Phase[];
|
|
66
71
|
/** The review leg remains readable in signed history but is no longer scheduled. */
|
|
@@ -68,6 +73,25 @@ export declare const ACTIVE_PHASES: readonly Phase[];
|
|
|
68
73
|
export type RiskLevel = "routine" | "elevated" | "high";
|
|
69
74
|
export declare const RISK_LEVELS: readonly RiskLevel[];
|
|
70
75
|
export declare function isRiskLevel(value: unknown): value is RiskLevel;
|
|
76
|
+
/** How big a change the task is, sized once at filing (by the classifier,
|
|
77
|
+
* by the description when it cannot answer, or by a person). */
|
|
78
|
+
export type TaskSize = "small" | "medium" | "large";
|
|
79
|
+
export declare const TASK_SIZES: readonly TaskSize[];
|
|
80
|
+
export type SizeSource = "classifier" | "heuristic" | "person";
|
|
81
|
+
export type TaskSizing = {
|
|
82
|
+
size: TaskSize;
|
|
83
|
+
risky: boolean;
|
|
84
|
+
source: SizeSource;
|
|
85
|
+
reason: string;
|
|
86
|
+
};
|
|
87
|
+
export declare function isTaskSize(value: unknown): value is TaskSize;
|
|
88
|
+
/** Whether a task makes no plan: a small change that is not risky, at
|
|
89
|
+
* routine risk. Elevated or high risk always plans, whatever the size. */
|
|
90
|
+
export declare function makesNoPlan(size: TaskSizing | null | undefined, risk: RiskLevel): boolean;
|
|
91
|
+
/** Where a size came from, in words. */
|
|
92
|
+
export declare function sizeSourceWords(source: SizeSource): string;
|
|
93
|
+
/** Strict read of a stored sizing: exact keys, known words, bounded reason. */
|
|
94
|
+
export declare function parseSizing(raw: unknown): TaskSizing | null;
|
|
71
95
|
export declare function riskTitle(risk: RiskLevel): string;
|
|
72
96
|
/**
|
|
73
97
|
* What declaring each risk level DOES, in one plain sentence — derived
|
|
@@ -95,7 +119,7 @@ export type ReadinessObservation = {
|
|
|
95
119
|
observedAt: string;
|
|
96
120
|
runner: string;
|
|
97
121
|
};
|
|
98
|
-
export type CandidateTier = "routine" | "strong";
|
|
122
|
+
export type CandidateTier = "light" | "routine" | "strong";
|
|
99
123
|
/** An exact agent: provider and model id, both required. */
|
|
100
124
|
export type ExactSpec = {
|
|
101
125
|
provider: ProviderId;
|
|
@@ -106,10 +130,21 @@ export type RouteCandidate = ExactSpec & {
|
|
|
106
130
|
source: string;
|
|
107
131
|
};
|
|
108
132
|
export type PhaseCandidates = {
|
|
133
|
+
/** The operator's named LIGHT (fast) builder (`config set build --tier
|
|
134
|
+
* light …`), or null/absent when none is configured. A small change
|
|
135
|
+
* builds on it. Never inferred; the plan phase has none (a small change
|
|
136
|
+
* makes no plan). */
|
|
137
|
+
light?: RouteCandidate | null;
|
|
109
138
|
routine: RouteCandidate;
|
|
110
139
|
/** The operator's named strong agent for this phase, or null when none
|
|
111
140
|
* is configured. Never inferred. */
|
|
112
141
|
strong: RouteCandidate | null;
|
|
142
|
+
/** v2: a tier may hold one more candidate per OTHER provider (`config
|
|
143
|
+
* set <phase> [--tier …] --also --provider … --model …`) — e.g. a
|
|
144
|
+
* routine builder on claude and on codex. At filing the one whose
|
|
145
|
+
* provider has more plan room runs. Each list holds providers other
|
|
146
|
+
* than its tier's own candidate, at most one each. */
|
|
147
|
+
alternates?: Partial<Record<CandidateTier, readonly RouteCandidate[]>>;
|
|
113
148
|
};
|
|
114
149
|
/** The repair phase's candidates: with no repair row configured the
|
|
115
150
|
* routine candidate is null and the leg INHERITS the build leg's exact
|
|
@@ -138,6 +173,30 @@ export type RouteEvidenceKind = "check" | "screenshot" | "changed-path" | "manua
|
|
|
138
173
|
* grant at all, a grant whose merges wait for a human (notify), or a live
|
|
139
174
|
* mode that merges by itself (automerge). */
|
|
140
175
|
export type PublicationAuthority = "none" | "notify" | "automerge";
|
|
176
|
+
/** How much of one provider's plan is used, as it last said: the 5-hour
|
|
177
|
+
* window and the weekly window, in percent; null when unknown. */
|
|
178
|
+
export type ProviderRoom = {
|
|
179
|
+
provider: ProviderId;
|
|
180
|
+
fiveHour: number | null;
|
|
181
|
+
weekly: number | null;
|
|
182
|
+
};
|
|
183
|
+
/** Above these a provider has no headroom: even a candidate one tier
|
|
184
|
+
* stronger on another provider takes the phase. Below them, a tier's
|
|
185
|
+
* candidates on different providers still go to the one with more room. */
|
|
186
|
+
export declare const HEADROOM_FIVE_HOUR_LIMIT = 80;
|
|
187
|
+
export declare const HEADROOM_WEEKLY_LIMIT = 90;
|
|
188
|
+
/** The latest plan-window readings as headroom: a window whose reset time
|
|
189
|
+
* has passed counts as empty; a reading older than its own window is
|
|
190
|
+
* unknown. Weekly is the plan-wide week (`seven_day`), else the fullest
|
|
191
|
+
* model-specific week. */
|
|
192
|
+
export declare function headroomFrom(limits: readonly {
|
|
193
|
+
provider: string;
|
|
194
|
+
window: string;
|
|
195
|
+
usedPercent: number;
|
|
196
|
+
windowMinutes: number | null;
|
|
197
|
+
resetsAt: string | null;
|
|
198
|
+
observedAt: string;
|
|
199
|
+
}[], now: Date): ProviderRoom[];
|
|
141
200
|
export type RouteInput = {
|
|
142
201
|
risk: RiskLevel;
|
|
143
202
|
qualityMode: QualityMode;
|
|
@@ -153,6 +212,13 @@ export type RouteInput = {
|
|
|
153
212
|
build?: ExactSpec | null;
|
|
154
213
|
repair?: ExactSpec | null;
|
|
155
214
|
};
|
|
215
|
+
/** How big the change is (v2): small builds on the light tier with no
|
|
216
|
+
* plan; large or risky plans and builds on the strong tier. */
|
|
217
|
+
size?: TaskSizing | null;
|
|
218
|
+
/** Each provider's plan headroom at filing (v2): when a tier has
|
|
219
|
+
* candidates on more than one provider, the one with more room runs; a
|
|
220
|
+
* provider past its limits gives way even to a stronger tier. */
|
|
221
|
+
headroom?: readonly ProviderRoom[];
|
|
156
222
|
};
|
|
157
223
|
export type RouteLeg = {
|
|
158
224
|
phase: Phase;
|
|
@@ -178,7 +244,7 @@ export type RouteLeg = {
|
|
|
178
244
|
problem: string | null;
|
|
179
245
|
};
|
|
180
246
|
export type PhaseRoute = {
|
|
181
|
-
version:
|
|
247
|
+
version: RouteVersion;
|
|
182
248
|
risk: RiskLevel;
|
|
183
249
|
qualityMode: QualityMode;
|
|
184
250
|
publication: PublicationAuthority;
|
|
@@ -192,6 +258,9 @@ export type PhaseRoute = {
|
|
|
192
258
|
demands: string[];
|
|
193
259
|
legs: RouteLeg[];
|
|
194
260
|
overrides: RouteOverride[];
|
|
261
|
+
/** The task's size as it was routed (v2); null on a v1 route or an
|
|
262
|
+
* unsized task. */
|
|
263
|
+
size: TaskSizing | null;
|
|
195
264
|
};
|
|
196
265
|
export declare function specWords(spec: {
|
|
197
266
|
provider: string;
|
|
@@ -259,9 +328,18 @@ export type RouteProjection = {
|
|
|
259
328
|
halted: boolean;
|
|
260
329
|
/** Every stated leg problem, phase-prefixed. */
|
|
261
330
|
problems: string[];
|
|
331
|
+
/** The size the route was made for (v2), or null. */
|
|
332
|
+
size: TaskSizing | null;
|
|
333
|
+
/** The plain line an approval card leads with — "Small change: fast
|
|
334
|
+
* model, no plan" — or null on an unsized route. */
|
|
335
|
+
sizeWords: string | null;
|
|
336
|
+
/** Why it was sized so, and by whom, or null. */
|
|
337
|
+
sizeReason: string | null;
|
|
262
338
|
};
|
|
263
339
|
/** What actually runs: stronger agents only when a leg draws from one. */
|
|
264
340
|
export declare function postureWords(route: Pick<PhaseRoute, "legs">): string;
|
|
341
|
+
/** The size line, plainly: what the size does to this route. */
|
|
342
|
+
export declare function sizeWords(route: Pick<PhaseRoute, "legs" | "size" | "risk">): string | null;
|
|
265
343
|
export declare function readinessWords(state: ReadinessState, reason: string | null): string;
|
|
266
344
|
export declare function chosenWords(leg: Pick<RouteLeg, "chosen" | "tier">): string;
|
|
267
345
|
/**
|
|
@@ -270,7 +348,10 @@ export declare function chosenWords(leg: Pick<RouteLeg, "chosen" | "tier">): str
|
|
|
270
348
|
* plans, builds, and repairs; claude · opus reviews" — with the posture
|
|
271
349
|
* said honestly (stronger agents only when one is actually selected).
|
|
272
350
|
*/
|
|
273
|
-
export declare function agentsSummary(route: Pick<PhaseRoute, "legs">
|
|
351
|
+
export declare function agentsSummary(route: Pick<PhaseRoute, "legs"> & {
|
|
352
|
+
size?: TaskSizing | null;
|
|
353
|
+
risk?: RiskLevel;
|
|
354
|
+
}): string;
|
|
274
355
|
export declare function legLine(leg: RouteLegProjection): string;
|
|
275
356
|
/** The one projection CLI, task page, and chat all render from. */
|
|
276
357
|
export declare function projectRoute(route: PhaseRoute, readiness: ReadinessLookup): RouteProjection;
|