omp-llm-role 1.0.0 → 1.2.0
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/README.md +202 -12
- package/agents/designer.md +4 -3
- package/docs/dev/README.md +44 -7
- package/docs/dev/agent-authoring.md +59 -1
- package/docs/dev/architecture.md +106 -28
- package/docs/dev/ci.md +163 -0
- package/docs/dev/current-state.md +364 -24
- package/docs/dev/data-sources.md +231 -38
- package/docs/dev/explorer.md +175 -46
- package/docs/dev/quirks.md +77 -4
- package/docs/dev/releasing.md +130 -17
- package/docs/dev/scoring.md +851 -27
- package/docs/dev/testing.md +253 -0
- package/docs/dev/writes-and-state.md +232 -28
- package/docs/llm-role-rankings.md +149 -148
- package/package.json +814 -80
- package/skills/omp-llm-role-create-agent/SKILL.md +7 -1
- package/src/agent-architect.ts +148 -19
- package/src/agent-create.ts +800 -40
- package/src/agent-file.ts +2 -5
- package/src/architect-provenance.ts +16 -0
- package/src/availability.ts +191 -15
- package/src/benchmark-author.ts +9 -4
- package/src/benchmark-sources.ts +283 -18
- package/src/cli/create-role.ts +29 -1
- package/src/cli/llm-role-rank.ts +40 -1
- package/src/config-edit.ts +207 -80
- package/src/engine.ts +519 -75
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +128 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +126 -33
- package/src/extension.ts +464 -48
- package/src/features.ts +105 -0
- package/src/project-registry.ts +65 -0
- package/src/project-setup.ts +382 -0
- package/src/prompts/agent-creation-architect.md +2 -0
- package/src/prompts/project-profile-architect.md +33 -0
- package/src/prompts/project-profile-user.md +8 -0
- package/src/provider-display.ts +67 -0
- package/src/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +211 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +234 -37
- package/web/app.js +434 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
package/docs/dev/spec.md
DELETED
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
# SPEC — omp-llm-role: daily model-role updater plugin
|
|
2
|
-
|
|
3
|
-
Status: spec (implementation not started). Every decision below was confirmed with the
|
|
4
|
-
product owner in a three-round interview (2026-09-21). Ground-truth facts were verified
|
|
5
|
-
live against OpenRouter's API and the local omp install the same day.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. Goal
|
|
10
|
-
|
|
11
|
-
A omp plugin in this repo that keeps `~/.omp/agent/config.yml` → `modelRoles` on today's
|
|
12
|
-
best models: it ranks models per role (existing scoring), keeps only models the user's
|
|
13
|
-
OpenRouter key can actually run, and surgically rewrites the role selectors — with
|
|
14
|
-
thinking suffixes, fallback chains, and a session-start daily trigger.
|
|
15
|
-
|
|
16
|
-
Existing `src/cli/llm-role-rank.ts` stays the scoring engine; the plugin is the actuator on top.
|
|
17
|
-
|
|
18
|
-
## 2. Verified ground truth (2026-09-21)
|
|
19
|
-
|
|
20
|
-
These facts constrain the design; they were probed live, not assumed.
|
|
21
|
-
|
|
22
|
-
| Fact | Evidence |
|
|
23
|
-
|---|---|
|
|
24
|
-
| OpenRouter has **no per-key model allowlist** | `GET /api/v1/key` response fields: `label, limit, limit_reset, limit_remaining, usage, byok_usage, is_free_tier, expires_at, allowed_data_regions, free_model_daily_requests{used,limit,remaining}, rate_limit` — no model/provider restriction field |
|
|
25
|
-
| Key tier + budget is the availability signal | `is_free_tier: false`, `limit: 10` (daily), `limit_remaining`, `/api/v1/credits` → `{total_credits, total_usage}` |
|
|
26
|
-
| `omp token openrouter` prints omp's resolved OpenRouter key | verified, non-empty `sk-or-…` |
|
|
27
|
-
| omp catalog exposes concrete ids incl. `~org/…-latest` aliases and `:batch`/`:free` variants | `omp models find deepseek-v4-flash`: `~deepseek/deepseek-v4-flash-latest`, `deepseek/deepseek-v4-flash`, `deepseek/deepseek-v4-flash-0731`, `…:batch`, `…:free` |
|
|
28
|
-
| `omp models ls --json` is the selector universe | rows: `{provider, id, selector, name, contextWindow, maxTokens, reasoning, thinking[], input[], cost{}}` |
|
|
29
|
-
| `modelRoles` values are `provider/modelId[:thinkingLevel]` | `omp://models.md`; levels `off|minimal|low|medium|high|xhigh|max|auto` |
|
|
30
|
-
| `retry.fallbackChains` keys are selectors **without** thinking suffix | user's config.yml: role values carry `:off/:high/:max`, chain keys don't (values may — `omp://settings.md`: "selectors accept an optional thinking suffix") |
|
|
31
|
-
| omp plugins load via package manifest; `omp plugin link <dir>` for dev | `omp://plugin-manager-installer-plumbing.md`; settings map in `omp-plugins.lock.json`, `omp plugin config set <pkg> <key> <value>` (values stored as strings) |
|
|
32
|
-
| Extensions run in-process under Bun; `ctx.modelRegistry`, `ctx.ui.notify`, `registerCommand` available; `ctx.setTimeout` for contained background work | `omp://extensions.md` |
|
|
33
|
-
| config.yml hot-reloads (task/eval preflight re-reads settings); omp writes it under `config.yml.lock` | `omp://config-usage.md`, `omp://task-agent-discovery.md` |
|
|
34
|
-
|
|
35
|
-
**Interpretation note.** The owner's phrase *"fetch the available providers for that key"*
|
|
36
|
-
maps to reality as: OpenRouter exposes no provider list per key; availability is derived
|
|
37
|
-
from key tier + budget (§5). This interpretation was presented and accepted in round 1.
|
|
38
|
-
|
|
39
|
-
## 3. Decision record
|
|
40
|
-
|
|
41
|
-
| # | Question | Decision |
|
|
42
|
-
|---|---|---|
|
|
43
|
-
| 1 | Form factor | **omp plugin** living in this repo, installed via `omp plugin link` (dev) / install (later) |
|
|
44
|
-
| 2 | Role coverage | **Config-driven weights.** Shipped defaults cover the nine official roles; `designer` ships as an opt-in role (`enabled: false`, §6.2). The settings schema is flexible enough to define full weight sets for custom agent roles |
|
|
45
|
-
| 3 | Key-scoped availability | **Tier gate + catalog check** (§5) |
|
|
46
|
-
| 4 | Config write | **Surgical in-place edit** of `~/.omp/agent/config.yml` (only managed lines change; atomic tmp+rename) |
|
|
47
|
-
| 5 | Thinking suffixes | **Per-role `thinking` field** on the role def, shipped as defaults, overridable in settings |
|
|
48
|
-
| 6 | Trigger | **session_start, UTC-day gated** (first omp session of the day refreshes; later sessions no-op) + manual `/refresh-roles` |
|
|
49
|
-
| 7 | Switch policy | **Hysteresis**: switch only if current model ineligible or new best beats current score by `switchMargin` (default `0.02`; `0` = always take today's best). The margin is a flat band on `value`, so it can veto up to `switchMargin / λ` $/M of savings; a challenger inside the band that undercuts the incumbent's effective price by `priceSwitchFraction` (default `0.5`) is adopted anyway (2026-09-30, `switched-cost`) |
|
|
50
|
-
| 8 | Settings home | **omp plugin settings** (`omp-plugins.lock.json` → `settings["omp-llm-role"]`); plugin deep-merges flat dotted keys and nested objects itself. `omp plugin config set` stores values as strings, so typed role defs are written by `src/cli/create-role.ts` / `/create-agent` / the explorer's Export (all validate through `resolveSettings`); `/remove-agent` deletes them through the same module |
|
|
51
|
-
| 9 | Variant pick | **Exact dated slug**: resolution order exact id → newest dated → bare → `-latest` alias (last resort); never `:batch`; `:free` only on free-tier keys |
|
|
52
|
-
| 10 | Deliverable | **This spec**; implementation in a later session on owner go |
|
|
53
|
-
| 11 | `retry.fallbackChains` | **Auto-populate** #2/#3 per managed role; prune stale keys the plugin wrote |
|
|
54
|
-
| 12 | Provider scope | **`openrouter/*` selectors only** (ranking price/throughput is OpenRouter-derived; the key is an OpenRouter key) |
|
|
55
|
-
| 13 | Naming | Plugin `omp-llm-role`, slash commands `/refresh-roles` (role refresh) + `/explore-roles` (in-session ranking explorer) + `/create-agent` (agent + role + wiring in one command) + `/remove-agent` (delete an agent and its role) |
|
|
56
|
-
| 14 | Provider routing | **Price-based load balancing model**: price and throughput are the 1/price²-weighted means over the stable standard-tier billed routes (OpenRouter's default routing), per-provider data from the model pages; single find route only as fallback |
|
|
57
|
-
|
|
58
|
-
## 4. Architecture
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
~/Documents/omp-llm-role/
|
|
62
|
-
package.json # name: omp-llm-role, omp.extensions: ["./src/extension.ts"]
|
|
63
|
-
src/
|
|
64
|
-
engine.ts # fetch/caches, cardinal transforms, value scoring;
|
|
65
|
-
# computeRankings(models, rolesConfig)
|
|
66
|
-
settings.ts # defaults + dotted-key deep-merge of plugin settings map
|
|
67
|
-
availability.ts # key fetch, tier gate, catalog check, variant resolution
|
|
68
|
-
config-edit.ts # surgical YAML edit for modelRoles + retry.fallbackChains
|
|
69
|
-
state.ts # state + history files
|
|
70
|
-
updater.ts # orchestration: run(trigger, deps) → decisions
|
|
71
|
-
extension.ts # Bun/omp entry: session_start day gate + /refresh-roles + /explore-roles + /create-agent + /remove-agent
|
|
72
|
-
role-settings.ts # the one validated role write path (validate → merge → backup → atomic write for creation **and removal**)
|
|
73
|
-
role-archetypes.ts # purpose -> weight archetype table (10 sets) + keyword fitting
|
|
74
|
-
agent-file.ts # agent .md rendering/placement (frontmatter; body from omp's architect)
|
|
75
|
-
agent-architect.ts # omp's agent-creation architect, run in-process (extension-only)
|
|
76
|
-
agent-create.ts # /create-agent core: purpose -> archetype -> validated role -> agent .md (+ shared arg parser/report)
|
|
77
|
-
agent-remove.ts # /remove-agent core: deletes agent .md and role definition
|
|
78
|
-
explorer/boot.ts # shared explorer launcher (in-process server: bind, port fallback, close)
|
|
79
|
-
explorer/server.ts # explorer HTTP surface (static SPA + JSON API)
|
|
80
|
-
explorer/explain.ts # pure explanation layer (rank rows, decomposition, targets)
|
|
81
|
-
cli/llm-role-rank.ts # report CLI (per-role tables + suggested modelRoles)
|
|
82
|
-
cli/create-role.ts # `node src/cli/create-role.ts --name <role> --weights m=w,...`
|
|
83
|
-
docs/llm-role-rankings.md # generated report (value-ranking format)
|
|
84
|
-
cache/*.json # daily UTC caches (gitignored)
|
|
85
|
-
```
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Runtimes: extension runs **inside omp (Bun)**; CLI entry runs under **Node ≥ 23.6 (type
|
|
89
|
-
stripping)** — this machine has Node 26, no bun. Code must be dual-runtime safe: only
|
|
90
|
-
`node:` builtins + global `fetch`; relative imports with explicit `.ts` extensions.
|
|
91
|
-
|
|
92
|
-
### 4.1 Refactor contract (`src/cli/llm-role-rank.ts` → `engine.ts`)
|
|
93
|
-
|
|
94
|
-
- `loadRankData(deps?)` — fetch/cache chain per source (fresh cache → live fetch →
|
|
95
|
-
stale cache → none), returns models + match counts. Since 2026-09-30 the
|
|
96
|
-
OpenRouter step also fetches the model pages (~150/day, own cache) and blends
|
|
97
|
-
price/throughput per decision #14; the find payload alone is the fallback.
|
|
98
|
-
`opts.roles` names the roles the caller will rank: a role-exclusive source is
|
|
99
|
-
fetched only when some role weights its metric — Design Arena (`website`) and the
|
|
100
|
-
writing leaderboard (`writing`); skipped, the OpenRouter mirror alone populates
|
|
101
|
-
`designElo` and `metrics.writing` is never set. Undefined = fetch all, which is
|
|
102
|
-
what the explorer passes so a user-created role always gets real values for the
|
|
103
|
-
metrics it weights.
|
|
104
|
-
- `computeRankings(models, roles)` — cardinal fixed-anchor transforms (index_* affine
|
|
105
|
-
`(v+20)/80`, benchmarks chance-anchored, throughput log-anchored, `website`/`writing`
|
|
106
|
-
identity over an already-0-1 percentile/score) + quality composite
|
|
107
|
-
`q` + value `q − λ·$/M` + eligibility (`required` non-null, billed price). `roles`
|
|
108
|
-
comes from resolved settings (§7), not the hardcoded `ROLES`.
|
|
109
|
-
- `src/cli/llm-role-rank.ts` keeps its CLI, flags, report format, and suggested-YAML output; its
|
|
110
|
-
suggested `modelRoles` block switches from `PROVIDER_BY_ORG` first-party guesses to the
|
|
111
|
-
same catalog-resolved `openrouter/*` selectors the plugin emits (`PROVIDER_BY_ORG`
|
|
112
|
-
retires from that path).
|
|
113
|
-
- `src/benchmark-sources.ts` is the benchmark-source registry: the single source of
|
|
114
|
-
truth for which sources exist, what metric each feeds, and how to fetch/parse/join
|
|
115
|
-
each. `BENCHMARK_SOURCES` documents the shipped static sources (the llm-stats index
|
|
116
|
-
leaderboard, the writing evidence export, Design Arena, the six raw llm-stats
|
|
117
|
-
benchmarks); `resolveBenchmarkSource(link)` maps a link to a source (a static
|
|
118
|
-
pattern, then the generic llm-stats benchmark for `llm-stats.com/benchmarks/<id>` or
|
|
119
|
-
a bare id); `sourceForMetric(metric)` maps a metric back; `loadBenchmarkScores` is
|
|
120
|
-
the shared cache chain and `applyBenchmarkScores` the shared join. The writing
|
|
121
|
-
branch moves behind the registry entry (its cache, gating and non-fatal failure
|
|
122
|
-
preserved). A new source is one registry entry (or one declaration), not a new
|
|
123
|
-
branch in the engine.
|
|
124
|
-
- **External metrics** are namespaced `<namespace>:<local>` and MUST be dot-free: the
|
|
125
|
-
flat dotted settings path splits on `.` (`setNested(patchObj, key.split("."), value)`),
|
|
126
|
-
so a dotted key mis-nests on read-back. `isKnownMetric` (`src/settings.ts`) is the
|
|
127
|
-
pure name check (`name in KNOWN_METRICS || /^[a-z0-9_-]+:[a-z0-9_-]+$/`); the registry
|
|
128
|
-
is the engine-side resolver, and `settings.ts` never imports it (no cycle). The
|
|
129
|
-
generic llm-stats benchmark's metric is `bench:<normalized-id>` while the source
|
|
130
|
-
retains the raw id for the fetch URL, so the source is persisted as a declaration.
|
|
131
|
-
- **Declarative sources are data, not code**: a user-level `benchmark-sources.json`
|
|
132
|
-
under the agent dir declares `{ id, label, metric, urlPatterns, fetch, payloadPath,
|
|
133
|
-
idField, scoreField, scoreMax, join, fill }`; the plugin executes it
|
|
134
|
-
deterministically (fetch, walk, read, normalize `score / scoreMax`, join, apply).
|
|
135
|
-
A declaration whose metric is not `<namespace>:<local>` is rejected. Adding a
|
|
136
|
-
provider never executes untrusted code.
|
|
137
|
-
- **`/create-agent` resolves benchmark links**: `extractBenchmarkLinks` pulls URLs from
|
|
138
|
-
the free text; the extension resolves each (a saved declaration, then the registry,
|
|
139
|
-
then the authoring step), warms the cache, reports coverage/leader, and folds the
|
|
140
|
-
resolved metric names into `extraBenchmarks`. An unknown link goes through the
|
|
141
|
-
authoring step (`src/benchmark-author.ts`: fetch the link, run an in-process
|
|
142
|
-
architect, validate, dry-run the coverage/leader, confirm via `ctx.ui.select` or
|
|
143
|
-
`--yes`, save the declaration). A link that cannot be resolved or authored fails
|
|
144
|
-
with a clear message — never a silent fallback to generic weights.
|
|
145
|
-
- **`/create-agent` discovers benchmarks**: when a purpose is given and neither
|
|
146
|
-
`--no-discover` nor an explicit `--benchmarks` list is set, the command fetches
|
|
147
|
-
the llm-stats benchmark catalog (`GET /leaderboard/benchmarks`, daily-cached),
|
|
148
|
-
filters it by coverage (`modelCount >= 3`), ranks the survivors by IDF-weighted
|
|
149
|
-
lexical overlap with the purpose (a category that names the purpose's skill is
|
|
150
|
-
the strongest signal), caps the list at 40, and asks omp's configured `judge`
|
|
151
|
-
role (one `noul` question per candidate, batched into a single judgment) which
|
|
152
|
-
are direct measures. A selected benchmark resolves to its shipped metric when
|
|
153
|
-
one exists (`writingbench` → `writing`), else `bench:<id>`, and is folded in as
|
|
154
|
-
a focus metric. Discovery is non-fatal: a catalog or judge failure skips it and
|
|
155
|
-
the role still ranks on its other weights.
|
|
156
|
-
- **The focus-share fit** (`applyFocusBenchmarks`) replaces the mean-weight fold: a
|
|
157
|
-
named benchmark takes `clamp(specialistShare, 0.25, 0.40)` of the non-price budget
|
|
158
|
-
(split across the named set), the archetype's remaining non-price weights are
|
|
159
|
-
rescaled to fill `1 − w_price − focusShare`, and both invariants hold exactly.
|
|
160
|
-
- Verification after any engine change (existing convention): `node src/cli/llm-role-rank.ts --top 5`,
|
|
161
|
-
check stderr `openrouter: matched N/<pool> models (throughput), M priced` plus the
|
|
162
|
-
`openrouter endpoints: K/L model pages` line, and per-role eligible counts.
|
|
163
|
-
|
|
164
|
-
### 4.2 Dependency injection
|
|
165
|
-
|
|
166
|
-
Core modules take injected adapters so both entries share one code path:
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
type Deps = {
|
|
170
|
-
getToken(): Promise<string>; // extension: ModelRegistry.getApiKey("openrouter")
|
|
171
|
-
// CLI: `omp token openrouter` (honors OMP_PROFILE)
|
|
172
|
-
getCatalog(): Promise<CatalogEntry[]>;// extension: ctx.modelRegistry.getAvailable()
|
|
173
|
-
// CLI: `omp models ls --json`
|
|
174
|
-
notify(lines: string[]): void; // extension: ctx.ui.notify; CLI: stdout
|
|
175
|
-
nowUtcDay(): string;
|
|
176
|
-
};
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## 5. Availability model (tier gate + catalog check)
|
|
180
|
-
|
|
181
|
-
### 5.1 Key metadata
|
|
182
|
-
|
|
183
|
-
Bearer-authenticated GETs with the omp-resolved key:
|
|
184
|
-
|
|
185
|
-
- `GET https://openrouter.ai/api/v1/key` → `is_free_tier`, `limit_remaining`, `limit`
|
|
186
|
-
- `GET https://openrouter.ai/api/v1/credits` → `total_credits`, `total_usage`
|
|
187
|
-
|
|
188
|
-
### 5.2 Gate
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
billedUsable = !is_free_tier && limit_remaining > 0 && (total_credits - total_usage) > 0
|
|
192
|
-
freeUsable = free_model_daily_requests.remaining > 0 (from key response)
|
|
193
|
-
|
|
194
|
-
tierFilter(model):
|
|
195
|
-
billedUsable → billed variants only (exclude `:free`, `:batch`)
|
|
196
|
-
else freeUsable → `:free` variants only (exclude `:batch`)
|
|
197
|
-
else → no candidates (run aborts, §9)
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### 5.3 Catalog check
|
|
201
|
-
|
|
202
|
-
Every candidate must resolve to a concrete entry in the **authenticated** omp catalog
|
|
203
|
-
(`provider === "openrouter"`), i.e. a selector omp can actually dispatch. Catalog entries
|
|
204
|
-
also supply `thinking[]` (for suffix clamping sanity) and `input[]` (role filters, §6.3).
|
|
205
|
-
|
|
206
|
-
### 5.4 Variant resolution (per ranked model → selector)
|
|
207
|
-
|
|
208
|
-
Matching ranking row (llm-stats bare id) → openrouter-provider catalog ids: a catalog id
|
|
209
|
-
matches when its suffix after the last `/` equals the ranking id, or equals it after
|
|
210
|
-
stripping a trailing `-latest` (org prefix — including `~`-prefixed — is ignored for
|
|
211
|
-
matching). Among matched candidates, emit in this order:
|
|
212
|
-
|
|
213
|
-
1. **exact id match** (bare or dated, whatever equals the ranking id)
|
|
214
|
-
2. **newest dated id** (max trailing `-MMDD`/date suffix)
|
|
215
|
-
3. **bare id**
|
|
216
|
-
4. `~org/…-latest` alias (last resort — a valid selector beats no update)
|
|
217
|
-
|
|
218
|
-
Never emit `:batch`. `:free` only when the free-tier branch of §5.2 is active. Ties break
|
|
219
|
-
lexicographically. The emitted selector is always `openrouter/<catalogId>` (decision #12).
|
|
220
|
-
|
|
221
|
-
### 5.5 Provider-allowlist probe (added 2026-09-23)
|
|
222
|
-
|
|
223
|
-
The account-level **allowed-providers privacy whitelist** (openrouter.ai/settings/privacy)
|
|
224
|
-
is enforced at request time and invisible to every catalog surface: `/api/v1/key` has no
|
|
225
|
-
provider field (§2 facts), and the key-authenticated `GET /api/v1/models/{id}/endpoints`
|
|
226
|
-
still returns 200 with the full serving-provider list for blocked models. The only
|
|
227
|
-
reliable signal is a real request: a one-token `POST /api/v1/chat/completions` whose 404
|
|
228
|
-
body reads "No allowed providers are available for the selected model …". Models are
|
|
229
|
-
runnable iff at least one serving endpoint's provider is whitelisted — the org prefix is
|
|
230
|
-
**not** a valid filter (aggregator-org models such as `deepseek/*`, `z-ai/*`,
|
|
231
|
-
`inclusionai/*` run via whitelisted third-party endpoints while `openai/*` and
|
|
232
|
-
`anthropic/*` fail on first-party-only routing).
|
|
233
|
-
|
|
234
|
-
Per role, candidates are verified with `probeModel` (`availability.ts`) in a bounded
|
|
235
|
-
walk: the current selector's candidate first (hysteresis must see it), then rank order.
|
|
236
|
-
It stops only when `1 + fallbackChainDepth` clean candidates exist AND — when the current
|
|
237
|
-
candidate is clean — `fallbackChainDepth` clean candidates lie beyond the current rank,
|
|
238
|
-
so every chain entry written to the config is probe-verified and a kept role's chain is
|
|
239
|
-
filled to full depth. Budget-capped at 12 probes per role (a short chain is the graceful
|
|
240
|
-
degradation); verdicts are cached per run (roles share candidates). Only the
|
|
241
|
-
narrow no-allowed-providers 404 disqualifies; every other failure (5xx, timeout, unknown
|
|
242
|
-
model) counts as usable and stays in omp's runtime-fallback domain. Blocked candidates
|
|
243
|
-
are excluded from selection and from fallback chains, are recorded on the decision
|
|
244
|
-
(`blocked[]`) and in history, and a role whose probed candidates are all blocked is left
|
|
245
|
-
untouched with a notify note.
|
|
246
|
-
|
|
247
|
-
## 6. Selection pipeline
|
|
248
|
-
|
|
249
|
-
Per run, per role in the **resolved role set** (shipped defaults ∪ user-defined roles
|
|
250
|
-
that declare weights):
|
|
251
|
-
|
|
252
|
-
1. **Rank** — `computeRankings` with that role's weights/required (cardinal value
|
|
253
|
-
math: `value = q − λ·priceEff`; λ from the price-weight share ÷ $20 or
|
|
254
|
-
`roles.<r>.lambda`. `priceEff` = billed blend × the role's thinking factor
|
|
255
|
-
`(3ρ+1+T)/(3ρ+1)`, gated per model on what it will actually run: the omp
|
|
256
|
-
catalog's per-model `thinking[]` wins when available (a model whose level
|
|
257
|
-
list excludes the role's level is priced bare — omp clamps unsupported
|
|
258
|
-
levels, and the written selector stays bare in that case), meta levels
|
|
259
|
-
(`off`, `auto`) need only a non-empty list, and without the catalog the OR
|
|
260
|
-
`supports_reasoning` flag gates; bare/off roles are unadjusted).
|
|
261
|
-
2. **Filter** — tier gate (§5.2) → catalog resolution (§5.4); unresolvable models drop.
|
|
262
|
-
3. **Choose** with hysteresis (decision #7):
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
best = candidates[0] // highest value
|
|
266
|
-
current = today's config.yml value for role // suffix stripped for identity
|
|
267
|
-
if no current entry → adopt best
|
|
268
|
-
else if current fails tier or catalog gate
|
|
269
|
-
(incl. not in today's ranked pool) → adopt best
|
|
270
|
-
else if best.value - current.value >= switchMargin → adopt best
|
|
271
|
-
else if best.value >= current.value - switchMargin
|
|
272
|
-
&& priceSwitchFraction > 0
|
|
273
|
-
&& best.priceEff <= current.priceEff * (1 - priceSwitchFraction)
|
|
274
|
-
→ adopt best (cost override, reason `switched-cost`)
|
|
275
|
-
else → keep current (chain still refreshed, §6.4)
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
4. **Suffix** — append `roles[role].thinking` (§7) only when the chosen model's
|
|
279
|
-
catalog `thinking[]` includes the level (meta levels `off`, `auto` need only
|
|
280
|
-
a non-empty list) — omp clamps unsupported levels, so an unsupported pin
|
|
281
|
-
would run at a different effort than the role intends; the same gate is
|
|
282
|
-
applied per target when building fallback-chain values (§6.4).
|
|
283
|
-
5. **Diff** — a role with unchanged final selector produces no write and no notify line.
|
|
284
|
-
|
|
285
|
-
Roles in `config.yml` that have **no** weights in resolved settings are never touched
|
|
286
|
-
(e.g. custom agent roles until the owner adds weights for them).
|
|
287
|
-
|
|
288
|
-
### 6.1 Determinism
|
|
289
|
-
|
|
290
|
-
Same inputs → same output. Ranking ties break by: score desc → blended $/M asc → id asc.
|
|
291
|
-
|
|
292
|
-
### 6.2 Shipped default role set
|
|
293
|
-
|
|
294
|
-
Exactly the current `ROLES` from `src/cli/llm-role-rank.ts` (weights/required verbatim, see
|
|
295
|
-
`docs/llm-role-rankings.md` legend for metric meanings): `default, smol, slow, vision, plan,
|
|
296
|
-
commit, tiny, task, advisor, designer`. No custom roles ship beyond these (decision #2).
|
|
297
|
-
|
|
298
|
-
`designer` is the only non-built-in role and ships `enabled: false`: `resolveSettings`
|
|
299
|
-
drops it from the resolved set (like `weights: null`), so a stock run ranks the nine
|
|
300
|
-
built-in roles and never fetches Design Arena. `roles.designer.enabled=true` opts in.
|
|
301
|
-
The shipped `agents/designer.md` is discovered from the plugin's extension root
|
|
302
|
-
regardless, but the plugin keeps it in `task.disabledAgents` until
|
|
303
|
-
`roles.designer.enabled=true` (below), so it is off the roster by default. Its
|
|
304
|
-
`model: "@designer, @default"` chain only matters if a user re-enables the agent by
|
|
305
|
-
hand: the unresolved first entry is skipped and the child runs on `@default` (a bare
|
|
306
|
-
`@designer` would hard-fail — an unresolved `@x` is a literal pattern, not a
|
|
307
|
-
parent-model fallback). The CLI report documents all shipped roles via `--all`.
|
|
308
|
-
|
|
309
|
-
The shipped agent is **opt-in too**: omp has no per-agent frontmatter gate
|
|
310
|
-
(`parseAgentFields` has no `enabled`; `discoverAgents` scans `<ext>/agents/*.md`
|
|
311
|
-
unconditionally), so the plugin manages the core `task.disabledAgents` key in
|
|
312
|
-
`config.yml` — the same key the `/agents` hub writes. The updater derives the pin
|
|
313
|
-
from each agent's `model:` chain (`src/agent-pins.ts`: `discoverAgentPins` scans the
|
|
314
|
-
shipped `agents/`, `~/.omp/agent/agents/` and `<project>/.omp/agents/` dirs, project >
|
|
315
|
-
user > plugin) and keeps an agent in `task.disabledAgents` exactly while the role it
|
|
316
|
-
pins is known to the plugin but disabled; an agent that pins no role, or a
|
|
317
|
-
role the plugin does not know, is left alone. So `designer` is off the roster and
|
|
318
|
-
refuses spawns until `roles.designer.enabled=true`, and the same holds for any agent
|
|
319
|
-
whose `model:` frontmatter pins a disabled role. The patch is surgical (other entries
|
|
320
|
-
and their order preserved, self-checked) and the sync is not day-gated — enabling the
|
|
321
|
-
role takes effect on the next session.
|
|
322
|
-
|
|
323
|
-
**The `/remove-agent` command deletes the agent `.md` file and its role definition, refusing shipped default roles. The plugin now tracks `managedDisabledAgents` to clean up stale `task.disabledAgents` entries when an agent is removed.**
|
|
324
|
-
|
|
325
|
-
### 6.3 Role filters (schema capability)
|
|
326
|
-
|
|
327
|
-
Role definitions may declare `filters: { image: true }` (require image input) and
|
|
328
|
-
`filters: { maxPriceUsdPerM, minContextTokens }`. Shipped defaults: `vision` and `designer` set
|
|
329
|
-
`image: true` (matching the existing multimodal eligibility filter). Filters apply
|
|
330
|
-
before ranking eligibility so percentile norms stay on the unfiltered pool (existing
|
|
331
|
-
behavior — vision already filters this way).
|
|
332
|
-
|
|
333
|
-
### 6.4 Fallback chains (decision #11)
|
|
334
|
-
|
|
335
|
-
After decisions, for **every managed role** (switched, adopted, or kept):
|
|
336
|
-
|
|
337
|
-
- key = chosen selector **without thinking suffix** (a chain key matches the active
|
|
338
|
-
model id, never a level)
|
|
339
|
-
- value = the next `fallbackChainDepth` (default 2) tier-eligible candidates after the
|
|
340
|
-
chosen one, **preferring candidates priced at or below the chosen model** (by
|
|
341
|
-
`priceEff`); when fewer than `fallbackChainDepth` such candidates exist, the remainder
|
|
342
|
-
is filled with the next-best by value. Deduped, each carrying the role's thinking
|
|
343
|
-
suffix when the role has one and that entry's own catalog row advertises thinking
|
|
344
|
-
support (§6 step 4 rule, applied per target) — so a fallback runs at the role's effort
|
|
345
|
-
instead of the session `defaultThinkingLevel`. **Exception:** a key claimed by more than one managed role (two
|
|
346
|
-
roles chosen onto the same model) gets level-free values — one model-scoped chain
|
|
347
|
-
cannot serve two different role levels, so no role's suffix is imposed on another.
|
|
348
|
-
|
|
349
|
-
Maintenance rules:
|
|
350
|
-
|
|
351
|
-
- Write only roles the plugin manages.
|
|
352
|
-
- Replace-in-place when a chain key already exists in the file (YAML duplicate keys are
|
|
353
|
-
forbidden); never duplicate.
|
|
354
|
-
- Prune keys that (a) the plugin wrote on a previous run (tracked in state, §8) and
|
|
355
|
-
(b) are no longer referenced by any managed role's selector. Unwritten, unreferenced
|
|
356
|
-
keys (e.g. the owner's hand-maintained `openrouter/~deepseek/deepseek-v4-flash-latest`
|
|
357
|
-
entry) are left alone unless the plugin needs to write that exact key (then replace).
|
|
358
|
-
- Delete the `modelRoles.<role>` line for a role the plugin managed on a previous run
|
|
359
|
-
(state `managedRoles`) but no longer does — disabled via `enabled: false` /
|
|
360
|
-
`weights: null`, or removed from settings. A stale pin would otherwise keep routing
|
|
361
|
-
`@<role>` (the shipped `designer` agent falls through to its `@default` chain entry
|
|
362
|
-
only when the key is gone).
|
|
363
|
-
|
|
364
|
-
## 7. Plugin settings schema
|
|
365
|
-
|
|
366
|
-
Stored in the omp plugin settings map (`omp-plugins.lock.json` → `settings["omp-llm-role"]`).
|
|
367
|
-
The plugin receives the raw map and **deep-merges flat dotted keys and nested objects
|
|
368
|
-
itself** (`roles.slow.weights.code=0.2` and `roles: { slow: {...} }` both nest, in either
|
|
369
|
-
order); a single `config` key holding JSON is accepted as a power-user escape hatch for
|
|
370
|
-
whole-object overrides. `omp plugin config set <pkg> <key> <value>` stores every value as
|
|
371
|
-
a **string**, which the validator rejects for numbers/arrays/booleans, so typed role defs
|
|
372
|
-
are written by `src/cli/create-role.ts`, `/create-agent` or the explorer's Export (all
|
|
373
|
-
go through `src/role-settings.ts`: validate → merge → backup → atomic write). Role
|
|
374
|
-
removal goes through the same module (`removeRoleSettings`: delete the flat dotted keys
|
|
375
|
-
and any nested entry, backup + atomic write).
|
|
376
|
-
|
|
377
|
-
```jsonc
|
|
378
|
-
// logical shape (defaults shown for knobs; role weights default to §6.2)
|
|
379
|
-
{
|
|
380
|
-
"switchMargin": 0.02, // hysteresis margin on the 0–1 score; 0 = always switch
|
|
381
|
-
"priceSwitchFraction": 0.5, // cost override: inside the margin, a challenger this
|
|
382
|
-
// much cheaper (0.5 = half the $/M) is adopted anyway;
|
|
383
|
-
// 0 disables it
|
|
384
|
-
"writeFallbackChains": true,
|
|
385
|
-
"fallbackChainDepth": 2,
|
|
386
|
-
"roles": {
|
|
387
|
-
"slow": {
|
|
388
|
-
"description": "…",
|
|
389
|
-
// Opt-in gate (default true). `false` drops the role from the resolved
|
|
390
|
-
// set, like `weights: null`; the shipped `designer` default is false.
|
|
391
|
-
"enabled": true,
|
|
392
|
-
"weights": { "general": 0.26, "reasoning": 0.26, "code": 0.18, "agents": 0.13,
|
|
393
|
-
"math": 0.08, "throughput": 0.04, "price": 0.05 },
|
|
394
|
-
"required": ["general", "price", "throughput"],
|
|
395
|
-
"thinking": "high",
|
|
396
|
-
// Per-role thinking level (decision #5; moved out of the former `suffixes`
|
|
397
|
-
// map). The shipped VALUES are hand-authored (design session, never
|
|
398
|
-
// derived from the ranking): smol off, slow high, vision auto, plan auto,
|
|
399
|
-
// commit off, designer auto, default/task/advisor auto, tiny off — no
|
|
400
|
-
// role is bare (absent = no suffix). The 2026-09-30 value review lowered
|
|
401
|
-
// slow max → high and
|
|
402
|
-
// plan/designer high → auto: `medium` is not in any reachable model's
|
|
403
|
-
// catalog thinking[], so a `medium` pin writes bare at a bare price while
|
|
404
|
-
// the session default (`auto`) bills ~1.86×, and the level is a pure cost
|
|
405
|
-
// multiplier — the ranking never rewards it. The FIELD is ranking-active:
|
|
406
|
-
// it scales the price axis
|
|
407
|
-
// (§6 step 1) for models whose catalog thinking[] includes the level, so
|
|
408
|
-
// editing a role's level can change its ranking and picks.
|
|
409
|
-
"filters": { "image": false }
|
|
410
|
-
}
|
|
411
|
-
// add "my-custom-role": { weights: {...}, required: [...], filters: {...} }
|
|
412
|
-
// → that config.yml role gets updated too (decision #2)
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Validation (fail the run, notify, no write): weights > 0, each role's weights sum to
|
|
418
|
-
1.0 ± 0.01, `required` entries ∈ {general, reasoning, math, code, agents, search,
|
|
419
|
-
vision, tool_calling, long_context, mrcr, website, writing, gpqa, aime, swe_bench,
|
|
420
|
-
arc_agi, terminal_bench, tau_bench, price, throughput} (the eligibility gate,
|
|
421
|
-
independent of weights), weightable metric names ∈ the same set, `roles.<role>.thinking` ∈
|
|
422
|
-
{off, minimal, low, medium, high, xhigh, max, auto}, `switchMargin` and
|
|
423
|
-
`priceSwitchFraction` ∈ [0, 1], `roles.<role>.enabled` a boolean. A role entry with
|
|
424
|
-
`weights: null` explicitly opts that role out; `enabled: false` drops a shipped role
|
|
425
|
-
from the resolved set (the `designer` default) and keeps any agent whose `model:` chain
|
|
426
|
-
pins it in `task.disabledAgents` (§6.2). Legacy `suffixes.*` keys are rejected with a migration hint
|
|
427
|
-
(moved into `roles.<role>.thinking`).
|
|
428
|
-
|
|
429
|
-
Six metrics are the raw llm-stats benchmark pass rates — `gpqa` (chance-anchored at
|
|
430
|
-
0.25), `aime`, `swe_bench`, `arc_agi`, `terminal_bench`, `tau_bench` (raw 0-1). They are
|
|
431
|
-
weightable (so `/create-agent`'s "another benchmark" step can name one) but sparse
|
|
432
|
-
(gpqa 62.5%, aime 30.5%, swe_bench 29.0%, arc_agi/terminal_bench/tau_bench ~5-6% of the
|
|
433
|
-
field), so they are differentiators, never `required` gates.
|
|
434
|
-
|
|
435
|
-
Three metrics are **capability-filled**: `website`, `long_context` and `writing`. A
|
|
436
|
-
model missing one is scored at `CAPABILITY_FILL[metric]` instead of 0 — absence is not
|
|
437
|
-
a coverage penalty — and none of the three may be a `required` gate.
|
|
438
|
-
|
|
439
|
-
`website` is the first of them: Design Arena's `models-website` Elo as a percentile
|
|
440
|
-
within the design-covered population, computed once per run in
|
|
441
|
-
`applyDesignPercentiles` (role-independent), so it is never null for a model with a
|
|
442
|
-
general index — the newest frontier models carry no Design Arena data. Shipped weight:
|
|
443
|
-
`designer` 0.18 only (with `code` trimmed to 0.10 against their r = 0.876 collinearity).
|
|
444
|
-
|
|
445
|
-
`writing` is the WritingBench score (0–1, identity transform) from the writing
|
|
446
|
-
leaderboard's canonical export (`/research/best-ai-for-writing/evidence.json`), joined
|
|
447
|
-
by the bare llm-stats id (15/400 covered, all Qwen), computed once per run in
|
|
448
|
-
`applyWritingScores` (role-independent) and only when some ranked role weights it.
|
|
449
|
-
|
|
450
|
-
`long_context` is never written into `metrics`: `rankRole` applies its fill at rank
|
|
451
|
-
time, and `explainModel` mirrors that branch (same untransformed fill, no
|
|
452
|
-
`cardinalMetric` pass) so the explorer's contributions sum exactly to `q`.
|
|
453
|
-
|
|
454
|
-
`CAPABILITY_FILL` (0.195) is a **stated assumption, not a per-metric calibration**: it
|
|
455
|
-
is the percentile implied by the *capability* cohort's uncovered-mean general index
|
|
456
|
-
(29.8 vs covered 38.2). The `writing` cohort is stronger, not weaker — 19.8 covered vs
|
|
457
|
-
22.2 uncovered, i.e. ≈0.513 on the same construction — so the shared constant
|
|
458
|
-
understates an unmeasured model's writing. It stays conservative because 15
|
|
459
|
-
self-reported, unverified rows cannot calibrate a per-metric fill, and a value derived
|
|
460
|
-
per metric would need its own justification for each.
|
|
461
|
-
|
|
462
|
-
## 8. State, history, and the write
|
|
463
|
-
|
|
464
|
-
Files (all under `~/.omp/agent/`, next to the config they describe):
|
|
465
|
-
|
|
466
|
-
- `llm-role-state.json` — `{ lastRunDay, managedRoles, role→lastSelector,
|
|
467
|
-
pluginWrittenChainKeys[], managedDisabledAgents[], previousModelRoles }`.
|
|
468
|
-
`previousModelRoles` is the full snapshot of the last `modelRoles` block before the
|
|
469
|
-
plugin changed it (manual rollback aid; no rollback command in scope).
|
|
470
|
-
`managedDisabledAgents` is the set of agent names the plugin added to
|
|
471
|
-
`task.disabledAgents`; a later run removes a name whose agent file is gone (e.g.
|
|
472
|
-
`/remove-agent`) or whose role is no longer disabled.
|
|
473
|
-
- `llm-role-history.jsonl` — append-only per run: `{ts, trigger, keyMeta{isFreeTier,
|
|
474
|
-
limitRemaining, creditsRemaining}, decisions[{role, from, to, reason: adopted|switched|
|
|
475
|
-
switched-cost|kept-margin|kept-eligible|no-current, scores}]}`.
|
|
476
|
-
|
|
477
|
-
### 8.1 Surgical edit algorithm (decision #4)
|
|
478
|
-
|
|
479
|
-
1. Acquire advisory lock `~/.omp/agent/.llm-role-refresh.lock` (serializes concurrent
|
|
480
|
-
session starts).
|
|
481
|
-
2. Read `~/.omp/agent/config.yml`; parse with the line-oriented reader (no runtime YAML
|
|
482
|
-
dependency — required for marketplace installs); validate structure (block-style
|
|
483
|
-
top level, no inline/indented/duplicate blocks).
|
|
484
|
-
3. Build the patched document as **text**, line-oriented: rewrite only value tokens of
|
|
485
|
-
managed roles inside the top-level `modelRoles:` block (upsert missing roles at the
|
|
486
|
-
block end, two-space indent; delete the line for a role that left the managed set,
|
|
487
|
-
§6.4), the managed keys inside `retry.fallbackChains:`, and the plugin-managed names
|
|
488
|
-
in `task.disabledAgents` (§6.2: add/remove the name of an agent whose pinned role is
|
|
489
|
-
disabled, other entries and their order preserved). All other bytes identical — comments, blank lines, unknown
|
|
490
|
-
keys untouched.
|
|
491
|
-
4. Values always emitted YAML-quoted (`"openrouter/z-ai/glm-5.3-flash:high"`).
|
|
492
|
-
5. mtime re-check before write; if config.yml changed underneath, re-read + recompute
|
|
493
|
-
(max 3 attempts). Atomic: temp file + `rename`.
|
|
494
|
-
6. Read the patched text back through the same line-oriented reader and assert it equals
|
|
495
|
-
the intended state; on any mismatch abort without writing. Never corrupt.
|
|
496
|
-
|
|
497
|
-
## 9. Error handling
|
|
498
|
-
|
|
499
|
-
| Failure | Behavior |
|
|
500
|
-
|---|---|
|
|
501
|
-
| Ranking data: fetch fails, no fresh cache | Existing chain: fresh cache → live fetch → stale cache → **abort, notify** (no write) |
|
|
502
|
-
| Key fetch (`omp token`/registry) or `/api/v1/key`,`/api/v1/credits` fails | **Abort, notify** — without tier info no selector is trustworthy |
|
|
503
|
-
| Catalog fetch fails | Abort, notify |
|
|
504
|
-
| Settings validation fails (§7) | Abort, notify the offending role/key |
|
|
505
|
-
| Both tier branches false (no budget, no free quota) | Abort, notify |
|
|
506
|
-
| Write conflict after 3 retries | Abort, notify; state file untouched |
|
|
507
|
-
| Zero decisions changed | No role write; the non-day-gated `task.disabledAgents` sync (§6.2) can still rewrite `config.yml` (mtime moves) with no role change, and that path appends no history row. A full run that changes nothing still appends a history row |
|
|
508
|
-
|
|
509
|
-
Session-start runs are **awaited** before the first prompt is dispatched (a deferred
|
|
510
|
-
timer would be cleared when a short-lived session exits before it fires); the day gate
|
|
511
|
-
(`state.lastRunDay !== today(UTC)`) makes the ranking run at most once/day; a concurrent
|
|
512
|
-
second starter loses the lock and finds the day already stamped → no-op. The
|
|
513
|
-
pin-derived `task.disabledAgents` sync (§6.2) is **not** day-gated: it runs on every
|
|
514
|
-
session start so enabling/disabling a role takes effect on the next session.
|
|
515
|
-
|
|
516
|
-
## 10. Plugin surfaces
|
|
517
|
-
|
|
518
|
-
- **Manifest**: `package.json` → `"omp": { "extensions": ["./src/extension.ts"] }`;
|
|
519
|
-
dev install `omp plugin link ~/Documents/omp-llm-role` (repo stays the source of truth).
|
|
520
|
-
- **session_start**: day-gated refresh (§9), notify only on switches/errors.
|
|
521
|
-
- **`/refresh-roles`**: synchronous forced run in-session; notification summarizes every
|
|
522
|
-
decision (kept lines included when verbose).
|
|
523
|
-
- **`/explore-roles [--port N] [--no-open]`**: boots the interactive ranking explorer
|
|
524
|
-
**in-process** (catalog from the live model registry), notifies the URL and opens the
|
|
525
|
-
browser. Roles come from the user-level lock file (`project: null`); a busy port falls
|
|
526
|
-
back to an OS-assigned one; a repeat invocation re-notifies the running URL; the handle
|
|
527
|
-
is closed on `session_shutdown`.
|
|
528
|
-
- **`/create-agent <request> [flags]`** (free text) or
|
|
529
|
-
**`/create-agent --name <n> --purpose "<text>" [flags]`**: creates the agent
|
|
530
|
-
**and** its role in one command. The free-text form takes the text up to the
|
|
531
|
-
first `--flag` as the purpose, folds any benchmark it names into the weights
|
|
532
|
-
(`extractBenchmarks`), records any benchmark link it points at
|
|
533
|
-
(`extractBenchmarkLinks`), and uses the architect's `identifier` as the name;
|
|
534
|
-
trailing flags still apply. Each link is resolved (a saved declaration, then
|
|
535
|
-
the registry, then the authoring step) and its metric folded into the weights
|
|
536
|
-
as a decisive focus share; an unknown link is authored into a source
|
|
537
|
-
declaration after a dry-run coverage/leader report and a confirmation
|
|
538
|
-
(`--yes` covers headless runs). Unless `--no-discover` or an explicit
|
|
539
|
-
`--benchmarks` list is given, it also discovers the catalog benchmarks relevant
|
|
540
|
-
to the purpose (coverage filter → IDF lexical rank → the configured `judge`
|
|
541
|
-
role) and folds them in. Runs **omp's agent-creation architect**
|
|
542
|
-
in-process (`src/agent-architect.ts`: the `/agents` hub's prompt shipped verbatim
|
|
543
|
-
in `src/prompts/`, run through `createAgentSession` with no tools) to author the
|
|
544
|
-
routing rule and the body, then adds the `model: "@<n>, @default"` and `tools:`
|
|
545
|
-
frontmatter omp's own writer omits. Fits the weights to the purpose from
|
|
546
|
-
`src/role-archetypes.ts` (10 sets; `--archetype` forces one, `--weights`
|
|
547
|
-
overrides), asks the user for extra benchmarks to fold in (listing every
|
|
548
|
-
weightable metric with `--list-benchmarks`; `--benchmarks m,...` covers headless
|
|
549
|
-
runs), writes the validated role, authors `~/.omp/agent/agents/<n>.md`
|
|
550
|
-
(`--scope project` → `<anchor>/.omp/agents/`), then runs the updater in-process so
|
|
551
|
-
`modelRoles.<n>` lands in `config.yml`. All-or-nothing: an existing agent file
|
|
552
|
-
without `--force`, a name outside `[A-Za-z0-9_-]+`, a reserved name, or a
|
|
553
|
-
weight set violating Σ = 1 / Σ(non-price) = 1 − w_price aborts **before** either
|
|
554
|
-
write. `--body-file` replaces the architect body. `--list-archetypes` prints the
|
|
555
|
-
archetype table.
|
|
556
|
-
- **`/remove-agent --name <n> [--scope user|project] [--lock PATH] [--yes] [--dry-run]`**:
|
|
557
|
-
the inverse of `/create-agent`. Deletes the role's lock-file keys
|
|
558
|
-
(`removeRoleSettings`: backup + atomic write) and the agent `.md` from the user
|
|
559
|
-
and/or project scope, then runs the updater in-process so `modelRoles.<n>` and the
|
|
560
|
-
plugin-managed `task.disabledAgents` entry are dropped. Refuses a shipped default
|
|
561
|
-
role (`DEFAULT_ROLES`), a reserved name, and an invalid name; errors when there is
|
|
562
|
-
nothing to remove. Asks for confirmation in-session unless `--yes` (skipped when
|
|
563
|
-
there is no UI).
|
|
564
|
-
- **Role authoring**: `node src/cli/create-role.ts --name <role> --weights m=w,...` writes a
|
|
565
|
-
validated role def into the settings lock file (backup + atomic write); the shipped
|
|
566
|
-
skill `omp-llm-role-create-agent` drives agent authoring + role creation + verification
|
|
567
|
-
by hand (for bodies that need real authoring rather than the archetype scaffold).
|
|
568
|
-
|
|
569
|
-
## 11. Verification plan (implementation gate)
|
|
570
|
-
|
|
571
|
-
1. **Refactor sanity** — `node src/cli/llm-role-rank.ts --top 5`: stderr match/eligible counts
|
|
572
|
-
identical to pre-refactor run; report diff empty.
|
|
573
|
-
2. **Tier gate fixtures** — unit: paid+credit → billed-only; free-tier key → `:free`-only;
|
|
574
|
-
zero budget both branches → abort.
|
|
575
|
-
3. **Variant resolution fixtures** — exact/dated/bare/alias ordering; `:batch` never
|
|
576
|
-
emitted; `:free` only on free branch.
|
|
577
|
-
4. **Surgical edit fixtures** — commented config, unknown keys, missing roles upsert,
|
|
578
|
-
chain-key replace-in-place (no duplicate YAML keys), unchanged file → zero-byte diff
|
|
579
|
-
and no mtime change.
|
|
580
|
-
5. **Hysteresis fixtures** — no-current adopt; ineligible current switch; margin-below
|
|
581
|
-
keep; margin-above switch; `switchMargin: 0` always takes best; cost override —
|
|
582
|
-
challenger inside the margin at ≥ `priceSwitchFraction` cheaper switches
|
|
583
|
-
(`switched-cost`), `priceSwitchFraction: 0` keeps the margin in charge, and an
|
|
584
|
-
inside-margin challenger short of the fraction stays kept.
|
|
585
|
-
6. **Chain pruning fixtures** — plugin-written stale keys removed; owner-written keys
|
|
586
|
-
preserved.
|
|
587
|
-
7. **Live E2E** — `omp plugin link .` → new omp session → observe day-gated run;
|
|
588
|
-
`/refresh-roles` → verify config.yml diff + notification; second same-day session →
|
|
589
|
-
no-op; a forced `/refresh-roles` matches in-session decisions;
|
|
590
|
-
`/explore-roles --no-open` → `curl` the notified port for `/api/bootstrap`, a repeat
|
|
591
|
-
invocation keeps the same port, and the port is released when the session ends.
|
|
592
|
-
8. **Agent creation** — `create-agent` fixtures: a purpose fits the expected archetype;
|
|
593
|
-
a `--weights` set violating Σ(non-price) = 1 − w_price is refused; an existing agent
|
|
594
|
-
file without `--force` is refused **and writes no role**; `--dry-run` writes neither
|
|
595
|
-
file; an architect `spec` replaces the description/body; `applyFocusBenchmarks` gives
|
|
596
|
-
a named benchmark a decisive share, is a no-op for the archetype's own specialist set,
|
|
597
|
-
and keeps both invariants. Live: `/create-agent` in a session (RPC mode dispatches
|
|
598
|
-
slash commands) → the architect authors the body, the updater line
|
|
599
|
-
`@<n>: (unset) -> <selector>` + `modelRoles.<n>` land in `config.yml`, then a headless
|
|
600
|
-
spawn whose record reads `{"agent":"<n>","agentSource":"user","modelRole":"<n>"}` with
|
|
601
|
-
`resolvedModel` equal to the role's selector.
|
|
602
|
-
9. **Benchmark sources** — `benchmark-sources` fixtures: `resolveBenchmarkSource` maps an
|
|
603
|
-
llm-stats benchmark page, a bare benchmark id, the writing leaderboard and a Design
|
|
604
|
-
Arena link, and returns `null` for an unknown host; `normalizeMetricKey` is dot-free;
|
|
605
|
-
`parseBenchmarkPayload` reads the llm-stats `entries[]` and writing evidence shapes and
|
|
606
|
-
rejects junk; `applyBenchmarkScores` fills uncovered models; `loadBenchmarkScores`
|
|
607
|
-
follows fresh → live → stale with a temp cache dir and an injected fetch; a declaration
|
|
608
|
-
executes (payload path, id/score fields, `scoreMax`, join) and a metric colliding with a
|
|
609
|
-
shipped key is rejected; `dryRunDeclaration` reports coverage/leader and rejects a
|
|
610
|
-
zero-join; `extractBenchmarkLinks` pulls URLs from prose. Live: a role weighting
|
|
611
|
-
`bench:<id>` fetches the source and ranks on it; the explorer's `/api/bootstrap` lists
|
|
612
|
-
the external metric with its derived metadata.
|
|
613
|
-
10. **Agent removal** — `remove-agent` fixtures: the role's lock-file keys and the agent
|
|
614
|
-
file are deleted (backup written); a shipped default role, a reserved name and an
|
|
615
|
-
invalid name are refused; nothing-to-remove errors; `--dry-run` writes nothing; the
|
|
616
|
-
updater drops `modelRoles.<n>` and the plugin-managed `task.disabledAgents` entry
|
|
617
|
-
(state `managedDisabledAgents`). Live: `/remove-agent --name <n>` in a session
|
|
618
|
-
deletes both artifacts and the updater pass removes the config entries.
|
|
619
|
-
|
|
620
|
-
## 12. Out of scope
|
|
621
|
-
|
|
622
|
-
First-party provider selectors (decision #12), non-OpenRouter scoring sources, a
|
|
623
|
-
`/rollback` command (state snapshot only), marketplace publishing (link/install is the
|
|
624
|
-
path), and auto-tuning `switchMargin`. Weight editing is the explorer's Export,
|
|
625
|
-
`src/cli/create-role.ts` or `/create-agent` (all validate through `resolveSettings`);
|
|
626
|
-
role/agent removal is `/remove-agent` (through `removeRoleSettings`).
|
|
627
|
-
|
|
628
|
-
Benchmark sources: a generic scraper that auto-detects a payload with no user review
|
|
629
|
-
(the declarative spec + authoring step is the mechanism), a UI for browsing/editing
|
|
630
|
-
sources (the source file is edited by hand or by the authoring step; the explorer's
|
|
631
|
-
role editor gains the external metrics already in use), changing the cardinal
|
|
632
|
-
transform classes (external metrics are normalized to 0–1 at parse time), and
|
|
633
|
-
re-ranking or re-fetching beyond the existing daily UTC cache chain.
|