@sayknow-cli/coding-agent 0.5.24 → 0.5.25

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,7 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.5.24] - 2026-09-21
5
+ ## [0.5.25] - 2026-09-21
6
6
 
7
7
  ## [0.5.22] - 2026-09-19
8
8
 
@@ -2457,6 +2457,20 @@ export declare const SETTINGS_SCHEMA: {
2457
2457
  readonly type: "string";
2458
2458
  readonly default: "";
2459
2459
  };
2460
+ /**
2461
+ * Frontend planning model — the **domain** axis.
2462
+ *
2463
+ * When a planning role (planner/architect) is handed work that clearly reads
2464
+ * as frontend/UI, it takes this model instead of the ladder's answer. A
2465
+ * design-strong model is not "more capable" than a code-strong one; this is a
2466
+ * lateral swap. Unset disables it entirely, and implementation roles are
2467
+ * never swapped by domain — the user asked for the design model to plan the
2468
+ * frontend, not to write it.
2469
+ */
2470
+ readonly "task.modelRouting.frontendModel": {
2471
+ readonly type: "string";
2472
+ readonly default: "";
2473
+ };
2460
2474
  readonly "ttsr.enabled": {
2461
2475
  readonly type: "boolean";
2462
2476
  readonly default: true;
@@ -19,8 +19,31 @@ export interface TaskRoutingPolicy {
19
19
  * this bar sits higher than the upgrade bar on purpose.
20
20
  */
21
21
  minDowngradeConfidence: number;
22
+ /**
23
+ * Model for frontend planning, when the assignment reads as frontend work.
24
+ *
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.
29
+ */
30
+ frontendModel?: string;
31
+ /**
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.
35
+ */
36
+ minDomainConfidence: number;
22
37
  }
23
38
  export declare const DEFAULT_TASK_ROUTING_POLICY: Omit<TaskRoutingPolicy, "tiers">;
39
+ /**
40
+ * Roles whose output is a plan or a design review.
41
+ *
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.
45
+ */
46
+ export declare const PLANNING_ROLES: ReadonlySet<string>;
24
47
  export interface TaskRoutingRequest {
25
48
  agentName: string;
26
49
  /** The assignment text the subagent will act on. */
@@ -31,7 +54,8 @@ export interface TaskRoutingRequest {
31
54
  }
32
55
  export interface TaskRoutingResult {
33
56
  model: string;
34
- tier: TaskTier;
57
+ /** Null when the move was a domain swap — that axis has no ladder. */
58
+ tier: TaskTier | null;
35
59
  reason: string;
36
60
  }
37
61
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/coding-agent",
4
- "version": "0.5.24",
4
+ "version": "0.5.25",
5
5
  "description": "Sayknow-CLI CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -54,12 +54,12 @@
54
54
  "@agentclientprotocol/sdk": "1.3.0",
55
55
  "@babel/parser": "^7.29.3",
56
56
  "@mozilla/readability": "^0.6.0",
57
- "@sayknow-cli/stats": "0.5.24",
58
- "@sayknow-cli/agent-core": "0.5.24",
59
- "@sayknow-cli/ai": "0.5.24",
60
- "@sayknow-cli/natives": "0.5.24",
61
- "@sayknow-cli/tui": "0.5.24",
62
- "@sayknow-cli/utils": "0.5.24",
57
+ "@sayknow-cli/stats": "0.5.25",
58
+ "@sayknow-cli/agent-core": "0.5.25",
59
+ "@sayknow-cli/ai": "0.5.25",
60
+ "@sayknow-cli/natives": "0.5.25",
61
+ "@sayknow-cli/tui": "0.5.25",
62
+ "@sayknow-cli/utils": "0.5.25",
63
63
  "@puppeteer/browsers": "^2.13.0",
64
64
  "@types/turndown": "5.0.6",
65
65
  "@xterm/headless": "^6.0.0",
@@ -2039,6 +2039,17 @@ export const SETTINGS_SCHEMA = {
2039
2039
  "task.modelRouting.balancedModel": { type: "string", default: "" },
2040
2040
  /** Most capable tier. Unclear cause, cross-cutting design, hard to undo. */
2041
2041
  "task.modelRouting.deepModel": { type: "string", default: "" },
2042
+ /**
2043
+ * Frontend planning model — the **domain** axis.
2044
+ *
2045
+ * When a planning role (planner/architect) is handed work that clearly reads
2046
+ * as frontend/UI, it takes this model instead of the ladder's answer. A
2047
+ * design-strong model is not "more capable" than a code-strong one; this is a
2048
+ * lateral swap. Unset disables it entirely, and implementation roles are
2049
+ * never swapped by domain — the user asked for the design model to plan the
2050
+ * frontend, not to write it.
2051
+ */
2052
+ "task.modelRouting.frontendModel": { type: "string", default: "" },
2042
2053
 
2043
2054
  // TTSR
2044
2055
  "ttsr.enabled": {
@@ -37,6 +37,21 @@ export interface TaskRoutingPolicy {
37
37
  * this bar sits higher than the upgrade bar on purpose.
38
38
  */
39
39
  minDowngradeConfidence: number;
40
+ /**
41
+ * Model for frontend planning, when the assignment reads as frontend work.
42
+ *
43
+ * This is the **domain** axis, not a rung on the ladder: a design-strong model
44
+ * is not "better" than a code-strong one, it is a different specialty. Only
45
+ * planning roles ever take it, and only laterally — the implementation roles
46
+ * stay on the difficulty ladder.
47
+ */
48
+ frontendModel?: string;
49
+ /**
50
+ * Bar for the lateral swap above. Directional bars do not apply here because
51
+ * neither direction is "spending more": being wrong either way costs quality,
52
+ * symmetrically, so one bar is the whole story.
53
+ */
54
+ minDomainConfidence: number;
40
55
  }
41
56
 
42
57
  export const DEFAULT_TASK_ROUTING_POLICY: Omit<TaskRoutingPolicy, "tiers"> = {
@@ -46,8 +61,18 @@ export const DEFAULT_TASK_ROUTING_POLICY: Omit<TaskRoutingPolicy, "tiers"> = {
46
61
  // overriding one needs a stronger signal in either direction.
47
62
  minUpgradeConfidence: 0.5,
48
63
  minDowngradeConfidence: 0.75,
64
+ minDomainConfidence: 0.6,
49
65
  };
50
66
 
67
+ /**
68
+ * Roles whose output is a plan or a design review.
69
+ *
70
+ * These are the only roles the domain swap applies to — the user's intent is
71
+ * "a design-strong model *plans* the frontend; implementation stays where it
72
+ * is". Executor keeps the difficulty ladder regardless of domain.
73
+ */
74
+ export const PLANNING_ROLES: ReadonlySet<string> = new Set(["planner", "architect"]);
75
+
51
76
  /**
52
77
  * The questions describe the *work*, never a model name.
53
78
  *
@@ -71,6 +96,11 @@ function buildQuestions(): Record<string, Question> {
71
96
  instructions:
72
97
  "Does this assignment touch production, money, credentials, published releases, or state that cannot be undone?",
73
98
  },
99
+ domain: {
100
+ type: "noul",
101
+ instructions:
102
+ "Is this assignment frontend/UI work — interfaces, components, visual design, styling or interaction — rather than data, APIs, infrastructure or business logic?",
103
+ },
74
104
  };
75
105
  }
76
106
 
@@ -85,7 +115,8 @@ export interface TaskRoutingRequest {
85
115
 
86
116
  export interface TaskRoutingResult {
87
117
  model: string;
88
- tier: TaskTier;
118
+ /** Null when the move was a domain swap — that axis has no ladder. */
119
+ tier: TaskTier | null;
89
120
  reason: string;
90
121
  }
91
122
 
@@ -140,9 +171,9 @@ export async function routeTaskModel(
140
171
  request: TaskRoutingRequest,
141
172
  ): Promise<TaskRoutingResult | null> {
142
173
  const configured = TASK_TIERS.filter(tier => policy.tiers[tier]);
143
- // One tier is not a ladder; with nothing to move between there is no decision
144
- // worth paying a model call for.
145
- if (configured.length < 2) return null;
174
+ const frontendModel = policy.frontendModel?.trim() || undefined;
175
+ // Neither axis has anything to move on: no ladder and no domain model.
176
+ if (configured.length < 2 && !frontendModel) return null;
146
177
 
147
178
  const assignment = request.assignment.trim();
148
179
  if (assignment.length < 24) return null;
@@ -154,6 +185,37 @@ export async function routeTaskModel(
154
185
  });
155
186
  if (!result) return null;
156
187
 
188
+ // --- Domain axis: lateral swap for planning roles ---
189
+ //
190
+ // A design-strong model is not "more capable" than a code-strong one, so this
191
+ // is not a rung on the ladder and the directional bars do not apply. When the
192
+ // assignment clearly reads as frontend work and a frontend model is
193
+ // configured, planning roles take it — that is the whole of the user's
194
+ // intent: the design model *plans* the frontend, implementation stays put.
195
+ // When the swap fires, the difficulty ladder is skipped entirely for this
196
+ // spawn; for planning, design judgment is the point, not raw capability.
197
+ const domainAnswer = result.answers.domain;
198
+ if (
199
+ frontendModel &&
200
+ PLANNING_ROLES.has(request.agentName) &&
201
+ domainAnswer?.type === "noul" &&
202
+ domainAnswer.noul >= policy.minDomainConfidence
203
+ ) {
204
+ if (request.currentModel && matchesModel(frontendModel, request.currentModel)) return null;
205
+ logger.debug("decisions/task-routing: routed", {
206
+ agent: request.agentName,
207
+ model: frontendModel,
208
+ reason: `frontend (domain ${domainAnswer.noul.toFixed(2)})`,
209
+ });
210
+ return {
211
+ model: frontendModel,
212
+ tier: null,
213
+ reason: `frontend (domain ${domainAnswer.noul.toFixed(2)})`,
214
+ };
215
+ }
216
+
217
+ // --- Difficulty axis: the ladder ---
218
+ if (configured.length < 2) return null;
157
219
  const answer = result.answers.tier;
158
220
  if (answer?.type !== "choice") return null;
159
221
  let tier = TASK_TIERS.find(candidate => candidate === answer.choice);
package/src/task/index.ts CHANGED
@@ -484,7 +484,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
484
484
  balanced: this.session.settings.get("task.modelRouting.balancedModel") || undefined,
485
485
  deep: this.session.settings.get("task.modelRouting.deepModel") || undefined,
486
486
  };
487
- if (Object.values(tiers).filter(Boolean).length < 2) return undefined;
487
+ const frontendModel = this.session.settings.get("task.modelRouting.frontendModel") || undefined;
488
+ if (Object.values(tiers).filter(Boolean).length < 2 && !frontendModel) return undefined;
488
489
 
489
490
  const assignment = (tasks ?? [])
490
491
  .map(task => [task.description, task.assignment].filter(Boolean).join("\n"))
@@ -499,7 +500,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
499
500
  if (!registry) return undefined;
500
501
  const routed = await routeTaskModel(
501
502
  createDecisionService({ registry, settings: this.session.settings, enabled: true }),
502
- { ...DEFAULT_TASK_ROUTING_POLICY, tiers },
503
+ { ...DEFAULT_TASK_ROUTING_POLICY, tiers, frontendModel },
503
504
  // A role may be configured with a fallback chain; the first entry is what it
504
505
  // actually runs on, so that is the baseline the direction is measured from.
505
506
  {