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/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;
|
package/dist/phase-routing.js
CHANGED
|
@@ -67,12 +67,16 @@ function exactModel(v) {
|
|
|
67
67
|
export function exactModelId(v) {
|
|
68
68
|
return exactModel(v);
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
/** v2 adds the task's size (small / medium / large, risky or not), the
|
|
71
|
+
* light tier, and plan headroom. A v1 route — sealed before sizing — still
|
|
72
|
+
* reads back exactly and keeps its own digest domain. */
|
|
73
|
+
export const ROUTE_VERSION = 2;
|
|
71
74
|
/** The durable route ERA a scope row carries once it was filed under this
|
|
72
75
|
* policy: NULL on a row proven to predate v47 (legacy — the sealed profile
|
|
73
76
|
* alone governs), this value on every row filed since. A row with an era
|
|
74
|
-
* and no readable route is corrupt and fails closed.
|
|
75
|
-
|
|
77
|
+
* and no readable route is corrupt and fails closed. The era marks a ROUTED
|
|
78
|
+
* row; which route version it holds rides in the snapshot itself. */
|
|
79
|
+
export const ROUTE_ERA = 1;
|
|
76
80
|
export const PHASES = ["plan", "build", "repair", "review"];
|
|
77
81
|
/** The review leg remains readable in signed history but is no longer scheduled. */
|
|
78
82
|
export const ACTIVE_PHASES = ["plan", "build", "repair"];
|
|
@@ -80,6 +84,34 @@ export const RISK_LEVELS = ["routine", "elevated", "high"];
|
|
|
80
84
|
export function isRiskLevel(value) {
|
|
81
85
|
return value === "routine" || value === "elevated" || value === "high";
|
|
82
86
|
}
|
|
87
|
+
export const TASK_SIZES = ["small", "medium", "large"];
|
|
88
|
+
export function isTaskSize(value) {
|
|
89
|
+
return value === "small" || value === "medium" || value === "large";
|
|
90
|
+
}
|
|
91
|
+
/** Whether a task makes no plan: a small change that is not risky, at
|
|
92
|
+
* routine risk. Elevated or high risk always plans, whatever the size. */
|
|
93
|
+
export function makesNoPlan(size, risk) {
|
|
94
|
+
return size != null && size.size === "small" && !size.risky && risk === "routine";
|
|
95
|
+
}
|
|
96
|
+
/** Where a size came from, in words. */
|
|
97
|
+
export function sizeSourceWords(source) {
|
|
98
|
+
return source === "classifier" ? "sized automatically" : source === "person" ? "set by a person" : "sized from the description";
|
|
99
|
+
}
|
|
100
|
+
/** Strict read of a stored sizing: exact keys, known words, bounded reason. */
|
|
101
|
+
export function parseSizing(raw) {
|
|
102
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
103
|
+
return null;
|
|
104
|
+
const v = raw;
|
|
105
|
+
if (!exactKeysOf(v, ["size", "risky", "source", "reason"]))
|
|
106
|
+
return null;
|
|
107
|
+
if (!isTaskSize(v["size"]) || typeof v["risky"] !== "boolean")
|
|
108
|
+
return null;
|
|
109
|
+
if (v["source"] !== "classifier" && v["source"] !== "heuristic" && v["source"] !== "person")
|
|
110
|
+
return null;
|
|
111
|
+
if (typeof v["reason"] !== "string" || v["reason"].length > 300)
|
|
112
|
+
return null;
|
|
113
|
+
return { size: v["size"], risky: v["risky"], source: v["source"], reason: v["reason"] };
|
|
114
|
+
}
|
|
83
115
|
export function riskTitle(risk) {
|
|
84
116
|
return risk === "high" ? "High risk" : risk === "elevated" ? "Elevated risk" : "Routine";
|
|
85
117
|
}
|
|
@@ -93,10 +125,59 @@ export function riskConsequence(risk) {
|
|
|
93
125
|
if (risk === "high")
|
|
94
126
|
return "every active role — planner, builder, and repair — uses the strongest agent you have configured";
|
|
95
127
|
if (risk === "elevated")
|
|
96
|
-
return "planning and building
|
|
128
|
+
return "planning and building use the strongest agent you have configured";
|
|
97
129
|
return "every role uses the everyday configured agent unless the work itself asks for more (strict quality, screenshots, or a self-merging mode)";
|
|
98
130
|
}
|
|
99
131
|
export const RISK_CHOICES = RISK_LEVELS.map(risk => ({ risk, title: riskTitle(risk), consequence: riskConsequence(risk) }));
|
|
132
|
+
const TIER_RANK = { light: 0, routine: 1, strong: 2 };
|
|
133
|
+
/** Above these a provider has no headroom: even a candidate one tier
|
|
134
|
+
* stronger on another provider takes the phase. Below them, a tier's
|
|
135
|
+
* candidates on different providers still go to the one with more room. */
|
|
136
|
+
export const HEADROOM_FIVE_HOUR_LIMIT = 80;
|
|
137
|
+
export const HEADROOM_WEEKLY_LIMIT = 90;
|
|
138
|
+
/** The latest plan-window readings as headroom: a window whose reset time
|
|
139
|
+
* has passed counts as empty; a reading older than its own window is
|
|
140
|
+
* unknown. Weekly is the plan-wide week (`seven_day`), else the fullest
|
|
141
|
+
* model-specific week. */
|
|
142
|
+
export function headroomFrom(limits, now) {
|
|
143
|
+
const out = new Map();
|
|
144
|
+
const value = (one) => {
|
|
145
|
+
if (one.resetsAt !== null && Date.parse(one.resetsAt) <= now.getTime())
|
|
146
|
+
return 0;
|
|
147
|
+
const age = now.getTime() - Date.parse(one.observedAt);
|
|
148
|
+
if (!Number.isFinite(age) || (one.windowMinutes !== null && age > one.windowMinutes * 60_000))
|
|
149
|
+
return null;
|
|
150
|
+
return one.usedPercent;
|
|
151
|
+
};
|
|
152
|
+
for (const one of limits) {
|
|
153
|
+
if (!isProviderId(one.provider))
|
|
154
|
+
continue;
|
|
155
|
+
const room = out.get(one.provider) ?? { provider: one.provider, fiveHour: null, weekly: null };
|
|
156
|
+
const used = value(one);
|
|
157
|
+
if (one.window === "five_hour")
|
|
158
|
+
room.fiveHour = used;
|
|
159
|
+
else if (one.window === "seven_day")
|
|
160
|
+
room.weekly = used;
|
|
161
|
+
else if (/^seven_day_/.test(one.window) && used !== null && room.weekly === null)
|
|
162
|
+
room.weekly = used;
|
|
163
|
+
out.set(one.provider, room);
|
|
164
|
+
}
|
|
165
|
+
// A plan-wide week beats a model's week: re-read seven_day last.
|
|
166
|
+
for (const one of limits)
|
|
167
|
+
if (isProviderId(one.provider) && one.window === "seven_day")
|
|
168
|
+
out.get(one.provider).weekly = value(one);
|
|
169
|
+
return [...out.values()].sort((a, b) => PROVIDER_ID_LIST.indexOf(a.provider) - PROVIDER_ID_LIST.indexOf(b.provider));
|
|
170
|
+
}
|
|
171
|
+
function tightWords(room) {
|
|
172
|
+
if (room.fiveHour !== null && room.fiveHour > HEADROOM_FIVE_HOUR_LIMIT)
|
|
173
|
+
return `${room.provider} has used ${room.fiveHour}% of its 5-hour window`;
|
|
174
|
+
if (room.weekly !== null && room.weekly > HEADROOM_WEEKLY_LIMIT)
|
|
175
|
+
return `${room.provider} has used ${room.weekly}% of its weekly window`;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function usedOf(room) {
|
|
179
|
+
return Math.max(room.fiveHour ?? 0, room.weekly ?? 0);
|
|
180
|
+
}
|
|
100
181
|
const PHASE_NOUN = { plan: "planner", build: "builder", repair: "repair", review: "reviewer" };
|
|
101
182
|
const PHASE_VERB = { plan: "plans", build: "builds", repair: "repairs", review: "reviews" };
|
|
102
183
|
export function specWords(spec) {
|
|
@@ -110,8 +191,18 @@ const DEMANDS = [
|
|
|
110
191
|
},
|
|
111
192
|
{
|
|
112
193
|
when: input => input.risk === "elevated",
|
|
113
|
-
phases: ["review"],
|
|
114
|
-
reason: "risk is elevated —
|
|
194
|
+
phases: ["plan", "build", "repair", "review"],
|
|
195
|
+
reason: "risk is elevated — planning and building use the strongest configured agent",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
when: input => input.size?.size === "large",
|
|
199
|
+
phases: ["plan", "build", "repair"],
|
|
200
|
+
reason: "a large change — planning and building use the strongest configured agent",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
when: input => input.size?.risky === true,
|
|
204
|
+
phases: ["plan", "build", "repair"],
|
|
205
|
+
reason: "a risky change — planning and building use the strongest configured agent",
|
|
115
206
|
},
|
|
116
207
|
{
|
|
117
208
|
when: input => input.qualityMode === "strict",
|
|
@@ -164,9 +255,24 @@ export function recommendRoute(input) {
|
|
|
164
255
|
.sort((a, b) => PHASES.indexOf(a.phase) - PHASES.indexOf(b.phase));
|
|
165
256
|
const overrideFor = (phase) => overrides.find(one => one.phase === phase) ?? null;
|
|
166
257
|
const legs = [];
|
|
258
|
+
// Elevated or high risk lifts both legs through the table, so it never reaches the small branches.
|
|
259
|
+
const small = input.size?.size === "small";
|
|
167
260
|
const pick = (phase) => {
|
|
261
|
+
const chosen = pickTier(phase);
|
|
262
|
+
return phase === "review" ? { ...chosen, moved: false } : withHeadroom(input, phase, chosen);
|
|
263
|
+
};
|
|
264
|
+
const pickTier = (phase) => {
|
|
168
265
|
const demanded = demandedTier(input, phase);
|
|
169
266
|
const candidates = input.candidates[phase];
|
|
267
|
+
if (demanded.tier === "routine" && small && phase === "build") {
|
|
268
|
+
const light = candidates.light ?? null;
|
|
269
|
+
if (light !== null)
|
|
270
|
+
return { spec: light, tier: "light", reasons: ["small change — a fast model builds it and no plan is made", `light builder from ${light.source}`] };
|
|
271
|
+
return { spec: candidates.routine, tier: "routine", reasons: ["small change — no plan is made; no light builder is configured, so the configured builder runs it", `configured builder from ${candidates.routine.source}`] };
|
|
272
|
+
}
|
|
273
|
+
if (demanded.tier === "routine" && small && phase === "plan") {
|
|
274
|
+
return { spec: candidates.routine, tier: "routine", reasons: ["small change — no plan is made", `configured planner from ${candidates.routine.source}`] };
|
|
275
|
+
}
|
|
170
276
|
if (demanded.tier === "strong") {
|
|
171
277
|
if (candidates.strong !== null) {
|
|
172
278
|
return { spec: candidates.strong, tier: "strong", reasons: [...demanded.reasons, `strong ${PHASE_NOUN[phase]} from ${candidates.strong.source}`] };
|
|
@@ -199,11 +305,15 @@ export function recommendRoute(input) {
|
|
|
199
305
|
}
|
|
200
306
|
}
|
|
201
307
|
// build — pin > override > recommendation.
|
|
308
|
+
let buildMoved = false;
|
|
202
309
|
const build = (() => {
|
|
203
310
|
const rec = pick("build");
|
|
311
|
+
buildMoved = rec.moved;
|
|
204
312
|
const pin = input.pins?.build ?? null;
|
|
205
313
|
const override = overrideFor("build");
|
|
206
314
|
const recommended = { provider: rec.spec.provider, model: rec.spec.model, tier: rec.tier };
|
|
315
|
+
if (pin !== null || override !== null)
|
|
316
|
+
buildMoved = false;
|
|
207
317
|
const leg = pin !== null
|
|
208
318
|
? { phase: "build", provider: pin.provider, model: pin.model, tier: rec.tier, chosen: "pinned", recommended, reasons: [`pinned to ${specWords(pin)} by the firing that filed this task — nothing overrides a pin`], problem: null }
|
|
209
319
|
: override !== null
|
|
@@ -225,7 +335,15 @@ export function recommendRoute(input) {
|
|
|
225
335
|
let tier;
|
|
226
336
|
let reasons;
|
|
227
337
|
let problem = null;
|
|
228
|
-
|
|
338
|
+
// A build moved to another provider for headroom takes its repairs
|
|
339
|
+
// with it: repair rows on the provider it left cannot resume its session.
|
|
340
|
+
const usedRow = demanded.tier === "strong" && candidates.strong !== null ? candidates.strong : candidates.routine;
|
|
341
|
+
if (buildMoved && usedRow !== null && usedRow.provider !== build.provider) {
|
|
342
|
+
model = build.model;
|
|
343
|
+
tier = "routine";
|
|
344
|
+
reasons = [`the build moved to ${build.provider} for plan headroom — repairs resume its session with the build model (${build.model})`];
|
|
345
|
+
}
|
|
346
|
+
else if (demanded.tier === "strong" && candidates.strong !== null) {
|
|
229
347
|
if (candidates.strong.provider === build.provider) {
|
|
230
348
|
model = candidates.strong.model;
|
|
231
349
|
tier = "strong";
|
|
@@ -311,6 +429,64 @@ export function recommendRoute(input) {
|
|
|
311
429
|
demands,
|
|
312
430
|
legs,
|
|
313
431
|
overrides,
|
|
432
|
+
size: input.size ?? null,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* PLAN HEADROOM (v2), at filing:
|
|
437
|
+
*
|
|
438
|
+
* - a tier with candidates on more than one provider (`--also`) runs on
|
|
439
|
+
* the provider with more plan room — the lower window use — so work
|
|
440
|
+
* spreads across plans before either fills;
|
|
441
|
+
* - a chosen provider past its limits (80% of the 5-hour window, 90% of
|
|
442
|
+
* the weekly) gives way to the roomiest other provider at the same
|
|
443
|
+
* tier or one tier stronger, never weaker.
|
|
444
|
+
*
|
|
445
|
+
* Readings are the providers' own; a provider with no reading is unknown,
|
|
446
|
+
* never "empty", so nothing moves toward or away from it on a guess. Ties
|
|
447
|
+
* keep the tier's own candidate. The choice and its reason are sealed with
|
|
448
|
+
* the route.
|
|
449
|
+
*/
|
|
450
|
+
function withHeadroom(input, phase, chosen) {
|
|
451
|
+
const rooms = input.headroom ?? [];
|
|
452
|
+
const roomOf = (provider) => {
|
|
453
|
+
const room = rooms.find(one => one.provider === provider) ?? null;
|
|
454
|
+
return room !== null && (room.fiveHour !== null || room.weekly !== null) ? room : null;
|
|
455
|
+
};
|
|
456
|
+
const current = roomOf(chosen.spec.provider);
|
|
457
|
+
if (current === null)
|
|
458
|
+
return { ...chosen, moved: false };
|
|
459
|
+
const tight = tightWords(current);
|
|
460
|
+
const candidates = input.candidates[phase];
|
|
461
|
+
const pool = [];
|
|
462
|
+
for (const tier of ["light", "routine", "strong"]) {
|
|
463
|
+
const own = tier === "light" ? candidates.light ?? null : tier === "routine" ? candidates.routine : candidates.strong;
|
|
464
|
+
for (const spec of [own, ...(candidates.alternates?.[tier] ?? [])]) {
|
|
465
|
+
if (spec === null || spec.provider === chosen.spec.provider || pool.some(one => one.spec.provider === spec.provider && one.tier === tier))
|
|
466
|
+
continue;
|
|
467
|
+
// Not tight: only the same tier competes on room. Tight: one tier stronger may take it too.
|
|
468
|
+
if (tight === null ? tier !== chosen.tier : TIER_RANK[tier] < TIER_RANK[chosen.tier] || TIER_RANK[tier] > TIER_RANK[chosen.tier] + 1)
|
|
469
|
+
continue;
|
|
470
|
+
pool.push({ spec, tier });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (pool.length === 0)
|
|
474
|
+
return { ...chosen, moved: false };
|
|
475
|
+
const open = pool
|
|
476
|
+
.map(one => ({ ...one, room: roomOf(one.spec.provider) }))
|
|
477
|
+
.filter((one) => one.room !== null && tightWords(one.room) === null)
|
|
478
|
+
.filter(one => tight !== null || usedOf(one.room) < usedOf(current))
|
|
479
|
+
.sort((a, b) => usedOf(a.room) - usedOf(b.room) || TIER_RANK[a.tier] - TIER_RANK[b.tier]);
|
|
480
|
+
const best = open[0];
|
|
481
|
+
if (best === undefined) {
|
|
482
|
+
return tight === null ? { ...chosen, moved: false } : { ...chosen, reasons: [...chosen.reasons, `plan headroom: ${tight}, and no other configured ${PHASE_NOUN[phase]} has room — kept`], moved: false };
|
|
483
|
+
}
|
|
484
|
+
const why = tight ?? `${chosen.spec.provider} has used ${usedOf(current)}% of its plan`;
|
|
485
|
+
return {
|
|
486
|
+
spec: best.spec,
|
|
487
|
+
tier: best.tier,
|
|
488
|
+
reasons: [...chosen.reasons.slice(0, -1), `plan headroom: ${why} — ${best.spec.provider} has more room (${usedOf(best.room)}% used), so its ${best.tier} ${PHASE_NOUN[phase]} from ${best.spec.source} runs it`],
|
|
489
|
+
moved: true,
|
|
314
490
|
};
|
|
315
491
|
}
|
|
316
492
|
/** Whether any leg actually runs a configured strong agent. */
|
|
@@ -367,6 +543,8 @@ export function canonicalRouteJson(route) {
|
|
|
367
543
|
problem: leg.problem,
|
|
368
544
|
})),
|
|
369
545
|
overrides: route.overrides.map(one => ({ phase: one.phase, provider: one.provider, model: one.model, by: one.by, at: one.at })),
|
|
546
|
+
// v2 carries the size it was routed for; a v1 snapshot's bytes never change.
|
|
547
|
+
...(route.version === 1 ? {} : { size: route.size === null ? null : { size: route.size.size, risky: route.size.risky, source: route.size.source, reason: route.size.reason } }),
|
|
370
548
|
});
|
|
371
549
|
}
|
|
372
550
|
/** sha256 over a domain-separated canonical encoding, truncated to the
|
|
@@ -374,7 +552,7 @@ export function canonicalRouteJson(route) {
|
|
|
374
552
|
* route digest can never collide with a profile or chain digest. */
|
|
375
553
|
export function routeDigestOf(route) {
|
|
376
554
|
return createHash("sha256")
|
|
377
|
-
.update(`standing-orders:route:v${
|
|
555
|
+
.update(`standing-orders:route:v${route.version}:${canonicalRouteJson(route)}`, "utf8")
|
|
378
556
|
.digest("hex")
|
|
379
557
|
.slice(0, 32);
|
|
380
558
|
}
|
|
@@ -413,9 +591,17 @@ export function routeFromJson(json) {
|
|
|
413
591
|
// Exact keys (v48 integrity): the snapshot carries what canonicalRouteJson
|
|
414
592
|
// writes and nothing else — on the route, on every leg, on every leg's
|
|
415
593
|
// recommendation, and on every override.
|
|
416
|
-
|
|
594
|
+
// A v1 route (sealed before sizing) reads back exactly as it was written:
|
|
595
|
+
// no size, routine and strong tiers only.
|
|
596
|
+
const version = r["version"];
|
|
597
|
+
if (version !== 1 && version !== 2)
|
|
598
|
+
return null;
|
|
599
|
+
const v1Keys = ["version", "risk", "qualityMode", "publication", "evidence", "posture", "demands", "legs", "overrides"];
|
|
600
|
+
if (!exactKeysOf(r, version === 1 ? v1Keys : [...v1Keys, "size"]))
|
|
417
601
|
return null;
|
|
418
|
-
|
|
602
|
+
const tierOk = (tier) => tier === "routine" || tier === "strong" || (version === 2 && tier === "light");
|
|
603
|
+
const size = version === 1 || r["size"] === null ? null : parseSizing(r["size"]);
|
|
604
|
+
if (version === 2 && r["size"] !== null && size === null)
|
|
419
605
|
return null;
|
|
420
606
|
if (!isRiskLevel(r["risk"]))
|
|
421
607
|
return null;
|
|
@@ -445,7 +631,7 @@ export function routeFromJson(json) {
|
|
|
445
631
|
return null;
|
|
446
632
|
if (!exactModel(leg["model"]))
|
|
447
633
|
return null;
|
|
448
|
-
if (leg["tier"]
|
|
634
|
+
if (!tierOk(leg["tier"]))
|
|
449
635
|
return null;
|
|
450
636
|
if (leg["chosen"] !== "recommended" && leg["chosen"] !== "override" && leg["chosen"] !== "pinned")
|
|
451
637
|
return null;
|
|
@@ -459,7 +645,7 @@ export function routeFromJson(json) {
|
|
|
459
645
|
return null;
|
|
460
646
|
if (!exactModel(recommended["model"]))
|
|
461
647
|
return null;
|
|
462
|
-
if (recommended["tier"]
|
|
648
|
+
if (!tierOk(recommended["tier"]))
|
|
463
649
|
return null;
|
|
464
650
|
if (!stringList(leg["reasons"]))
|
|
465
651
|
return null;
|
|
@@ -484,7 +670,7 @@ export function routeFromJson(json) {
|
|
|
484
670
|
if (overrides === null)
|
|
485
671
|
return null;
|
|
486
672
|
return {
|
|
487
|
-
version
|
|
673
|
+
version,
|
|
488
674
|
risk: r["risk"],
|
|
489
675
|
qualityMode: r["qualityMode"],
|
|
490
676
|
publication: r["publication"],
|
|
@@ -493,6 +679,7 @@ export function routeFromJson(json) {
|
|
|
493
679
|
demands: [...r["demands"]],
|
|
494
680
|
legs,
|
|
495
681
|
overrides,
|
|
682
|
+
size,
|
|
496
683
|
};
|
|
497
684
|
}
|
|
498
685
|
/** Strict parse of an override list: one per phase, every field proved,
|
|
@@ -545,7 +732,25 @@ export function canonicalOverridesJson(overrides) {
|
|
|
545
732
|
}
|
|
546
733
|
/** What actually runs: stronger agents only when a leg draws from one. */
|
|
547
734
|
export function postureWords(route) {
|
|
548
|
-
|
|
735
|
+
const active = route.legs.filter(leg => leg.phase !== "review");
|
|
736
|
+
if (drawsStrong(active))
|
|
737
|
+
return "stronger configured agents";
|
|
738
|
+
return active.some(leg => leg.phase === "build" && leg.chosen === "recommended" && leg.tier === "light") ? "a fast configured agent" : "everyday configured agents";
|
|
739
|
+
}
|
|
740
|
+
/** The size line, plainly: what the size does to this route. */
|
|
741
|
+
export function sizeWords(route) {
|
|
742
|
+
if (route.size === null)
|
|
743
|
+
return null;
|
|
744
|
+
const build = route.legs.find(leg => leg.phase === "build");
|
|
745
|
+
// Said from the build leg that actually runs: another demand (strict quality, screenshots) can lift it.
|
|
746
|
+
const model = build?.tier === "light" ? "fast model" : build?.tier === "strong" ? "strongest model" : "everyday model";
|
|
747
|
+
if (makesNoPlan(route.size, route.risk))
|
|
748
|
+
return `Small change: ${model}, no plan`;
|
|
749
|
+
// Elevated or high risk plans a small change too.
|
|
750
|
+
const label = route.size.size === "large" ? "Large change" : route.size.size === "small" ? (route.size.risky ? "Small but risky change" : "Small change") : route.size.risky ? "Risky change" : "Medium change";
|
|
751
|
+
if (route.size.size !== "large" && !route.size.risky && route.risk === "routine")
|
|
752
|
+
return build?.tier === "strong" ? "Medium change: strongest agents" : "Medium change: everyday agents";
|
|
753
|
+
return build?.tier === "strong" ? `${label}: strongest agents plan and build` : `${label}: planned first, everyday agents (no stronger agent is configured)`;
|
|
549
754
|
}
|
|
550
755
|
export function readinessWords(state, reason) {
|
|
551
756
|
if (state === "ready")
|
|
@@ -555,7 +760,7 @@ export function readinessWords(state, reason) {
|
|
|
555
760
|
return `readiness unknown${reason === null ? "" : ` — ${reason}`}`;
|
|
556
761
|
}
|
|
557
762
|
export function chosenWords(leg) {
|
|
558
|
-
return leg.chosen === "override" ? "overridden" : leg.chosen === "pinned" ? "pinned" : leg.tier === "strong" ? "recommended · strong" : "recommended";
|
|
763
|
+
return leg.chosen === "override" ? "overridden" : leg.chosen === "pinned" ? "pinned" : leg.tier === "strong" ? "recommended · strong" : leg.tier === "light" ? "recommended · fast" : "recommended";
|
|
559
764
|
}
|
|
560
765
|
/**
|
|
561
766
|
* The one-line Agents summary every surface can show above the details:
|
|
@@ -565,7 +770,9 @@ export function chosenWords(leg) {
|
|
|
565
770
|
*/
|
|
566
771
|
export function agentsSummary(route) {
|
|
567
772
|
const groups = [];
|
|
568
|
-
|
|
773
|
+
// A small change makes no plan: its planner is not one of the agents that work on it.
|
|
774
|
+
const unplanned = makesNoPlan(route.size, route.risk ?? "routine");
|
|
775
|
+
for (const leg of route.legs.filter(leg => leg.phase !== "review" && !(unplanned && leg.phase === "plan" && leg.chosen === "recommended"))) {
|
|
569
776
|
const spec = specWords(leg);
|
|
570
777
|
const group = groups.find(one => one.spec === spec);
|
|
571
778
|
if (group === undefined)
|
|
@@ -599,7 +806,7 @@ export function projectRoute(route, readiness) {
|
|
|
599
806
|
return {
|
|
600
807
|
digest: routeDigestOf(route),
|
|
601
808
|
risk: route.risk,
|
|
602
|
-
riskTitle: riskTitle(route.risk),
|
|
809
|
+
riskTitle: route.risk === "routine" && route.size?.risky ? "Risky" : riskTitle(route.risk),
|
|
603
810
|
posture: drawsStrong(legs) ? "strong" : "economy",
|
|
604
811
|
postureWords: postureWords(route),
|
|
605
812
|
demands: route.demands.filter(reason => !/review/i.test(reason)),
|
|
@@ -607,6 +814,10 @@ export function projectRoute(route, readiness) {
|
|
|
607
814
|
summary: agentsSummary(route),
|
|
608
815
|
halted: legs.some(leg => leg.readiness === "unavailable"),
|
|
609
816
|
problems: routeProblems(route),
|
|
817
|
+
size: route.size,
|
|
818
|
+
sizeWords: sizeWords(route),
|
|
819
|
+
// A person's size already says who set it.
|
|
820
|
+
sizeReason: route.size === null ? null : route.size.source === "person" && route.size.reason !== "" ? route.size.reason : `${route.size.reason === "" ? "" : `${route.size.reason} · `}${sizeSourceWords(route.size.source)}`,
|
|
610
821
|
};
|
|
611
822
|
}
|
|
612
823
|
/** The approval-card lines: one route header, one line per leg, then the
|
|
@@ -615,6 +826,7 @@ export function routeWords(projection, indent = " ") {
|
|
|
615
826
|
const pad = `${indent} `;
|
|
616
827
|
return [
|
|
617
828
|
`${indent}route ${projection.riskTitle.toLowerCase()} · ${projection.postureWords} · ${projection.digest}`,
|
|
829
|
+
...(projection.sizeWords === null ? [] : [`${indent}size ${projection.sizeWords} — ${projection.sizeReason}`]),
|
|
618
830
|
`${pad}${projection.summary}`,
|
|
619
831
|
...projection.legs.flatMap(leg => [`${pad}${leg.words}`, ...leg.reasons.map(reason => `${pad} ${reason}`)]),
|
|
620
832
|
...(projection.halted ? [`${pad}HALTED: a provider on this route is reported unavailable — nothing substitutes; override the phase or restore the provider`] : []),
|
package/dist/process-liveness.js
CHANGED
|
@@ -43,6 +43,11 @@ function newerMacProcess(pid, witness) {
|
|
|
43
43
|
* unknown stays occupied. This concerns the historical identity, not whether
|
|
44
44
|
* the numeric PID/PGID is currently populated by unrelated processes. */
|
|
45
45
|
export function processMayBeAlive(pid, group, witness) {
|
|
46
|
+
// A recorded single-process exit is final: this number can now belong to
|
|
47
|
+
// someone else, even before the run ends. A leader's exit proves nothing
|
|
48
|
+
// about its group's remaining members, so groups keep the OS probe.
|
|
49
|
+
if (!group && witness?.exitedAt != null)
|
|
50
|
+
return false;
|
|
46
51
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
47
52
|
return true;
|
|
48
53
|
if (group && process.platform !== "win32") {
|
package/dist/project-skills.d.ts
CHANGED
|
@@ -123,6 +123,11 @@ export declare function testSkill(store: Store, args: {
|
|
|
123
123
|
ok: true;
|
|
124
124
|
id: string;
|
|
125
125
|
planning: boolean;
|
|
126
|
+
sizing: import("./phase-routing.js").TaskSizing;
|
|
127
|
+
} | {
|
|
128
|
+
ok: true;
|
|
129
|
+
id: string;
|
|
130
|
+
planning: boolean;
|
|
126
131
|
};
|
|
127
132
|
/** Exact finished test result; report feedback never pretends a code diff exists. */
|
|
128
133
|
export declare function skillTestResult(store: Store, runId: number, actor: string): {
|
|
@@ -143,6 +148,11 @@ export declare function reviseSkillTest(store: Store, args: {
|
|
|
143
148
|
ok: true;
|
|
144
149
|
id: string;
|
|
145
150
|
planning: boolean;
|
|
151
|
+
sizing: import("./phase-routing.js").TaskSizing;
|
|
152
|
+
} | {
|
|
153
|
+
ok: true;
|
|
154
|
+
id: string;
|
|
155
|
+
planning: boolean;
|
|
146
156
|
};
|
|
147
157
|
/** Public GitHub folders only, pinned to a commit before reading any package bytes. */
|
|
148
158
|
export declare function githubSkill(url: string, fetcher?: typeof fetch): Promise<{
|
package/dist/proposal.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { validateTaskText } from "./task-text.js";
|
|
|
3
3
|
import type { Store } from "./store.js";
|
|
4
4
|
import { type UnattendedPermissionMode } from "./scope.js";
|
|
5
5
|
import type { QualityMode } from "./quality.js";
|
|
6
|
+
import type { TaskSizing } from "./phase-routing.js";
|
|
6
7
|
export type ProposalRefusal = {
|
|
7
8
|
ok: false;
|
|
8
9
|
/** Stable, machine-readable — joins the envelope like every reason. */
|
|
@@ -47,11 +48,17 @@ export type TaskProposalInput = {
|
|
|
47
48
|
* which is the fail-closed reading of an empty ceiling (finding 4).
|
|
48
49
|
*/
|
|
49
50
|
admittedRepos?: readonly string[];
|
|
51
|
+
/** v2 routing: the task's size when the caller already knows it (a
|
|
52
|
+
* person's choice). Absent, filing sizes it from the description at
|
|
53
|
+
* once and the process's classifier refines it within five seconds. */
|
|
54
|
+
sizing?: TaskSizing;
|
|
50
55
|
};
|
|
51
56
|
/** One deterministic planning policy for console, chat, MCP, and sync
|
|
52
57
|
* filings. A planner needs a repository to inspect; report/scout work is
|
|
53
|
-
* already the discovery phase and never recursively plans itself.
|
|
54
|
-
|
|
58
|
+
* already the discovery phase and never recursively plans itself. With a
|
|
59
|
+
* size (v2 routing), a small change is not planned and a large or risky
|
|
60
|
+
* one is; a medium one keeps the description's signals. */
|
|
61
|
+
export declare function shouldPlanTask(input: TaskProposalInput, sizing?: TaskSizing): boolean;
|
|
55
62
|
export type RoutineProposalInput = {
|
|
56
63
|
name: string;
|
|
57
64
|
/** v102: who is making it (its instances are filed as theirs). */
|
|
@@ -74,6 +81,7 @@ export declare function fileTaskProposal(store: Store, input: TaskProposalInput,
|
|
|
74
81
|
ok: true;
|
|
75
82
|
id: string;
|
|
76
83
|
planning: boolean;
|
|
84
|
+
sizing: TaskSizing;
|
|
77
85
|
} | ProposalRefusal;
|
|
78
86
|
export declare function fileRoutineProposal(store: Store, input: RoutineProposalInput, now: Date): {
|
|
79
87
|
ok: true;
|