@mrciphersmith/keryx 0.3.27 → 0.3.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/dist/core.js CHANGED
@@ -9178,12 +9178,19 @@ var init_export = __esm(() => {
9178
9178
  SKILL_RUNTIMES = [...HARNESS_SKILL_RUNTIMES, "plugin"];
9179
9179
  });
9180
9180
 
9181
+ // src/gdskills/model-version.ts
9182
+ var TRAILING_ALIAS_WORDS;
9183
+ var init_model_version = __esm(() => {
9184
+ TRAILING_ALIAS_WORDS = new Set(["latest", "preview"]);
9185
+ });
9186
+
9181
9187
  // src/gdskills/model-tier.ts
9182
9188
  function isModelTier(value) {
9183
9189
  return typeof value === "string" && MODEL_TIERS.includes(value);
9184
9190
  }
9185
9191
  var MODEL_TIERS, TIER_ALIASES, SKILL_TIER_KEY = "model_tier", TIER_DECLARATION;
9186
9192
  var init_model_tier = __esm(() => {
9193
+ init_model_version();
9187
9194
  MODEL_TIERS = ["light", "standard", "deep"];
9188
9195
  TIER_ALIASES = new Map([["cheap", "light"]]);
9189
9196
  TIER_DECLARATION = new RegExp(`^\\s*(?:[-*]\\s*)?"?(?:${SKILL_TIER_KEY}|model-tier|modelTier)"?\\s*[:=]\\s*(.+?)\\s*,?\\s*$`, "i");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.27",
3
+ "version": "0.3.28",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -57,13 +57,14 @@ candidate set. `src/gdskills/model-tier.ts` then:
57
57
  | Tier | Resolves to |
58
58
  |---|---|
59
59
  | `standard` | the session's model, always |
60
- | `deep` | the highest-ranked discovered model **strictly above** the session's, else the session's |
61
- | `light` | the lowest-ranked discovered model **strictly below** the session's, else the session's |
60
+ | `deep` | the best discovered model **above** the session's — a larger size class, or a NEWER generation of the session model's own family — else the session's |
61
+ | `light` | the **next size step below** the session's (an Opus-class session takes the Sonnet class, not the Haiku class), else the session's; the smallest class only when nothing sits between |
62
62
 
63
63
  Anchoring on the session model is what makes "never a downgrade" checkable
64
64
  rather than hoped for: a tier can only move away from the session model in the
65
65
  direction its own name points, and a candidate at the session's own rank is a
66
- lateral move and never taken.
66
+ lateral move and never taken. An OLDER sibling of the session's own family is
67
+ never `light` or `deep` either.
67
68
 
68
69
  ## What the hints are, and what they are not
69
70
 
@@ -83,13 +84,24 @@ residue, and it is kept in the smallest shape that works:
83
84
  A model whose name is a codename carrying no size is unrankable by design. That
84
85
  is the honest outcome, not a gap to be patched with folklore.
85
86
 
86
- **Version is deliberately not ranked here.** `rankModelId` never parses a
87
- version number out of an id — two ids in the same size-word family but a
88
- different generation (an older vs. a newer numbered release of the same
89
- line) rank IDENTICALLY through this resolution, and a
90
- `standard`/`light`/`deep` dispatch never distinguishes between them. That
91
- stays true for `spawn_subagent` and `keryx review tier` after the decision
92
- below; it is a DIFFERENT consumer's rule, not this one's.
87
+ **Version is a second axis, not a size hint (flow 358).** `rankModelId` still
88
+ never parses a version number out of an id: the size words order size CLASSES.
89
+ Generation is ordered separately, and only within **the same family and
90
+ vendor** — the id with its version tokens removed (`familyKey`) must match. A
91
+ newer release outranks an older one of its own family, so a session on the
92
+ older one resolves `deep` to the newer sibling, and a session on the newer one
93
+ never resolves `light` or `deep` to the older. A version number is **never**
94
+ compared across families: a higher number on one line says nothing about a
95
+ different line.
96
+
97
+ **A larger class of an older generation is ambiguous, not "above".** When the
98
+ only discovered models above the session are a larger size class whose version is
99
+ older than the session's, size words alone cannot call them stronger, and
100
+ `deep` must not move onto a model that is older-generation and — by the
101
+ profile price — the more expensive one. Without an agent the tier keeps the
102
+ session's model as `session-fallback`, with the reason recorded (reason id
103
+ `kept-older-generation`, or `kept-older-generation-pricier` when the profile
104
+ price shows it costs more). The agent fallback below is the only other way out.
93
105
 
94
106
  The routing "derived default table" (`deriveDefaultTable`,
95
107
  `src/harness/routing/derive-default-table.ts`, flow 327/PRD §6.3) is that
@@ -102,8 +114,9 @@ version ranks higher there — the family word (a size marker from this same
102
114
  `MODEL_RANK_HINTS` list) already carries the size class, so version is free
103
115
  to order generations within it. That table reuses
104
116
  `rankModelId`/`MODEL_RANK_HINTS` UNMODIFIED for the size axis and adds its
105
- own separate, conservative version parse on top of it — nothing above
106
- changes for this module's own tier resolution.
117
+ own separate, conservative version parse on top of it. Since flow 358 the tier
118
+ resolution reads the same version parse (`src/gdskills/model-version.ts`, shared
119
+ so neither module imports the other).
107
120
 
108
121
  That version parse (`parseModelVersion`/`familyKey`, round 2) reads BOTH a
109
122
  dotted id (`vendor-opus-5.5`, `gemini-3.8-flash`) and a real
@@ -152,6 +165,35 @@ Do not "fix" a fallback by adding model ids to the code. Add a size word to the
152
165
  hints if one genuinely applies, or leave it: running on the session's model is a
153
166
  correct answer.
154
167
 
168
+ ## The agent fallback: comparing candidate models, nothing else
169
+
170
+ When the ranking is **refused** but the provider did report other models, or a
171
+ tier is **ambiguous** (an older-generation larger class, above), an injected
172
+ agent may be asked to put the candidate models in order. It is a port
173
+ (`TierRankAgent`): `src/gdskills/model-tier.ts` holds no network and no
174
+ filesystem, the caller supplies the real one, and a test supplies a fake.
175
+
176
+ Its limits are the point:
177
+
178
+ - **It compares candidate models and never rates a task's own difficulty.** It is
179
+ shown no task, no diff and no user text — only the discovered ids and the
180
+ profile prices — and it is asked one question: which of these models is more
181
+ capable than which. The tier is still computed by `assignTier` from signals
182
+ the orchestrator holds.
183
+ - It must answer JSON, `{"order": [...]}`, best first, and must place the session's
184
+ own model. An id that was not discovered is dropped. An agent error, a
185
+ timeout, a malformed answer, or one that leaves out the session's model
186
+ resolves to the session's model, exactly as without an agent.
187
+ - It never places `standard` or `deep` below the session: `deep` is only ever taken
188
+ from above the session in its order, `standard` is never asked. A larger
189
+ class of an older generation that costs more than the session's model is
190
+ refused whatever it says.
191
+ - The result is cached by a hash of the candidate catalogue with prices, so an
192
+ identical catalogue asks once.
193
+ - It runs on the **light** tier of the session's own provider — the cheap step
194
+ below the session, or the session model itself when nothing sits below.
195
+ - A resolution it produced is recorded `tier_resolution: agent-ranked`.
196
+
155
197
  ## Where this actually runs
156
198
 
157
199
  Two places, and they are not the same shape:
@@ -236,8 +278,9 @@ arithmetic straight back in the caller's head.
236
278
 
237
279
  ## Adaptive: `inherit: true`
238
280
 
239
- A model is named only when discovery assigned one **other than** the session's
240
- (`tier_resolution: discovered`). In every other case — no session named, ranking
281
+ A model is named only when discovery — or, failing that, the agent fallback —
282
+ assigned one **other than** the session's (`tier_resolution: discovered` or
283
+ `agent-ranked`). In every other case — no session named, ranking
241
284
  refused, or the tier resolving to the session's own model — the block carries the
242
285
  tier and `inherit: true`, and the **host** resolves it against the models its own
243
286
  dispatch tool offers:
@@ -255,18 +298,19 @@ fallback. Exit status is always 0.
255
298
  ## Recording the decision
256
299
 
257
300
  The dispatch carries the tier, the ordered rule ids that produced it, which of
258
- the three outcomes occurred, and what was on the table when it did —
259
- `model.tier`, `model.tier_reasons`, `model.tier_resolution` and
301
+ the outcomes occurred, and what was on the table when it did —
302
+ `model.tier`, `model.tier_reasons` (the assignment rule ids, then `resolve:<reason id>` for the resolution), `model.tier_resolution` and
260
303
  `model.model_discovery` in
261
304
  `.metaproject/core/gdskills/contracts/subagent-dispatch.schema.json`.
262
305
 
263
- `tier_resolution` distinguishes three facts that must not be flattened:
306
+ `tier_resolution` distinguishes four facts that must not be flattened:
264
307
 
265
308
  | Value | Means |
266
309
  |---|---|
267
310
  | `discovered` | a discovered model was assigned to this tier |
268
311
  | `session-ranked` | ranking worked and placed the tier at the session's model |
269
- | `session-fallback` | ranking was refused; the session's model is kept |
312
+ | `session-fallback` | ranking was refused or was ambiguous; the session's model is kept |
313
+ | `agent-ranked` | the agent fallback put the candidates in order and its answer was used |
270
314
 
271
315
  "Fell back to the session model" and "assigned the session model because it
272
316
  ranked there" are different facts. A run that cannot be explained afterwards is
@@ -286,6 +330,9 @@ recording them is one copy rather than four decisions.
286
330
  the resolution code.
287
331
  - Take the candidate set from runtime detection. Never from a literal list of
288
332
  what exists.
333
+ - The agent fallback compares candidate models. Never ask any model to rate a
334
+ task's own difficulty, and never let the fallback place `standard` or `deep`
335
+ below the session's model.
289
336
  - When the candidates cannot be ranked, the block is adaptive (`inherit: true`).
290
337
  The host never goes below its session model for `standard` or `deep`, and takes
291
338
  a lighter class only for a `light` tier.
@@ -79,7 +79,8 @@
79
79
  "enum": [
80
80
  "discovered",
81
81
  "session-ranked",
82
- "session-fallback"
82
+ "session-fallback",
83
+ "agent-ranked"
83
84
  ]
84
85
  },
85
86
  "model_discovery": {
@@ -83,7 +83,7 @@
83
83
  }
84
84
  },
85
85
  "model": {
86
- "description": "Which model runs this dispatch. Skills declare a `tier`; at dispatch time src/gdskills/model-tier.ts DISCOVERS the models runtime provider detection reported for the active provider, ranks them by size markers in their names, and places the tier relative to the session's own model — `standard` is the session model, `deep` a discovered model ranked above it, `light` one ranked below. No table of model ids exists anywhere in the resolution. When the candidates cannot be ranked, every tier keeps the session's model. `provider`/`model` remain available for an explicit lateral selection, and an omitted block inherits the parent verbatim. Two things fill this block. The interactive `spawn_subagent` tool does it in code: it builds the tier map with `buildTierMap` from its host's detection result, hands it to `resolveChildModel`, and records the outcome on the dispatch's run trace. An orchestrator authoring a dispatch document RUNS `keryx review tier`, which takes the §4.4 signals as flags and prints exactly the fields below, ready to paste (`--json` prints this object alone). It is a command rather than a documented function call because an orchestrator is an agent following prose and cannot call a TypeScript function: told to `decideDispatchModel`, it computed the tier in its head from a table, which is the mechanical step that belongs in code. When nothing can be worked out the command emits `inherit: true` instead of `provider`/`model` — the dispatch runs on the caller's own model, never a downgrade. Neither path is enforced by a reader — nothing validates a recorded resolution against the model that actually ran — so these fields are a record, not a gate.",
86
+ "description": "Which model runs this dispatch. Skills declare a `tier`; at dispatch time src/gdskills/model-tier.ts DISCOVERS the models runtime provider detection reported for the active provider, ranks them by size markers in their names, and places the tier relative to the session's own model — `standard` is the session model, `deep` a discovered model above it (a larger size class, or a newer generation of the session model's own family), `light` the next size step below it. When the ranking is refused or ambiguous an injected agent may order the candidate models instead (it compares models, never rates a task); its answer is recorded as `agent-ranked`. No table of model ids exists anywhere in the resolution. When the candidates cannot be ranked, every tier keeps the session's model. `provider`/`model` remain available for an explicit lateral selection, and an omitted block inherits the parent verbatim. Two things fill this block. The interactive `spawn_subagent` tool does it in code: it builds the tier map with `buildTierMap` from its host's detection result, hands it to `resolveChildModel`, and records the outcome on the dispatch's run trace. An orchestrator authoring a dispatch document RUNS `keryx review tier`, which takes the §4.4 signals as flags and prints exactly the fields below, ready to paste (`--json` prints this object alone). It is a command rather than a documented function call because an orchestrator is an agent following prose and cannot call a TypeScript function: told to `decideDispatchModel`, it computed the tier in its head from a table, which is the mechanical step that belongs in code. When nothing can be worked out the command emits `inherit: true` instead of `provider`/`model` — the dispatch runs on the caller's own model, never a downgrade. Neither path is enforced by a reader — nothing validates a recorded resolution against the model that actually ran — so these fields are a record, not a gate.",
87
87
  "type": "object",
88
88
  "additionalProperties": false,
89
89
  "properties": {
@@ -95,14 +95,14 @@
95
95
  "enum": ["light", "standard", "deep"]
96
96
  },
97
97
  "tier_reasons": {
98
- "description": "The ordered rule ids `assignTier` applied, e.g. [\"base:standard\", \"floor:blast-radius\"]. Produced by `keryx review tier` from the signals passed as flags, never written by hand: a reason list an author composes records the story they tell about the tier rather than the rules that produced it. Recorded so a tier can be explained after the run instead of re-derived from a state nobody kept. Ids rather than prose, because they are compared across runs.",
98
+ "description": "The ordered rule ids `assignTier` applied, e.g. [\"base:standard\", \"floor:blast-radius\"], followed by one `resolve:<reason-id>` entry from the model resolution (e.g. `resolve:picked-above`, `resolve:kept-older-generation-pricier`, `resolve:agent-picked-below`). Produced by `keryx review tier` from the signals passed as flags, never written by hand: a reason list an author composes records the story they tell about the tier rather than the rules that produced it. Recorded so a tier can be explained after the run instead of re-derived from a state nobody kept. Ids rather than prose, because they are compared across runs.",
99
99
  "type": "array",
100
100
  "items": { "type": "string", "minLength": 1 }
101
101
  },
102
102
  "tier_resolution": {
103
- "description": "Which of three outcomes produced the model, as `resolveTierModel` reports it on `TierResolution.source`. `discovered` — a model reported by runtime provider detection was assigned to this tier. `session-ranked` — ranking WORKED and placed the tier at the session's own model (always so for `standard`; for `light`/`deep` when nothing discovered ranks below/above the session). `session-fallback` — ranking was refused, or the ranking carried no rank for the session model, so the session's model is used because nothing could be worked out. Collapsing the last two would hide whether the environment was understood. Recorded, never read back: it explains a finished run and gates nothing.",
103
+ "description": "Which of four outcomes produced the model, as `resolveTierModel` reports it on `TierResolution.source`. `discovered` — a model reported by runtime provider detection was assigned to this tier. `session-ranked` — ranking WORKED and placed the tier at the session's own model (always so for `standard`; for `light`/`deep` when nothing discovered ranks below/above the session). `session-fallback` — ranking was refused or was ambiguous (a larger size class that is an older generation), or the ranking carried no rank for the session model, so the session's model is used because nothing could be worked out. `agent-ranked` — the ranking was refused or ambiguous and an injected agent, shown only the discovered ids and profile prices, put the candidate models in order and its validated answer picked the model; it compares models and never rates a task. Collapsing these would hide whether the environment was understood. Recorded, never read back: it explains a finished run and gates nothing.",
104
104
  "type": "string",
105
- "enum": ["discovered", "session-ranked", "session-fallback"]
105
+ "enum": ["discovered", "session-ranked", "session-fallback", "agent-ranked"]
106
106
  },
107
107
  "model_discovery": {
108
108
  "description": "What was on the table when the tier was resolved: which models runtime detection reported for the active provider, how the size-marker hints ordered them, and — when ranking was refused — why. `tier_resolution` alone says a fallback happened and never says what it fell back FROM.",