@sayknow-cli/coding-agent 0.5.25 → 0.5.26

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 CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.5.25] - 2026-09-21
5
+ ## [0.5.26] - 2026-09-23
6
+ ### Added
7
+
8
+ - `/model` role rows now open a second level so a model can be assigned to a *detailed use* rather than only to a whole role. `planner`/`architect` offer backend architecture and frontend design, `executor` offers implementation and testing, `critic` offers review; `default` has none and still assigns in one keystroke. The two bulk rows deliberately stay canonical-only — an action that also rewrote five detailed-use overrides could not be undone from the same menu. Assignments persist to the new `task.modelRouting.specialtyModels` record, whose keys are allowlisted to those five ids. Saving one while `task.modelRouting.enabled` is off persists it and says so instead of silently enabling a feature you did not ask for. First-level rows are now stable descriptors rather than positional index arithmetic, which is what previously made every added row a chance to assign a model to the wrong role.
9
+ - Subagent model routing now runs **one classification per child task** instead of one per `task` call. A batch shares an agent but not a workload, so an implementation slice and a test slice in the same call no longer have to share a single verdict. Each child dispatches its own ordered chain — detailed-use model, then tier model, then the role's configured chain — so a detailed-use model that cannot authenticate still falls through to something the role can actually run. A high-risk assignment skips the detailed-use axis entirely, because a lateral swap can move sideways into something weaker.
10
+ - Receipts and progress now carry what the router **requested** separately from what the spawn ran on. Because the dispatched value is a fallback chain, conflating the two would let a receipt claim a detailed-use model was used when the chain actually fell through to the baseline. When the decision backend is an ordinary LLM it returns no probabilities at all, so the record carries an ordinal clarity score and `calibrated: false` rather than a fabricated confidence.
11
+ - The `task` tool accepts `.specialty` per task. Declaring the kind of work is the deterministic path to a detailed-use model: no classifier runs, `task.modelRouting.enabled` is not consulted, and no confidence bar applies — if the user assigned a model to that specialty under `/model`, the child runs on it and leaves it only on a transport error (429, 5xx, auth, quota), when the child session's fallback chain advances to the role baseline composed behind it. A declared specialty ignores the menu's role grouping on purpose: the setting is one flat map, and a frontend build delegated to `executor` should get the frontend model. Receipts mark these `declared: true` so they cannot be mistaken for a classifier verdict. Auto-detection from assignment text is unchanged and still opt-in.
12
+
13
+ ### Fixed
14
+
15
+ - Model-selector suites that assert UI copy now pin the locale — and restore it. They previously inherited whatever `language` the developer had configured, because a sibling suite restores the real agent directory and reloads real settings, and one suite pinned Korean without putting it back — so whether they passed depended on test file ordering.
16
+ - The first-level `/model` role rows (`Set as EXECUTOR (Executor)`) were hardcoded English inside an otherwise localized menu. They now go through i18n in all seven locales; the redundant `(Name)` suffix is dropped where the locale does not need it, and the role tag stays verbatim because it is the same identifier receipts and `/model` arguments use.
17
+ - Login, `--list-models`, `/model`, and `/model provider/id` now refresh provider catalogs online so newly published Kimi/Grok (and other OpenAI-compatible) ids appear without waiting for a bundled `models.json` regen or a 2h cache TTL.
6
18
 
7
19
  ## [0.5.22] - 2026-09-19
8
20
 
@@ -104,6 +104,11 @@ interface RecordDef<T> {
104
104
  type: "record";
105
105
  default: Record<string, T>;
106
106
  valueSchema?: RecordValueDef;
107
+ /**
108
+ * Closed key set. When present, reconciliation rejects any other key instead of
109
+ * silently carrying a typo that no consumer will ever read.
110
+ */
111
+ keys?: readonly string[];
107
112
  ui?: UiBase;
108
113
  }
109
114
  type SettingDef = BooleanDef | StringDef | NumberDef | EnumDef<readonly string[]> | ArrayDef<unknown> | RecordDef<unknown>;
@@ -2471,6 +2476,27 @@ export declare const SETTINGS_SCHEMA: {
2471
2476
  readonly type: "string";
2472
2477
  readonly default: "";
2473
2478
  };
2479
+ /**
2480
+ * Per-specialty models — the **work-kind** axis.
2481
+ *
2482
+ * Keys are the bounded ids in `config/task-model-specialties.ts`; values use the
2483
+ * same selector grammar as any other model setting, so a chain is allowed. A
2484
+ * specialty with no entry inherits the role's resolved model unchanged, which is
2485
+ * why the default is empty rather than pre-populated.
2486
+ *
2487
+ * These are routing hints, never agents: nothing here widens the canonical role
2488
+ * roster, model-profile role keys, tool grants, or spawn permissions. Saving one
2489
+ * does not enable `task.modelRouting.enabled`; the assignment surface reports the
2490
+ * disabled state instead of silently turning routing on.
2491
+ */
2492
+ readonly "task.modelRouting.specialtyModels": {
2493
+ readonly type: "record";
2494
+ readonly default: Record<string, ModelSelectorValue>;
2495
+ readonly valueSchema: {
2496
+ readonly type: "model-selector-value";
2497
+ };
2498
+ readonly keys: readonly ["backendArchitecture", "frontendDesign", "implementation", "testing", "review"];
2499
+ };
2474
2500
  readonly "ttsr.enabled": {
2475
2501
  readonly type: "boolean";
2476
2502
  readonly default: true;
@@ -0,0 +1,55 @@
1
+ /** Stable configuration/routing identifiers. Display strings are localized separately. */
2
+ export declare const TASK_MODEL_SPECIALTY_IDS: readonly ["backendArchitecture", "frontendDesign", "implementation", "testing", "review"];
3
+ export type TaskModelSpecialty = (typeof TASK_MODEL_SPECIALTY_IDS)[number];
4
+ /**
5
+ * Which canonical role agents may receive each specialty.
6
+ *
7
+ * Planning roles share the two design specialties on purpose: the user's intent is
8
+ * "a backend-strong model plans backend work", not "planner and architect each get
9
+ * their own private backend model". Implementation roles never take a design
10
+ * specialty, mirroring the existing frontend-domain rule.
11
+ */
12
+ export declare const TASK_MODEL_SPECIALTY_ROLES: Readonly<Record<TaskModelSpecialty, readonly string[]>>;
13
+ /** Neutral classifier outcome: the work does not clearly belong to one specialty. */
14
+ export declare const TASK_MODEL_SPECIALTY_NONE: "none";
15
+ /** Bounded-key predicate for the `task.modelRouting.specialtyModels` record. */
16
+ export declare function isTaskModelSpecialty(value: unknown): value is TaskModelSpecialty;
17
+ /** Specialties a given canonical role agent is allowed to receive, in declaration order. */
18
+ export declare function specialtiesForRole(agentName: string): TaskModelSpecialty[];
19
+ /** Whether a specialty may be applied to work routed to this canonical role agent. */
20
+ export declare function specialtySupportsRole(specialty: TaskModelSpecialty, agentName: string): boolean;
21
+ /** Where a composed candidate came from, so a receipt never overstates what was selected. */
22
+ export type TaskRoutingSource = "specialty" | "legacy-frontend" | "tier" | "baseline";
23
+ export interface TaskRoutingCandidate {
24
+ /** The configured selector, normalized but otherwise untouched. */
25
+ selector: string;
26
+ source: TaskRoutingSource;
27
+ /** Set when `source` is `specialty` or `legacy-frontend`. */
28
+ specialty?: TaskModelSpecialty;
29
+ /** Set when `source` is `tier`. */
30
+ tier?: string;
31
+ }
32
+ /**
33
+ * Identity used for deduplication.
34
+ *
35
+ * An explicit thinking suffix is part of the identity: `provider/model:high` and
36
+ * `provider/model:low` are different configured intents, and collapsing them would
37
+ * silently drop the user's effort choice from a chain.
38
+ */
39
+ export declare function specialtySelectorIdentity(selector: string): string;
40
+ /** The `provider/model` part, ignoring any thinking suffix. Used for "same model" checks. */
41
+ export declare function specialtySelectorHead(selector: string): string;
42
+ /** Expand a configured selector value into normalized candidates tagged with their origin. */
43
+ export declare function toRoutingCandidates(value: string | readonly string[] | undefined, source: TaskRoutingSource, extra?: {
44
+ specialty?: TaskModelSpecialty;
45
+ tier?: string;
46
+ }): TaskRoutingCandidate[];
47
+ /**
48
+ * Concatenate candidate segments, keeping the first occurrence of each identity.
49
+ *
50
+ * A selector that also exists in the baseline segment is attributed to `baseline`
51
+ * even when an earlier specialty segment introduced it. Resolving that entry proves
52
+ * only that the role's own configured model was usable — reporting it as a specialty
53
+ * hit would claim a routing decision that never happened.
54
+ */
55
+ export declare function dedupeRoutingCandidates(segments: readonly TaskRoutingCandidate[][]): TaskRoutingCandidate[];
@@ -1,3 +1,6 @@
1
+ import type { ModelSelectorValue } from "../config/model-selector-value";
2
+ import type { Settings } from "../config/settings";
3
+ import { type TaskModelSpecialty, type TaskRoutingCandidate, type TaskRoutingSource } from "../config/task-model-specialties";
1
4
  import type { DecisionService } from "./index";
2
5
  /** Ordered cheapest to most capable. The order *is* the policy's direction. */
3
6
  export declare const TASK_TIERS: readonly ["fast", "balanced", "deep"];
@@ -22,26 +25,57 @@ export interface TaskRoutingPolicy {
22
25
  /**
23
26
  * Model for frontend planning, when the assignment reads as frontend work.
24
27
  *
25
- * This is the **domain** axis, not a rung on the ladder: a design-strong model
26
- * is not "better" than a code-strong one, it is a different specialty. Only
27
- * planning roles ever take it, and only laterally — the implementation roles
28
- * stay on the difficulty ladder.
28
+ * Superseded by `specialtyModels.frontendDesign`; kept as the fallback source
29
+ * so an existing configuration keeps working untouched until it is migrated.
29
30
  */
30
31
  frontendModel?: string;
31
32
  /**
32
- * Bar for the lateral swap above. Directional bars do not apply here because
33
- * neither direction is "spending more": being wrong either way costs quality,
34
- * symmetrically, so one bar is the whole story.
33
+ * Per-specialty models — the work-kind axis.
34
+ *
35
+ * Absent entries inherit the role's own chain, which is why an unset specialty
36
+ * is not an error and does not suppress the difficulty ladder.
37
+ */
38
+ specialtyModels?: Partial<Record<TaskModelSpecialty, ModelSelectorValue>>;
39
+ /**
40
+ * Bar for a lateral swap on a **calibrated** backend. Directional bars do not
41
+ * apply here because neither direction is "spending more": being wrong either
42
+ * way costs quality, symmetrically, so one bar is the whole story.
35
43
  */
36
44
  minDomainConfidence: number;
45
+ /**
46
+ * Bar for a lateral swap on an **uncalibrated** backend.
47
+ *
48
+ * The ordinary logged-in model cannot report a probability, and asking it for
49
+ * one measurably degrades the answer, so its choice carries no `confidence`.
50
+ * What it *can* report is an ordinal strength. Requiring a high ordinal is not
51
+ * the same guarantee as a calibrated threshold, and the result is recorded as
52
+ * uncalibrated — but refusing to route at all would make a user's explicit
53
+ * specialty selection silently inert on the default backend.
54
+ */
55
+ minSpecialtyOrdinal: number;
37
56
  }
38
57
  export declare const DEFAULT_TASK_ROUTING_POLICY: Omit<TaskRoutingPolicy, "tiers">;
58
+ /**
59
+ * The settings surface this module reads.
60
+ *
61
+ * Narrowed to `get` so the router cannot quietly start writing settings, and so
62
+ * a caller only has to supply a reader rather than a whole `Settings` instance.
63
+ */
64
+ export type TaskRoutingSettingsReader = Pick<Settings, "get">;
65
+ /**
66
+ * Build the routing policy from settings, or null when routing must not run.
67
+ *
68
+ * Null is returned for two distinct reasons that both mean "leave the configured
69
+ * model alone": the feature is off, or it is on but nothing is configured to
70
+ * route *to*. A lone tier is not an axis — there is nowhere to move from it —
71
+ * so two tiers is the floor unless a specialty or the legacy frontend model
72
+ * supplies a lateral target instead.
73
+ */
74
+ export declare function buildTaskRoutingPolicyFromSettings(settings: TaskRoutingSettingsReader): TaskRoutingPolicy | null;
39
75
  /**
40
76
  * Roles whose output is a plan or a design review.
41
77
  *
42
- * These are the only roles the domain swap applies to — the user's intent is
43
- * "a design-strong model *plans* the frontend; implementation stays where it
44
- * is". Executor keeps the difficulty ladder regardless of domain.
78
+ * Derived from the specialty compatibility map so the two never drift apart.
45
79
  */
46
80
  export declare const PLANNING_ROLES: ReadonlySet<string>;
47
81
  export interface TaskRoutingRequest {
@@ -50,14 +84,65 @@ export interface TaskRoutingRequest {
50
84
  assignment: string;
51
85
  /** Whatever the role is configured to use today, used as the direction baseline. */
52
86
  currentModel: string | undefined;
87
+ /**
88
+ * The role's fully resolved chain, in order. The composed candidate list ends
89
+ * with this, so a specialty or tier that cannot be authenticated falls through
90
+ * to the model the role would have used anyway.
91
+ */
92
+ baselineChain?: readonly string[];
53
93
  signal?: AbortSignal;
54
94
  }
55
95
  export interface TaskRoutingResult {
96
+ /** Head of the composed chain — what the spawn runs on if it authenticates. */
56
97
  model: string;
57
- /** Null when the move was a domain swap — that axis has no ladder. */
98
+ /** Null when the move was a specialty swap — that axis has no ladder. */
58
99
  tier: TaskTier | null;
59
100
  reason: string;
101
+ /** Ordered, provenance-tagged chain for the existing auth-aware resolver. */
102
+ candidates: TaskRoutingCandidate[];
103
+ /** What the classifier asked for. The *effective* source is only known after resolution. */
104
+ requestedSource: TaskRoutingSource;
105
+ requestedSpecialty?: TaskModelSpecialty;
106
+ requestedTier?: TaskTier;
107
+ /**
108
+ * True when the caller named the specialty on the spawn itself. No classifier
109
+ * ran, so `calibrated`/`confidence`/`ordinalStrength` describe nothing here.
110
+ */
111
+ declared: boolean;
112
+ /** False means `ordinalStrength` ranks, and no probability was available. */
113
+ calibrated: boolean;
114
+ confidence?: number;
115
+ ordinalStrength?: number;
116
+ }
117
+ export interface DeclaredSpecialtyRequest {
118
+ agentName: string;
119
+ specialty: TaskModelSpecialty;
120
+ /** Whatever the role is configured to use today. */
121
+ currentModel: string | undefined;
122
+ /** The role's fully resolved chain; always the tail so a dead specialty model falls through. */
123
+ baselineChain?: readonly string[];
60
124
  }
125
+ /**
126
+ * Route a spawn whose caller *declared* the kind of work.
127
+ *
128
+ * This is the deterministic half of the specialty axis. Nothing here asks a
129
+ * classifier, reads `task.modelRouting.enabled`, or applies a confidence bar:
130
+ * the user put a model on this specialty in `/model`, the caller says this is
131
+ * that work, and the only remaining reason not to run on it is that it fails —
132
+ * which the child session's fallback chain handles at the transport boundary
133
+ * (429, 5xx, auth, quota) by advancing to the role's baseline behind it.
134
+ *
135
+ * Role eligibility is deliberately not checked. The menu groups specialties
136
+ * under the roles that usually do that work, but the setting is one flat map:
137
+ * a frontend model the user chose for design is the same frontend model they
138
+ * expect when the *implementation* of that frontend is delegated. Refusing
139
+ * here would make "frontend uses a different model" false for exactly the
140
+ * spawns where it matters most.
141
+ *
142
+ * Returns null only when nothing is configured for the specialty, or when the
143
+ * configured model is already what the role would run on anyway.
144
+ */
145
+ export declare function resolveDeclaredSpecialtyRouting(settings: TaskRoutingSettingsReader, request: DeclaredSpecialtyRequest): TaskRoutingResult | null;
61
146
  /**
62
147
  * Decide the model for one subagent spawn, or null to leave the configured one alone.
63
148
  *
@@ -76,6 +76,21 @@ export declare const en: {
76
76
  readonly "modelSelector.noMatching": "No matching models.";
77
77
  readonly "modelSelector.modelName": "Model Name: {value}";
78
78
  readonly "modelSelector.actionFor": "Action for: {id}";
79
+ readonly "modelSelector.setAsTarget": "Set as {tag} ({name})";
80
+ readonly "modelSelector.setForAllRoleAgents": "Set for all role agents";
81
+ readonly "modelSelector.setForAllTargets": "Set for all targets";
82
+ readonly "modelSelector.hasDetailedUses": "(detailed uses)";
83
+ readonly "modelSelector.detailedUseFor": "Detailed use for {target}: {id}";
84
+ readonly "modelSelector.generalRole": "General (whole role)";
85
+ readonly "modelSelector.resetSpecialties": "Clear detailed-use overrides";
86
+ readonly "modelSelector.specialty.backendArchitecture": "Backend architecture";
87
+ readonly "modelSelector.specialty.frontendDesign": "Frontend design";
88
+ readonly "modelSelector.specialty.implementation": "Implementation";
89
+ readonly "modelSelector.specialty.testing": "Testing";
90
+ readonly "modelSelector.specialty.review": "Review";
91
+ readonly "modelSelector.specialtySaved": "{specialty} will use {value}.";
92
+ readonly "modelSelector.specialtySavedRoutingOff": "{specialty} will use {value} whenever a task declares that work. Auto-detection from assignment text stays off until you enable task.modelRouting.enabled.";
93
+ readonly "modelSelector.specialtyCleared": "Cleared detailed-use overrides for {target}.";
79
94
  readonly "modelSelector.reasoningFor": "Reasoning for {target}: {id}";
80
95
  readonly "modelSelector.temporaryModel": "temporary model";
81
96
  };
@@ -95,6 +95,7 @@ export declare class LspTool implements AgentTool<typeof lspSchema, LspToolDetai
95
95
  readonly description: string;
96
96
  readonly parameters: import("zod").ZodObject<{
97
97
  action: import("zod").ZodEnum<{
98
+ implementation: "implementation";
98
99
  status: "status";
99
100
  diagnostics: "diagnostics";
100
101
  definition: "definition";
@@ -105,7 +106,6 @@ export declare class LspTool implements AgentTool<typeof lspSchema, LspToolDetai
105
106
  rename_file: "rename_file";
106
107
  code_actions: "code_actions";
107
108
  type_definition: "type_definition";
108
- implementation: "implementation";
109
109
  reload: "reload";
110
110
  capabilities: "capabilities";
111
111
  request: "request";
@@ -3,6 +3,7 @@ import * as z from "zod/v4";
3
3
  import type { OwnedProcess } from "../runtime/process-lifecycle";
4
4
  export declare const lspSchema: z.ZodObject<{
5
5
  action: z.ZodEnum<{
6
+ implementation: "implementation";
6
7
  status: "status";
7
8
  diagnostics: "diagnostics";
8
9
  definition: "definition";
@@ -13,7 +14,6 @@ export declare const lspSchema: z.ZodObject<{
13
14
  rename_file: "rename_file";
14
15
  code_actions: "code_actions";
15
16
  type_definition: "type_definition";
16
- implementation: "implementation";
17
17
  reload: "reload";
18
18
  capabilities: "capabilities";
19
19
  request: "request";
@@ -5,6 +5,7 @@ import type { ModelRegistry, SkcModelAssignmentTargetId } from "../../config/mod
5
5
  import { type ScopedModelSelection } from "../../config/model-resolver";
6
6
  import type { ModelProfileConfig } from "../../config/models-config-schema";
7
7
  import type { Settings } from "../../config/settings";
8
+ import { type TaskModelSpecialty } from "../../config/task-model-specialties";
8
9
  type ScopedModelItem = ScopedModelSelection;
9
10
  export type ModelSelectorSelection = {
10
11
  kind: "assignment";
@@ -17,6 +18,16 @@ export type ModelSelectorSelection = {
17
18
  kind: "profile";
18
19
  profileName: string;
19
20
  setDefault: boolean;
21
+ } | {
22
+ kind: "specialtyAssignment";
23
+ model: Model;
24
+ role: SkcModelAssignmentTargetId;
25
+ specialty: TaskModelSpecialty;
26
+ thinkingLevel?: ThinkingLevel;
27
+ selector?: string;
28
+ } | {
29
+ kind: "specialtyReset";
30
+ role: SkcModelAssignmentTargetId;
20
31
  } | {
21
32
  kind: "createProfile";
22
33
  profile: ModelProfileConfig;
@@ -12,7 +12,7 @@ export { isValidAllocatedTaskId, isValidTaskId, TASK_ID_DESCRIPTION, TASK_ID_PAT
12
12
  export { AgentOutputManager } from "./output-manager";
13
13
  export type { TaskResultReceipt } from "./receipt";
14
14
  export { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoi, buildTaskRoiSummary, findRawTaskLeakKeys, sanitizeTaskToolDetails, } from "./receipt";
15
- export type { AgentDefinition, AgentProgress, SingleResult, SubagentLifecyclePayload, SubagentProgressPayload, TaskParams, TaskToolDetails, } from "./types";
15
+ export type { AgentDefinition, AgentProgress, SingleResult, SubagentLifecyclePayload, SubagentProgressPayload, TaskParams, TaskRoutingAttribution, TaskToolDetails, } from "./types";
16
16
  export { TASK_SUBAGENT_EVENT_CHANNEL, TASK_SUBAGENT_LIFECYCLE_CHANNEL, TASK_SUBAGENT_PROGRESS_CHANNEL, taskSchema, } from "./types";
17
17
  export declare function resolveForkContextMaxTokens(configured: number, model: Model | undefined): number;
18
18
  /**
@@ -28,6 +28,8 @@ export interface TaskResultReceipt {
28
28
  contextTokens?: number;
29
29
  contextWindow?: number;
30
30
  modelOverride?: string | string[];
31
+ /** What the router asked for, kept separate from what the spawn ran on. */
32
+ routing?: SingleResult["routing"];
31
33
  modelSubstitutionWarning?: SingleResult["modelSubstitutionWarning"];
32
34
  usage?: SingleResult["usage"];
33
35
  cost?: number;
@@ -1,6 +1,8 @@
1
1
  import type { ThinkingLevel } from "@sayknow-cli/agent-core";
2
2
  import type { Usage } from "@sayknow-cli/ai";
3
3
  import * as z from "zod/v4";
4
+ import { type TaskModelSpecialty, type TaskRoutingSource } from "../config/task-model-specialties";
5
+ import type { TaskTier } from "../decisions/task-routing";
4
6
  import type { TaskResultReceipt } from "./receipt";
5
7
  import type { SpawnRoiReconciliation } from "./roi-reconciliation";
6
8
  import { type TaskSimpleMode } from "./simple-mode";
@@ -49,12 +51,19 @@ export declare const taskItemSchema: z.ZodObject<{
49
51
  default: "default";
50
52
  "ultragoal-red-team": "ultragoal-red-team";
51
53
  }>>;
54
+ specialty: z.ZodOptional<z.ZodEnum<{
55
+ backendArchitecture: "backendArchitecture";
56
+ frontendDesign: "frontendDesign";
57
+ implementation: "implementation";
58
+ testing: "testing";
59
+ review: "review";
60
+ }>>;
52
61
  inheritContext: z.ZodOptional<z.ZodEnum<{
53
62
  none: "none";
54
- receipt: "receipt";
63
+ full: "full";
55
64
  "last-turn": "last-turn";
65
+ receipt: "receipt";
56
66
  bounded: "bounded";
57
- full: "full";
58
67
  }>>;
59
68
  repositoryBinding: z.ZodOptional<z.ZodObject<{
60
69
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -77,12 +86,19 @@ export declare const taskSchema: z.ZodObject<{
77
86
  default: "default";
78
87
  "ultragoal-red-team": "ultragoal-red-team";
79
88
  }>>;
89
+ specialty: z.ZodOptional<z.ZodEnum<{
90
+ backendArchitecture: "backendArchitecture";
91
+ frontendDesign: "frontendDesign";
92
+ implementation: "implementation";
93
+ testing: "testing";
94
+ review: "review";
95
+ }>>;
80
96
  inheritContext: z.ZodOptional<z.ZodEnum<{
81
97
  none: "none";
82
- receipt: "receipt";
98
+ full: "full";
83
99
  "last-turn": "last-turn";
100
+ receipt: "receipt";
84
101
  bounded: "bounded";
85
- full: "full";
86
102
  }>>;
87
103
  repositoryBinding: z.ZodOptional<z.ZodObject<{
88
104
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -112,12 +128,19 @@ export declare const taskSchemaNoIsolation: z.ZodObject<{
112
128
  default: "default";
113
129
  "ultragoal-red-team": "ultragoal-red-team";
114
130
  }>>;
131
+ specialty: z.ZodOptional<z.ZodEnum<{
132
+ backendArchitecture: "backendArchitecture";
133
+ frontendDesign: "frontendDesign";
134
+ implementation: "implementation";
135
+ testing: "testing";
136
+ review: "review";
137
+ }>>;
115
138
  inheritContext: z.ZodOptional<z.ZodEnum<{
116
139
  none: "none";
117
- receipt: "receipt";
140
+ full: "full";
118
141
  "last-turn": "last-turn";
142
+ receipt: "receipt";
119
143
  bounded: "bounded";
120
- full: "full";
121
144
  }>>;
122
145
  repositoryBinding: z.ZodOptional<z.ZodObject<{
123
146
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -147,12 +170,19 @@ declare const ALL_TASK_SCHEMAS: readonly [z.ZodObject<{
147
170
  default: "default";
148
171
  "ultragoal-red-team": "ultragoal-red-team";
149
172
  }>>;
173
+ specialty: z.ZodOptional<z.ZodEnum<{
174
+ backendArchitecture: "backendArchitecture";
175
+ frontendDesign: "frontendDesign";
176
+ implementation: "implementation";
177
+ testing: "testing";
178
+ review: "review";
179
+ }>>;
150
180
  inheritContext: z.ZodOptional<z.ZodEnum<{
151
181
  none: "none";
152
- receipt: "receipt";
182
+ full: "full";
153
183
  "last-turn": "last-turn";
184
+ receipt: "receipt";
154
185
  bounded: "bounded";
155
- full: "full";
156
186
  }>>;
157
187
  repositoryBinding: z.ZodOptional<z.ZodObject<{
158
188
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -181,12 +211,19 @@ declare const ALL_TASK_SCHEMAS: readonly [z.ZodObject<{
181
211
  default: "default";
182
212
  "ultragoal-red-team": "ultragoal-red-team";
183
213
  }>>;
214
+ specialty: z.ZodOptional<z.ZodEnum<{
215
+ backendArchitecture: "backendArchitecture";
216
+ frontendDesign: "frontendDesign";
217
+ implementation: "implementation";
218
+ testing: "testing";
219
+ review: "review";
220
+ }>>;
184
221
  inheritContext: z.ZodOptional<z.ZodEnum<{
185
222
  none: "none";
186
- receipt: "receipt";
223
+ full: "full";
187
224
  "last-turn": "last-turn";
225
+ receipt: "receipt";
188
226
  bounded: "bounded";
189
- full: "full";
190
227
  }>>;
191
228
  repositoryBinding: z.ZodOptional<z.ZodObject<{
192
229
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -215,12 +252,19 @@ declare const ALL_TASK_SCHEMAS: readonly [z.ZodObject<{
215
252
  default: "default";
216
253
  "ultragoal-red-team": "ultragoal-red-team";
217
254
  }>>;
255
+ specialty: z.ZodOptional<z.ZodEnum<{
256
+ backendArchitecture: "backendArchitecture";
257
+ frontendDesign: "frontendDesign";
258
+ implementation: "implementation";
259
+ testing: "testing";
260
+ review: "review";
261
+ }>>;
218
262
  inheritContext: z.ZodOptional<z.ZodEnum<{
219
263
  none: "none";
220
- receipt: "receipt";
264
+ full: "full";
221
265
  "last-turn": "last-turn";
266
+ receipt: "receipt";
222
267
  bounded: "bounded";
223
- full: "full";
224
268
  }>>;
225
269
  repositoryBinding: z.ZodOptional<z.ZodObject<{
226
270
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -249,12 +293,19 @@ declare const ALL_TASK_SCHEMAS: readonly [z.ZodObject<{
249
293
  default: "default";
250
294
  "ultragoal-red-team": "ultragoal-red-team";
251
295
  }>>;
296
+ specialty: z.ZodOptional<z.ZodEnum<{
297
+ backendArchitecture: "backendArchitecture";
298
+ frontendDesign: "frontendDesign";
299
+ implementation: "implementation";
300
+ testing: "testing";
301
+ review: "review";
302
+ }>>;
252
303
  inheritContext: z.ZodOptional<z.ZodEnum<{
253
304
  none: "none";
254
- receipt: "receipt";
305
+ full: "full";
255
306
  "last-turn": "last-turn";
307
+ receipt: "receipt";
256
308
  bounded: "bounded";
257
- full: "full";
258
309
  }>>;
259
310
  repositoryBinding: z.ZodOptional<z.ZodObject<{
260
311
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -283,12 +334,19 @@ declare const ALL_TASK_SCHEMAS: readonly [z.ZodObject<{
283
334
  default: "default";
284
335
  "ultragoal-red-team": "ultragoal-red-team";
285
336
  }>>;
337
+ specialty: z.ZodOptional<z.ZodEnum<{
338
+ backendArchitecture: "backendArchitecture";
339
+ frontendDesign: "frontendDesign";
340
+ implementation: "implementation";
341
+ testing: "testing";
342
+ review: "review";
343
+ }>>;
286
344
  inheritContext: z.ZodOptional<z.ZodEnum<{
287
345
  none: "none";
288
- receipt: "receipt";
346
+ full: "full";
289
347
  "last-turn": "last-turn";
348
+ receipt: "receipt";
290
349
  bounded: "bounded";
291
- full: "full";
292
350
  }>>;
293
351
  repositoryBinding: z.ZodOptional<z.ZodObject<{
294
352
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -317,12 +375,19 @@ declare const ALL_TASK_SCHEMAS: readonly [z.ZodObject<{
317
375
  default: "default";
318
376
  "ultragoal-red-team": "ultragoal-red-team";
319
377
  }>>;
378
+ specialty: z.ZodOptional<z.ZodEnum<{
379
+ backendArchitecture: "backendArchitecture";
380
+ frontendDesign: "frontendDesign";
381
+ implementation: "implementation";
382
+ testing: "testing";
383
+ review: "review";
384
+ }>>;
320
385
  inheritContext: z.ZodOptional<z.ZodEnum<{
321
386
  none: "none";
322
- receipt: "receipt";
387
+ full: "full";
323
388
  "last-turn": "last-turn";
389
+ receipt: "receipt";
324
390
  bounded: "bounded";
325
- full: "full";
326
391
  }>>;
327
392
  repositoryBinding: z.ZodOptional<z.ZodObject<{
328
393
  schema: z.ZodLiteral<"skc.repository_binding.v1">;
@@ -402,6 +467,33 @@ export interface ModelSubstitutionWarning {
402
467
  effective: string;
403
468
  reason: "auth_unavailable" | "assistant_model_mismatch";
404
469
  }
470
+ /**
471
+ * What the model router *asked for* on one child — deliberately not what it ran on.
472
+ *
473
+ * The dispatched value is a fallback chain, so the head can lose to a later
474
+ * candidate when it fails to authenticate. Recording the request separately is
475
+ * what keeps a receipt from claiming a specialty model was used when the spawn
476
+ * actually fell through to the role's baseline. `ModelSubstitutionWarning`
477
+ * covers the disagreement; this covers the intent.
478
+ */
479
+ export interface TaskRoutingAttribution {
480
+ /** Axis the head came from. `baseline` means the router declined to move. */
481
+ source: TaskRoutingSource;
482
+ /** Bounded specialty id, present only when the specialty axis won. */
483
+ specialty?: TaskModelSpecialty;
484
+ /** Tier the classifier settled on. Null for a specialty swap, which has no ladder. */
485
+ tier?: TaskTier;
486
+ /** True when the caller declared the specialty on the spawn; no classifier ran. */
487
+ declared: boolean;
488
+ /** False for ordinary LLM backends, which return no probabilities at all. */
489
+ calibrated: boolean;
490
+ /** Probability. Present only when `calibrated` is true — never synthesised. */
491
+ confidence?: number;
492
+ /** Ordinal clarity in [0,1], recorded in place of a probability when uncalibrated. */
493
+ ordinalStrength?: number;
494
+ /** Router's own explanation, surfaced verbatim on the receipt. */
495
+ reason: string;
496
+ }
405
497
  /** Progress tracking for a single agent */
406
498
  export interface AgentProgress {
407
499
  index: number;
@@ -439,6 +531,8 @@ export interface AgentProgress {
439
531
  durationMs: number;
440
532
  modelOverride?: string | string[];
441
533
  modelSubstitutionWarning?: ModelSubstitutionWarning;
534
+ /** What the router asked for on this child. See {@link TaskRoutingAttribution}. */
535
+ routing?: TaskRoutingAttribution;
442
536
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
443
537
  extractedToolData?: Record<string, unknown[]>;
444
538
  /**
@@ -500,6 +594,8 @@ export interface SingleResult {
500
594
  /** Model's context window in tokens, when known. */
501
595
  contextWindow?: number;
502
596
  modelOverride?: string | string[];
597
+ /** What the router asked for on this child. See {@link TaskRoutingAttribution}. */
598
+ routing?: TaskRoutingAttribution;
503
599
  modelSubstitutionWarning?: ModelSubstitutionWarning;
504
600
  error?: string;
505
601
  aborted?: boolean;
@@ -19,8 +19,8 @@ declare const subagentSchema: z.ZodObject<{
19
19
  timeout_ms: z.ZodOptional<z.ZodNumber>;
20
20
  limit: z.ZodOptional<z.ZodNumber>;
21
21
  verbosity: z.ZodOptional<z.ZodEnum<{
22
- receipt: "receipt";
23
22
  full: "full";
23
+ receipt: "receipt";
24
24
  preview: "preview";
25
25
  }>>;
26
26
  }, z.core.$strip>;
@@ -88,8 +88,8 @@ export declare class SubagentTool implements AgentTool<typeof subagentSchema, Su
88
88
  timeout_ms: z.ZodOptional<z.ZodNumber>;
89
89
  limit: z.ZodOptional<z.ZodNumber>;
90
90
  verbosity: z.ZodOptional<z.ZodEnum<{
91
- receipt: "receipt";
92
91
  full: "full";
92
+ receipt: "receipt";
93
93
  preview: "preview";
94
94
  }>>;
95
95
  }, z.core.$strip>;