omp-llm-role 1.0.0 → 1.1.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 +179 -11
- 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 +84 -27
- package/docs/dev/ci.md +152 -0
- package/docs/dev/current-state.md +327 -24
- package/docs/dev/data-sources.md +202 -38
- package/docs/dev/explorer.md +157 -46
- package/docs/dev/quirks.md +45 -4
- package/docs/dev/releasing.md +129 -17
- package/docs/dev/scoring.md +789 -28
- package/docs/dev/testing.md +206 -0
- package/docs/dev/writes-and-state.md +176 -28
- package/docs/llm-role-rankings.md +141 -140
- package/package.json +754 -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 +104 -6
- 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 +409 -46
- package/src/error-report.ts +562 -0
- package/src/explorer/boot.ts +36 -24
- package/src/explorer/explain.ts +110 -13
- package/src/explorer/scopes.ts +110 -0
- package/src/explorer/server.ts +112 -32
- package/src/extension.ts +407 -34
- package/src/features.ts +103 -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/role-archetypes.ts +12 -2
- package/src/role-settings.ts +22 -0
- package/src/settings.ts +205 -24
- package/src/state.ts +1 -1
- package/src/updater.ts +150 -31
- package/web/app.js +411 -12
- package/web/index.html +1 -0
- package/web/style.css +44 -1
- package/docs/dev/spec.md +0 -633
package/docs/dev/explorer.md
CHANGED
|
@@ -12,10 +12,12 @@ explorer never reimplements `value = q − λ·$/M`.
|
|
|
12
12
|
| File | Role |
|
|
13
13
|
|---|---|
|
|
14
14
|
| `src/explorer/server.ts` | Zero-dependency `node:http` surface: static SPA + JSON API. Exports `createExplorerServer`, `ExplorerOpts`. |
|
|
15
|
-
| `src/explorer/explain.ts` | Pure explanation layer (no I/O): rank rows with baseline deltas, per-model decomposition, inverse-cardinal targets. Exports `rankRows`, `explainModel`, `inverseCardinal`, `METRIC_META`, `WEIGHTABLE_METRICS`. |
|
|
16
|
-
| `src/explorer/
|
|
15
|
+
| `src/explorer/explain.ts` | Pure explanation layer (no I/O): rank rows with baseline deltas, per-model decomposition, inverse-cardinal targets, and the keyed-catalog availability overlay. Exports `rankRows`, `explainModel`, `inverseCardinal`, `METRIC_META`, `WEIGHTABLE_METRICS`. |
|
|
16
|
+
| `src/explorer/scopes.ts` | Scope resolution (no HTTP): the user-level scope, the registry's projects, and the session's project; the per-scope read; the dataset union. Exports `Scope`, `resolveScopes`, `defaultScopeId`, `resolveScope`, `readScopeRoles`, `unionRoles`. |
|
|
17
|
+
| `src/explorer/boot.ts` | Shared launcher: bind/port fallback, scope resolution, browser open, close. Exports `startExplorer`, `EXPLORER_DEFAULT_PORT`, `ExplorerHandle`, `ExplorerBootOpts`. |
|
|
18
|
+
| `src/project-registry.ts` | The global project registry (`llm-role-projects.json` in the agent dir): read/register/prune, atomic write. Exports `PROJECT_REGISTRY_FILE`, `readProjectRegistry`, `registerProject`. |
|
|
17
19
|
| `src/role-settings.ts` | The one validated role write path (validate → merge → backup → atomic write). Exports `validateRole`, `mergeExport`, `writeRoleSettings`, `timestamp`. |
|
|
18
|
-
| `web/app.js` | The SPA: renders and edits role defs, calls the API. No framework, no build step, no external requests. |
|
|
20
|
+
| `web/app.js` | The SPA: renders and edits role defs, switches scopes, calls the API. No framework, no build step, no external requests. |
|
|
19
21
|
| `src/extension.ts` | `/explore-roles`: in-process `startExplorer` (catalog from the live model registry), closed on `session_shutdown`. |
|
|
20
22
|
|
|
21
23
|
## HTTP surface (`src/explorer/server.ts`)
|
|
@@ -29,47 +31,76 @@ caller; no auth. Every handler is wrapped so a throw becomes a 500 JSON error
|
|
|
29
31
|
```ts
|
|
30
32
|
type ExplorerOpts = {
|
|
31
33
|
webDir: string;
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// All known scopes, re-resolved per call so a project registered by another
|
|
35
|
+
// session appears without a restart.
|
|
36
|
+
listScopes(): Scope[];
|
|
37
|
+
// The scope a request should use: the requested id when known and present,
|
|
38
|
+
// else the default (session project when present, else user-level).
|
|
39
|
+
resolveScope(requestedId: string | null): Scope;
|
|
40
|
+
// Fresh per call: rank (swapped by refresh) plus the scope's roles/universe
|
|
41
|
+
// re-read from its lock file, so a page reload after Export reflects the write.
|
|
42
|
+
getState(scope: Scope): {
|
|
36
43
|
rank: RankData;
|
|
37
44
|
roles: Record<string, RoleDef>;
|
|
38
45
|
universe: Record<string, UniverseEntry>; // every known role, disabled included
|
|
39
46
|
defaults: Record<string, RoleDef>;
|
|
47
|
+
availability: KeyAvailability; // keyed-catalog overlay (re-derived on refresh)
|
|
40
48
|
};
|
|
41
|
-
refresh(): Promise<void>; // re-runs loadRankData({ refresh: true }) and swaps rank
|
|
49
|
+
refresh(): Promise<void>; // re-runs loadRankData({ refresh: true, roles, extraMetrics }) and swaps rank
|
|
42
50
|
};
|
|
43
51
|
```
|
|
44
52
|
|
|
53
|
+
The server holds the active scope id as mutable state (alongside the mutable
|
|
54
|
+
`rank`/`availability`); `activeScope` resolves it per request, persisting the
|
|
55
|
+
fallback when the active scope became unavailable.
|
|
56
|
+
|
|
45
57
|
Endpoints:
|
|
46
58
|
|
|
47
59
|
| Method + path | Body | Response |
|
|
48
60
|
|---|---|---|
|
|
49
61
|
| `GET /` | — | `web/index.html` |
|
|
50
62
|
| `GET /app.js`, `/style.css` | — | static asset (traversal-guarded) |
|
|
51
|
-
| `GET /api/bootstrap` | — | `bootstrapPayload(opts)` |
|
|
63
|
+
| `GET /api/bootstrap` | — | `bootstrapPayload(opts, activeScope)` |
|
|
64
|
+
| `POST /api/scope` | `{scope}` | switch the active scope, then the bootstrap payload (404 unknown id, 409 non-present) |
|
|
52
65
|
| `POST /api/rank` | `{role, def}` | `{eligible, lambda, derivedLambda, rows, errors}` |
|
|
53
66
|
| `POST /api/explain` | `{role, def, modelId}` | `explainModel(...)` spread + `errors` |
|
|
54
|
-
| `POST /api/export` | `{roles}` | `{ok:true, backupPath, roles}` or `{ok:false, errors}` |
|
|
55
|
-
| `POST /api/refresh` | — | `await opts.refresh()` then the bootstrap payload |
|
|
67
|
+
| `POST /api/export` | `{roles}` | `{ok:true, backupPath, roles, lockPath}` or `{ok:false, errors}` |
|
|
68
|
+
| `POST /api/refresh` | — | `await opts.refresh()` (re-ranks and re-derives the availability overlay) then the bootstrap payload |
|
|
56
69
|
|
|
57
70
|
- `bootstrapPayload` ships `roles` (resolved), `defaults` (`DEFAULT_ROLES`),
|
|
58
71
|
`universe` (kind/enabled/locked + effective def per known role), `metrics`
|
|
59
72
|
(`weightableMetrics`: the shipped keys plus any external metric a resolved role
|
|
60
73
|
weights), `metricMeta` (`metricMetaFor`: `METRIC_META` plus a derived entry per
|
|
61
|
-
external metric), `
|
|
74
|
+
external metric), `focusAssessments` (per role, the nine-axis assessment of its
|
|
75
|
+
focus metrics over the resolved dataset), `levels` (`Object.keys(SUFFIX_LEVELS)`), `thinkingFactors`
|
|
62
76
|
(per-level billed-blend multiplier from the engine's own `thinkingPriceFactor`,
|
|
63
77
|
so the UI readout cannot drift), `fetchedAt`, `modelCount`, `orMatched`,
|
|
64
|
-
`orPriced`, `lockPath
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
`orPriced`, `lockPath` (the **active scope's** lock file), `scopes` (id, label,
|
|
79
|
+
kind, present per known scope), `activeScope` (the active scope id),
|
|
80
|
+
the active scope's global capability flags and the feature registry (the
|
|
81
|
+
`FEATURES` rows: id, label, description, recommended bundle, buys), so the SPA
|
|
82
|
+
can expand a posted def the same way the server does, and
|
|
83
|
+
`availability` (the keyed-catalog summary:
|
|
84
|
+
`{active, reason, publicCount, keyedCount, blockedCount, fetchedAt}`). Every
|
|
85
|
+
request calls `getState(scope)`, which re-reads that scope's lock file — so a
|
|
86
|
+
page reload after Export shows the new roles, not the boot-time snapshot.
|
|
87
|
+
- `handleScope` validates the requested id against `listScopes()`: an unknown id
|
|
88
|
+
is a 404, a known-but-not-present one a 409; a valid switch sets the active
|
|
89
|
+
scope and returns the new payload.
|
|
67
90
|
- `handleRank` baselines against `state.roles[role] ?? state.universe[role]?.def ??
|
|
68
91
|
state.defaults[role]`: an enabled role against its resolved def, a disabled or
|
|
69
92
|
lock-file-only role against its effective def, so selecting it still shows
|
|
70
|
-
deltas rather than an empty baseline.
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
deltas rather than an empty baseline. It passes `state.availability` to
|
|
94
|
+
`rankRows`, so every row carries its `key` badge (usable/blocked/unknown) with
|
|
95
|
+
no extra call. **`handleRank`/`handleExplain` expand the posted def** with the
|
|
96
|
+
active scope's global flags (`expandFeatures(def, …)`) *before*
|
|
97
|
+
`rankRows`/`explainModel`: the handlers rank the client's def verbatim, while
|
|
98
|
+
the Δ baseline is the resolved def (`state.roles[role]`), so without the
|
|
99
|
+
expansion the preview would omit the preset the baseline includes and every
|
|
100
|
+
row's Δ would be a lie. The posted def is the **authored** def (flags + explicit
|
|
101
|
+
keys); the expansion is applied only for the ranking call, never persisted.
|
|
102
|
+
- `handleExport` calls `writeRoleSettings(activeScope.lockPath, body.roles)` and
|
|
103
|
+
returns its result verbatim plus the written `lockPath`.
|
|
73
104
|
- Unknown `/api/*` → 404; wrong method → 405; non-API non-GET → 405.
|
|
74
105
|
- `readBody` caps at `MAX_BODY = 1 MiB` (413); `readJson` → 400 on invalid JSON.
|
|
75
106
|
- `serveStatic` resolves against `webDir` and rejects traversal (`full` must be
|
|
@@ -89,48 +120,86 @@ Pure, no I/O; all ranking math is delegated to `src/engine.ts`.
|
|
|
89
120
|
declared?)`** merges the shipped table with a derived entry per external metric.
|
|
90
121
|
- **`weightableMetrics(extra?)`** — the shipped keys plus any external metric in
|
|
91
122
|
`extra`; exactly what the validator accepts.
|
|
92
|
-
- **`rankRows(def, models, baseline)`** — `rankRole(def, models)` +
|
|
123
|
+
- **`rankRows(def, models, baseline, availability?)`** — `rankRole(def, models)` +
|
|
93
124
|
`paretoFrontier`, annotating each row with `baselineRank`/`delta`
|
|
94
125
|
(`baselineRank − rank`; positive = moved up) against the effective role's
|
|
95
126
|
ranking. Rows carry `priceEff` (the thinking-adjusted price the ranking
|
|
96
127
|
penalized), `parts`, `missing` (weighted non-price metrics whose raw value is
|
|
97
|
-
null), and `
|
|
128
|
+
null), `frontier`, and `key` — the availability overlay verdict
|
|
129
|
+
(`usable`/`blocked`/`unknown`), orthogonal to `missing` and to the eligibility
|
|
130
|
+
gates. `availability` is optional and pure (no I/O); omitted or inactive ⇒
|
|
131
|
+
every row reads `unknown`.
|
|
98
132
|
- **`inverseCardinal(metric, t)`** — inverse of `cardinalMetric` for the "what
|
|
99
133
|
would it take" targets: `index` → `t*80 − 20`; `throughput` → `10 * 30**t`
|
|
100
134
|
(null outside `[0,1]`, because the forward transform clamps); everything else
|
|
101
135
|
(benchmark, percentile, price) → identity. See *Open defect* below for the
|
|
102
136
|
`gpqa` exception.
|
|
103
|
-
- **`explainModel(def, models, modelId, roleName)`** — the full
|
|
104
|
-
the eligibility gates it failed (same gates `rankRole`
|
|
105
|
-
missing required, image filter, no billed price). On
|
|
137
|
+
- **`explainModel(def, models, modelId, roleName, availability?)`** — the full
|
|
138
|
+
decomposition, or the eligibility gates it failed (same gates `rankRole`
|
|
139
|
+
applies, same order: missing required, image filter, no billed price). On
|
|
140
|
+
success: `contributions`
|
|
106
141
|
(raw → cardinal `t` → renormalized weight → contribution → share of `q`, with
|
|
107
|
-
a `fillNote` when the value is a capability fill), `cost` (`priceEff`,
|
|
108
|
-
price, λ, penalty, `q`, `value`), `
|
|
142
|
+
a `fillNote` when the value is a capability fill), `cost` (`priceEff`, the
|
|
143
|
+
cache-adjusted billed price, λ, penalty, `q`, `value`), `role.cacheHitRate`,
|
|
144
|
+
`gapAbove`/`gapToTop`/`above`, `closing` (the
|
|
109
145
|
per-metric raw target that would close the gap, via `inverseCardinal`, with
|
|
110
|
-
unreachable/extrapolated notes),
|
|
111
|
-
least as good on `q`, top 3)
|
|
112
|
-
|
|
146
|
+
unreachable/extrapolated notes), `dominators` (models both cheaper and at
|
|
147
|
+
least as good on `q`, top 3), and the availability overlay `key`/`keyReason`
|
|
148
|
+
(the same verdict as `rankRows`, plus the reason it reads that way). It mirrors
|
|
149
|
+
`rankRole`'s capability fill so the contributions sum exactly to `q`.
|
|
113
150
|
|
|
114
151
|
## Shared boot path (`src/explorer/boot.ts`)
|
|
115
152
|
|
|
116
153
|
`startExplorer(opts: ExplorerBootOpts): Promise<ExplorerHandle>` — the one
|
|
117
154
|
launcher `/explore-roles` uses.
|
|
118
155
|
|
|
119
|
-
- `EXPLORER_DEFAULT_PORT = 5177`; `listen` binds
|
|
156
|
+
- `EXPLORER_DEFAULT_PORT = 5177`; `listen` binds the loopback address and resolves the
|
|
120
157
|
actual port. A permanent `error` listener is attached before `listen`, so a
|
|
121
158
|
post-listen socket error is logged instead of becoming an uncaughtException
|
|
122
159
|
(which would tear down the whole omp session). `EADDRINUSE` → retry on port 0
|
|
123
160
|
(OS-assigned).
|
|
124
|
-
-
|
|
125
|
-
|
|
126
|
-
project-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
161
|
+
- **Scopes** (`src/explorer/scopes.ts`). A scope is a role-config source: the
|
|
162
|
+
user-level scope (`lockPath`, default `PLUGIN_SETTINGS_PATH`), or one project
|
|
163
|
+
where `/project-roles` was used. `resolveScopes({ userLockPath, cwd, registryDir })`
|
|
164
|
+
returns the user-level scope, the registry's projects (most recently used
|
|
165
|
+
first), and the session's project when `cwd` has a project role config (a
|
|
166
|
+
project lock with a non-empty `omp-llm-role` settings entry — the updater's own
|
|
167
|
+
`resolveProjectDir` test) — included immediately, before the registry records
|
|
168
|
+
it. A scope id is `user` or `project:<root>`; `present` is whether the lock
|
|
169
|
+
file exists (the user scope is always available — Export creates it). The
|
|
170
|
+
default active scope is the session's project when present, else user-level.
|
|
171
|
+
`listScopes()` re-resolves on **every** request, so a project registered by
|
|
172
|
+
another session appears without a restart.
|
|
173
|
+
- **Per-scope read.** `readScopeRoles(scope, userLockPath)` reads the user-level
|
|
174
|
+
lock file for the user scope, and the project lock merged **over** the
|
|
175
|
+
user-level lock for a project scope — the exact read the updater performs, so
|
|
176
|
+
the explorer and the plugin agree. `resolveSettings` → `settings.roles` (the
|
|
177
|
+
resolved set = what the plugin does today); `roleUniverse(raw, roles)` adds the
|
|
178
|
+
roles it knows but does not rank (shipped opt-ins, lock-file-only roles).
|
|
179
|
+
`getState(scope)` re-runs this read on **every** request, so a page reload
|
|
130
180
|
after Export reflects the write instead of the state at boot.
|
|
181
|
+
- **Dataset union.** The host loads the ranking data through `unionRoles(scopes,
|
|
182
|
+
userLockPath)` — the union of every known scope's resolved roles — plus the
|
|
183
|
+
shipped keys (`loadRankData({ roles, extraMetrics: Object.keys(KNOWN_METRICS) })`),
|
|
184
|
+
so the dataset carries every metric any scope weights — including a generic
|
|
185
|
+
llm-stats benchmark (`bench:<id>`) that only a project role pulls in. Loading
|
|
186
|
+
with no roles (`loadRankData({})`) fetches declared sources only, so a role
|
|
187
|
+
weighting an undeclared benchmark would rank on a dataset missing that metric
|
|
188
|
+
and the explorer would disagree with the updater. `extraMetrics` also forces
|
|
189
|
+
the `website`/`writing` fetches even when no role weights them. The union is
|
|
190
|
+
what keeps a scope switch free (one fetch) and is asserted as a pure call over
|
|
191
|
+
temp lock files, not through the injected `rank` fixture.
|
|
131
192
|
- `enrichThinkingLevels(rank.models, opts.catalog)` gates the thinking price
|
|
132
|
-
factor on the omp catalog; `refresh()` re-runs `opts.reload(true)
|
|
133
|
-
re-enriches.
|
|
193
|
+
factor on the omp catalog; `refresh()` re-runs `opts.reload(true)`,
|
|
194
|
+
re-enriches, and — when `opts.reloadAvailability` is supplied — re-derives the
|
|
195
|
+
availability overlay.
|
|
196
|
+
- `opts.availability` is the boot-time keyed-catalog overlay; `getState(scope)`
|
|
197
|
+
returns it on every request, so `/api/rank` and `/api/explain` annotate rows
|
|
198
|
+
without a second fetch. It is scope-independent and carried across switches
|
|
199
|
+
unchanged. `/explore-roles` builds it in-process from the session's OpenRouter
|
|
200
|
+
key (`ctx.modelRegistry.getApiKeyForProvider("openrouter")`) via
|
|
201
|
+
`fetchKeyAvailability`; a missing key or a failed fetch is `unavailable`
|
|
202
|
+
(best-effort — the explorer still boots).
|
|
134
203
|
- `unref` (extension only) detaches the server from the event loop so a
|
|
135
204
|
short-lived `omp -p` run cannot hang.
|
|
136
205
|
- `open` launches the default browser on macOS (`execFile("open", [url])`).
|
|
@@ -146,7 +215,8 @@ The one validated role write path, shared by the explorer's Export,
|
|
|
146
215
|
- `mergeExport(existing, dirty)` → `{lock}` or `{error}`. Preserves `plugins` and
|
|
147
216
|
every sibling settings key. Writes each dirty role as **flat dotted keys**
|
|
148
217
|
(`roles.<name>.weights.<metric>`, `.description`, `.enabled`, `.locked`,
|
|
149
|
-
`.required`, `.thinking`, `.
|
|
218
|
+
`.required`, `.thinking`, `.providerPin`, `.cacheHitRate`, `.lambda`,
|
|
219
|
+
`.filters.image`) because omp's
|
|
150
220
|
`/settings` Plugins tab shallow-merges the settings object and does not
|
|
151
221
|
flatten nested objects — a nested `roles` object would render as schema
|
|
152
222
|
defaults. A pre-existing nested entry for a dirty role is deleted, so an older
|
|
@@ -166,10 +236,40 @@ server-side; the client only renders and edits role defs. Model names come from
|
|
|
166
236
|
a scraped third-party site, so the DOM is built with
|
|
167
237
|
`textContent`/`createElement` only (the `el()` helper).
|
|
168
238
|
|
|
169
|
-
- **Boot**: `boot()` → `GET /api/bootstrap` →
|
|
170
|
-
`
|
|
171
|
-
`
|
|
172
|
-
|
|
239
|
+
- **Boot**: `boot()` → `GET /api/bootstrap` → `applyBootstrap(data)`, which fills
|
|
240
|
+
`state` (`scopes`, `activeScope`, `universe`, `effective` = each entry's `def`,
|
|
241
|
+
`defs` = a structuredClone of it, `defaults`, `metrics`, `metricMeta`,
|
|
242
|
+
`focusAssessments`, `levels`, `thinkingFactors`, `lockPath`, `availability`),
|
|
243
|
+
renders the header/roles/table, and selects the previously selected role when
|
|
244
|
+
the new scope has it, else the first role. `boot()` then wires the
|
|
245
|
+
filter/topn/hide-key-blocked/export/copy/download controls.
|
|
246
|
+
- **Scope switcher**: the header renders a `#scope` `<select>` from
|
|
247
|
+
`state.scopes` (the user-level scope plus every known project); a scope whose
|
|
248
|
+
lock file is gone is `disabled` with a reason `title`. Changing the selection
|
|
249
|
+
confirms first when there are unsaved edits (mirroring Refresh), then
|
|
250
|
+
`POST /api/scope {scope}` and `applyBootstrap` on the returned payload — so
|
|
251
|
+
every panel (universe, per-scope effective/edited defs, role tabs, editor,
|
|
252
|
+
table, meta, lock path) reflects the new scope, and dirty tracking is per
|
|
253
|
+
scope. The header names the active scope and its lock file path.
|
|
254
|
+
- **Focus table**: the editor renders a `table.focus` for the role's weighted
|
|
255
|
+
benchmark/percentile metrics (`focusMetricsOf`), one row per metric with the
|
|
256
|
+
nine signals from `focusAssessments[role]` (coverage, dispersion, composition,
|
|
257
|
+
freshness, trust, modality, maintenance, provenance, cross-source). A below-bar cell is warned (tint + glyph), `unknown` is muted —
|
|
258
|
+
never green. A metric the editor just added reads "not assessed" until the edit
|
|
259
|
+
is **Exported and the page reloaded** (the payload is computed over the resolved
|
|
260
|
+
definition, and the editor's in-memory edit is discarded by a plain reload).
|
|
261
|
+
- **Features panel**: the editor renders one **tri-state** control per capability
|
|
262
|
+
(inherit / on / off) from the bootstrap payload's feature registry, showing the
|
|
263
|
+
recommended bundle and the effective values. A value the preset filled (rather
|
|
264
|
+
than an explicit key) is marked as **derived**, so authored and derived are
|
|
265
|
+
distinguishable. Toggling a control edits the role's `features.<id>` (the
|
|
266
|
+
tri-state's *inherit* leaves the key absent, so the global flag applies) and
|
|
267
|
+
re-ranks; the individual
|
|
268
|
+
per-knob fields the preset fills stay editable, and an explicit value wins over
|
|
269
|
+
the flag. **Export writes the flag**, never the expanded knobs — `dirtyRoles()`
|
|
270
|
+
is unchanged (it already compares the authored def against the authored
|
|
271
|
+
baseline), so a role whose only change is a flag exports just
|
|
272
|
+
`roles.<name>.features.<id>`.
|
|
173
273
|
- **Recompute**: `selectRole` → `renderRoles`/`renderEditor`/`renderExplain` +
|
|
174
274
|
`recompute()`. `recompute()` → `POST /api/rank {role, def}` → `state.rows`,
|
|
175
275
|
`lambda`, `derivedLambda`, `errors`; re-renders the table and readouts, and
|
|
@@ -177,10 +277,20 @@ a scraped third-party site, so the DOM is built with
|
|
|
177
277
|
changes call `scheduleRecompute()` (120 ms debounce).
|
|
178
278
|
- **Explain**: `selectModel(id)` → `POST /api/explain {role, def, modelId}` →
|
|
179
279
|
`renderExplain()` (composition table, cost line, "why not higher",
|
|
180
|
-
dominators).
|
|
280
|
+
dominators, and the availability line).
|
|
281
|
+
- **Availability overlay**: each row's `key` renders as a three-state badge in
|
|
282
|
+
the `key` column; the **hide key-blocked** checkbox beside the filter/top-n
|
|
283
|
+
controls drops `blocked` rows from the table (a client-side filter — the
|
|
284
|
+
ranking and Export are untouched); the header shows the allowlist size
|
|
285
|
+
(`keyedCount`/`publicCount`). The explain panel repeats the verdict and its
|
|
286
|
+
reason. When `availability.reason !== "active"` every badge reads `unknown`
|
|
287
|
+
and the tooltip/hint names OpenRouter → Settings → **"Filter the model catalog
|
|
288
|
+
for API keys"** — the explorer never guesses.
|
|
181
289
|
- **Dirty tracking**: `dirtyRoles()` compares `JSON.stringify(defs[role])` to
|
|
182
290
|
`effective[role]`; only dirty roles are sent to `POST /api/export`. On success
|
|
183
|
-
the client updates `effective` for the written roles
|
|
291
|
+
the client updates `effective` for the written roles and the status names the
|
|
292
|
+
written lock file. The state is rebuilt from the payload on a scope switch, so
|
|
293
|
+
dirty tracking is per scope.
|
|
184
294
|
- **Deep-merge parking**: `dropWeight(metric)` parks an inherited metric (one
|
|
185
295
|
the shipped default weights) at `EPSILON = 0.001` instead of deleting the key
|
|
186
296
|
— the plugin deep-merges over `DEFAULT_ROLES`, so the default's weight would
|
|
@@ -191,8 +301,9 @@ a scraped third-party site, so the DOM is built with
|
|
|
191
301
|
re-rendered tables and editors need no per-node wiring. Metric and role
|
|
192
302
|
tooltips are built from the bootstrap payload (`METRIC_META` / role
|
|
193
303
|
descriptions), not duplicated in the client.
|
|
194
|
-
- **Refresh**: `onRefresh()` confirms, then `POST /api/refresh` →
|
|
195
|
-
|
|
304
|
+
- **Refresh**: `onRefresh()` confirms, then `POST /api/refresh` →
|
|
305
|
+
`applyBootstrap` on the returned payload (the scope list is re-resolved, so a
|
|
306
|
+
project registered by another session appears).
|
|
196
307
|
|
|
197
308
|
## Open defect
|
|
198
309
|
|
package/docs/dev/quirks.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Quirks
|
|
2
2
|
|
|
3
3
|
Debugging-only oddities and gotchas that are **not** contracts. If a fact is a
|
|
4
|
-
contract the implementation MUST satisfy, it belongs in
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
contract the implementation MUST satisfy, it belongs in the topic doc that owns
|
|
5
|
+
it ([`architecture.md`](architecture.md), [`scoring.md`](scoring.md),
|
|
6
|
+
[`writes-and-state.md`](writes-and-state.md), …); if it is user-facing, it
|
|
7
|
+
belongs in [`../../README.md`](../../README.md). This file is the home for the
|
|
8
|
+
things that only bite someone changing or debugging the code.
|
|
7
9
|
|
|
8
10
|
## Data sources and joins
|
|
9
11
|
|
|
@@ -27,6 +29,28 @@ the home for the things that only bite someone changing or debugging the code.
|
|
|
27
29
|
- **`index_*` scores are interval-scale** (observed −16..+60, can be negative);
|
|
28
30
|
the fixed affine anchors (−20→0, +60→1) handle it.
|
|
29
31
|
|
|
32
|
+
## Availability
|
|
33
|
+
|
|
34
|
+
- **The probe's disqualifying 404 is narrow.** `probeModel` blocks a model only
|
|
35
|
+
on the exact "No allowed providers are available for the selected model …" 404;
|
|
36
|
+
every other failure (5xx, timeout, unknown model) counts as usable and stays in
|
|
37
|
+
omp's runtime-fallback domain. The account's allowed-providers whitelist is
|
|
38
|
+
enforced per serving endpoint, so the **org prefix is not a valid filter**:
|
|
39
|
+
aggregator-org models (`deepseek/*`, `z-ai/*`, `inclusionai/*`) run via
|
|
40
|
+
whitelisted third-party endpoints while `openai/*` and `anthropic/*` fail on
|
|
41
|
+
first-party-only routing.
|
|
42
|
+
|
|
43
|
+
## Capability fill
|
|
44
|
+
|
|
45
|
+
- **The fill can invert against the covered field.** The capability fill
|
|
46
|
+
(`CAPABILITY_FILL`, 0.195) is the percentile implied by the *uncovered*
|
|
47
|
+
cohort's mean general index, which sits above the bottom of the covered
|
|
48
|
+
field — so a covered model in the bottom ~8% of the field scores *below* a
|
|
49
|
+
model with no Design Arena data at all. Measured 2026-10-03: 4 of the 51
|
|
50
|
+
covered in-pool models (24 of 121 across all models). This is a
|
|
51
|
+
debugging-only oddity, not a contract; the fill is deliberately conservative
|
|
52
|
+
and a regression fill was rejected (see the `applyDesignPercentiles` comment).
|
|
53
|
+
|
|
30
54
|
## The 1/price² blend
|
|
31
55
|
|
|
32
56
|
- **The blend renormalizes throughput over the routes that have p50 data**,
|
|
@@ -87,7 +111,24 @@ the home for the things that only bite someone changing or debugging the code.
|
|
|
87
111
|
- **`api.zeroeval.com/leaderboard/benchmarks/<id>` is case- and
|
|
88
112
|
separator-sensitive**: `alpacaeval-2.0` returns 200, `alpacaeval-2_0` and
|
|
89
113
|
`ALPACAEVAL-2.0` return 404. The declaration's `fetch.url` must carry the raw
|
|
90
|
-
id exactly as the site spells it.
|
|
114
|
+
id exactly as the site spells it. The generic path normalizes the id to the
|
|
115
|
+
metric key (`deepswe-1.1` → `bench:deepswe-1_1`), so the updater's metric-key
|
|
116
|
+
fallback would fetch the normalized (404-ing) URL; the declaration
|
|
117
|
+
`catalogBenchmarkDeclaration` builds at discovery/authoring time is what
|
|
118
|
+
carries the raw id, and without it the metric is dead weight.
|
|
119
|
+
- **A capability-filled generic benchmark bypasses the coverage gate.** The
|
|
120
|
+
generic llm-stats source (`genericBenchmarkSource` / `llmStatsBenchmarkDeclaration`)
|
|
121
|
+
declares `fill: 0.195`, and `focusCoverageOk` returns `ok` for any metric whose
|
|
122
|
+
`metricFill > 0` — so a `bench:<id>` metric with 1 covered model reports
|
|
123
|
+
`coverage.status = "ok"` and is rejected (if at all) on dispersion,
|
|
124
|
+
composition or freshness, never on coverage. Measured 2026-10-06: `bench:supergpqa`
|
|
125
|
+
covers 20/400 (5.0%) and still reports `status=ok`. The coverage floor only
|
|
126
|
+
binds fill-0 metrics. Do not read `coverage.status = "ok"` as "well covered".
|
|
127
|
+
- **The per-benchmark endpoint caps `entries` at 20** (`BENCHMARK_ENTRY_CAP`),
|
|
128
|
+
regardless of `limit`/`offset`/`page`/`per_page`, so a generic `bench:<id>`
|
|
129
|
+
metric can never load more and no pagination loop helps. `total_models` still
|
|
130
|
+
reports the full set, so `loaded < total` marks a capped load and the coverage
|
|
131
|
+
count is the loadable count, never the catalog's `model_count`.
|
|
91
132
|
|
|
92
133
|
## Runtime
|
|
93
134
|
|
package/docs/dev/releasing.md
CHANGED
|
@@ -2,8 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
`version` in `package.json` is the release switch — bump it, commit, then
|
|
4
4
|
publish through any channel (same tree, no build step). The repo is public
|
|
5
|
-
(flipped 2026-10-02)
|
|
6
|
-
channel
|
|
5
|
+
(flipped 2026-10-02) and 1.0.0 is published to npm (2026-10-03), so all three
|
|
6
|
+
channel URLs below resolve — see Status.
|
|
7
|
+
|
|
8
|
+
## When to bump, and to what
|
|
9
|
+
|
|
10
|
+
The version is a **release switch, not a per-commit counter**: bump it once per
|
|
11
|
+
published batch, at release cut. The trigger is the scope of `CHANGELOG.md`'s
|
|
12
|
+
`## [Unreleased]` section — what actually landed. An empty `## [Unreleased]`
|
|
13
|
+
owes no release; a non-empty one owes exactly one bump. Unimplemented open
|
|
14
|
+
issues (the GitHub tracker) never decide the number.
|
|
15
|
+
|
|
16
|
+
Pick the number with SemVer over the plugin's **public surface**: the plugin
|
|
17
|
+
settings (`roles.*`, `filters.*`, …), the commands (`/refresh-roles`,
|
|
18
|
+
`/explore-roles`, `/create-agent`, `/remove-agent`), the `config.yml` write
|
|
19
|
+
contract (`modelRoles`, `retry.fallbackChains`, `task.disabledAgents`), the CLI
|
|
20
|
+
flags, the shipped roles/agents, and the three install channels.
|
|
21
|
+
|
|
22
|
+
- **PATCH** (`1.0.x`) — a fix with no public-interface and no shipped-role
|
|
23
|
+
behavior change (crash fix, cache parse fix, doc-only correction).
|
|
24
|
+
- **MINOR** (`1.x.0`) — any additive, backward-compatible change: a new optional
|
|
25
|
+
setting, metric, role, agent, command or flag. **Ranking-output changes are
|
|
26
|
+
minor too** (weights, price basis, sources): a ranking is data-dependent
|
|
27
|
+
output, not a versioned interface, so a reweight or re-basis is minor even
|
|
28
|
+
though every pick may move.
|
|
29
|
+
- **MAJOR** (`x.0.0`) — anything that invalidates an existing config or breaks
|
|
30
|
+
an existing caller: removing or renaming a public setting; changing an
|
|
31
|
+
existing setting's meaning so a previously valid config behaves differently;
|
|
32
|
+
changing the `config.yml` patch shape, selector or chain format
|
|
33
|
+
incompatibly; dropping an install channel or renaming the package; requiring
|
|
34
|
+
a manual migration.
|
|
35
|
+
- **Corner case.** Making a declared-but-inert setting start enforcing its
|
|
36
|
+
documented capability is a MINOR fix, not a MAJOR break — provided the key is
|
|
37
|
+
not renamed and no shipped role sets it.
|
|
38
|
+
|
|
39
|
+
## How to cut a release
|
|
40
|
+
|
|
41
|
+
The cut is one command, `scripts/release.ts` — its subcommands, the lockstep
|
|
42
|
+
set, the refusals and the tag→publish contract are in [`ci.md`](ci.md). The
|
|
43
|
+
policy above decides the number; the script applies it:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
node scripts/release.ts due # is a release owed? (exit 1 = no)
|
|
47
|
+
node scripts/release.ts cut minor --dry-run # review the target version + changelog diff
|
|
48
|
+
node scripts/release.ts cut minor # bump, cut, commit, push, tag
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`cut` bumps the four lockstep version fields, renames `## [Unreleased]` and
|
|
52
|
+
inserts a fresh one, rewrites the footer link refs, regenerates the committed
|
|
53
|
+
report, commits, pushes `main` and pushes the tag. The tag push publishes to npm
|
|
54
|
+
over OIDC and creates the GitHub Release — there is no local `npm publish` step
|
|
55
|
+
and no token. Verify the channels per "Install-route verification".
|
|
7
56
|
|
|
8
57
|
## Channels
|
|
9
58
|
|
|
@@ -35,7 +84,27 @@ src, web, agents, skills,
|
|
|
35
84
|
docs/dev, docs/llm-role-rankings.md
|
|
36
85
|
```
|
|
37
86
|
|
|
38
|
-
Caches and
|
|
87
|
+
Caches, tests and `.githooks` stay out. Gate: `node --test tests/` (the
|
|
88
|
+
pre-commit hook and CI both run it — see [`ci.md`](ci.md)).
|
|
89
|
+
|
|
90
|
+
## Changelog rules
|
|
91
|
+
|
|
92
|
+
`CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
93
|
+
and is the source of truth for the release scope.
|
|
94
|
+
|
|
95
|
+
- Every **user-visible** change gets a line under `## [Unreleased]`, in the
|
|
96
|
+
section that fits (`Added` / `Changed` / `Fixed` / `Removed`; add
|
|
97
|
+
`Deprecated` / `Security` only when needed).
|
|
98
|
+
- **User-visible** = a user or maintainer would notice: ranking or config
|
|
99
|
+
output, the settings surface, shipped roles/agents, commands, install routes,
|
|
100
|
+
a contract or prompt version marker, or a doc fact that changed with the
|
|
101
|
+
behavior. Internal-only work (refactors, tests, formatting, a
|
|
102
|
+
`docs/dev`-only policy or research note) gets no entry.
|
|
103
|
+
- One entry per change: bold lead phrase, then what changed and why; end with
|
|
104
|
+
`(issue #N)` / `(PR #N)` when one exists.
|
|
105
|
+
- Land the entry in the same commit as the change (see `AGENTS.md`).
|
|
106
|
+
- Never write a `## [<version>]` heading outside a release cut; never reword or
|
|
107
|
+
move entries when cutting — rename the heading and keep them verbatim.
|
|
39
108
|
|
|
40
109
|
## Install-route verification
|
|
41
110
|
|
|
@@ -47,17 +116,60 @@ path, plus `marketplace update` + `upgrade` to a bumped catalog version), and
|
|
|
47
116
|
|
|
48
117
|
## Status
|
|
49
118
|
|
|
50
|
-
- **
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
119
|
+
- **CI/CD landed 2026-10-08** (`fa7e546`): every pull request and every `main`
|
|
120
|
+
push runs the suite on GitHub (verified green on PR #57 and on the `main`
|
|
121
|
+
push), and a `v*` tag push publishes to npm over OIDC and creates the GitHub
|
|
122
|
+
Release. The guard was verified with a negative control — tag `v0.0.1`
|
|
123
|
+
against version `1.1.0` failed in `notes` before any publish. The first real
|
|
124
|
+
CD run still needs the one-time trusted-publisher registration and a route
|
|
125
|
+
for the legacy `v1.1.0` tag; see [`ci.md`](ci.md).
|
|
126
|
+
- **1.1.0 cut 2026-10-08** (`c013918`, tagged `v1.1.0`). `package.json` and
|
|
127
|
+
`.omp-plugin/marketplace.json` bumped in lockstep (plus `package-lock.json`
|
|
128
|
+
via `npm install --package-lock-only` — the repo keeps it in lockstep, see
|
|
129
|
+
`8811309`), `## [Unreleased]` renamed to `## [1.1.0] - 2026-10-08`, and the
|
|
130
|
+
report regenerated (407 models, matched 155/407, priced 153;
|
|
131
|
+
`docs/dev/current-state.md` refreshed to the same measurement).
|
|
132
|
+
- **git + marketplace channels live and verified** (2026-10-08, omp 18.4.12,
|
|
133
|
+
throwaway `HOME`s, cwd outside the real home tree):
|
|
134
|
+
`omp plugin install github:Gott50/omp-llm-role` installed 1.1.0
|
|
135
|
+
(`omp plugin doctor`: 4 ok, 0 warnings, 0 errors), and
|
|
136
|
+
`omp plugin marketplace add Gott50/omp-llm-role` +
|
|
137
|
+
`omp plugin install omp-llm-role@gott50-plugins` installed 1.1.0 into
|
|
138
|
+
`…/cache/plugins/gott50-plugins___omp-llm-role___1.1.0`. Both routes then
|
|
139
|
+
passed the post-install session smoke (`omp -p "say ok"` with
|
|
140
|
+
`OPENROUTER_API_KEY` from `omp token openrouter`): the installed 1.1.0 wrote
|
|
141
|
+
`modelRoles` (9 roles), `retry.fallbackChains` and
|
|
142
|
+
`task.disabledAgents: [designer]` into that HOME's `config.yml`, landed
|
|
143
|
+
`llm-role-state.json` + `llm-role-history.jsonl`, and the session answered.
|
|
144
|
+
The npm route has **not** been smoke-tested for 1.1.0 — it is still serving
|
|
145
|
+
1.0.0.
|
|
146
|
+
- **npm publish pending a valid token.** The `~/.npmrc` token is rejected
|
|
147
|
+
(`npm whoami` → 401 "your authentication token seems to be invalid",
|
|
148
|
+
`npm publish` → 404 on `PUT /omp-llm-role`), so `omp-llm-role@1.1.0` is not
|
|
149
|
+
on the registry yet, `dist-tags.latest` still points at `1.0.0`, and **npm's
|
|
150
|
+
`gitHead` for 1.1.0 does not exist yet** — the tag currently stands on its
|
|
151
|
+
own. The token is the only credential source (no repo `.npmrc`, no
|
|
152
|
+
`/opt/homebrew/etc/npmrc`, no `NPM_TOKEN` in the environment or the shell
|
|
153
|
+
profiles, no keychain entry) and it is a literal value, not a `${NPM_TOKEN}`
|
|
154
|
+
indirection, so the cause is a rejected/revoked/expired token — **not** the
|
|
155
|
+
2026-07-31 GAT restriction, which covers account/org/package-management
|
|
156
|
+
actions and not `whoami`, and not direct publish either (that loss is
|
|
157
|
+
targeted January 2027). `npm ping` and `npm owner ls` still succeed because
|
|
158
|
+
they are unauthenticated reads. Fix: the release workflow — OIDC trusted
|
|
159
|
+
publishing, no token — once the trusted publisher is registered (see
|
|
160
|
+
[`ci.md`](ci.md)). The tag was cut before the publish (the documented order is
|
|
161
|
+
publish-then-tag) precisely so the pinned git ref and the changelog link
|
|
162
|
+
resolve meanwhile; that also means the tag's commit predates the workflow
|
|
163
|
+
file, so the workflow cannot be dispatched against it.
|
|
164
|
+
- **1.0.0 on all three channels** (2026-10-03): `omp-llm-role@1.0.0` on npm
|
|
165
|
+
(`dist-tags.latest` = `1.0.0`), tagged `v1.0.0` at `fe98b76` (npm's
|
|
166
|
+
`gitHead`); all three routes verified end-to-end against the real URLs
|
|
167
|
+
(omp 18.4.12, throwaway `HOME`s) — each post-install session run wrote
|
|
168
|
+
`modelRoles` + `retry.fallbackChains` into that HOME's `config.yml` and
|
|
169
|
+
landed the three daily caches in the installed copy.
|
|
170
|
+
- **Stray registry placeholder.** The registry also carries a `0.0.0-stage`
|
|
171
|
+
version ("Temporary package placeholder for staged publishing", 334 B,
|
|
172
|
+
published 2026-10-03). It is not from this tree; `latest` still points at
|
|
173
|
+
`1.0.0`, so it does not affect installs.
|
|
174
|
+
- **Next release: one owed.** `## [Unreleased]` carries the CI/CD entry; the
|
|
175
|
+
number comes from the Unreleased scope, not from the open issues.
|