@pfoundation/ocadvisor 26.9.0 → 26.9.2

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.
Files changed (52) hide show
  1. package/README.md +420 -43
  2. package/dist/artificialAnalysis.d.ts +4 -0
  3. package/dist/artificialAnalysis.d.ts.map +1 -0
  4. package/dist/artificialAnalysis.js +77 -0
  5. package/dist/artificialAnalysis.js.map +1 -0
  6. package/dist/benchmarkConfig.d.ts +19 -0
  7. package/dist/benchmarkConfig.d.ts.map +1 -0
  8. package/dist/benchmarkConfig.js +58 -0
  9. package/dist/benchmarkConfig.js.map +1 -0
  10. package/dist/benchmarkEvidence.d.ts +37 -0
  11. package/dist/benchmarkEvidence.d.ts.map +1 -0
  12. package/dist/benchmarkEvidence.js +81 -0
  13. package/dist/benchmarkEvidence.js.map +1 -0
  14. package/dist/benchmarkMatch.d.ts +57 -0
  15. package/dist/benchmarkMatch.d.ts.map +1 -0
  16. package/dist/benchmarkMatch.js +236 -0
  17. package/dist/benchmarkMatch.js.map +1 -0
  18. package/dist/benchmarkStore.d.ts +44 -0
  19. package/dist/benchmarkStore.d.ts.map +1 -0
  20. package/dist/benchmarkStore.js +308 -0
  21. package/dist/benchmarkStore.js.map +1 -0
  22. package/dist/benchmarkTypes.d.ts +48 -0
  23. package/dist/benchmarkTypes.d.ts.map +1 -0
  24. package/dist/benchmarkTypes.js +298 -0
  25. package/dist/benchmarkTypes.js.map +1 -0
  26. package/dist/benchmarkUpdate.d.ts +33 -0
  27. package/dist/benchmarkUpdate.d.ts.map +1 -0
  28. package/dist/benchmarkUpdate.js +327 -0
  29. package/dist/benchmarkUpdate.js.map +1 -0
  30. package/dist/cli.d.ts +13 -0
  31. package/dist/cli.d.ts.map +1 -0
  32. package/dist/cli.js +377 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/data/artificialAnalysis.mappings.json +197 -0
  35. package/dist/data/artificialAnalysis.snapshot.json +17684 -0
  36. package/dist/modelProfiles.d.ts +45 -0
  37. package/dist/modelProfiles.d.ts.map +1 -0
  38. package/dist/modelProfiles.js +141 -0
  39. package/dist/modelProfiles.js.map +1 -0
  40. package/dist/ocAdvisor.d.ts +152 -7
  41. package/dist/ocAdvisor.d.ts.map +1 -1
  42. package/dist/ocAdvisor.js +681 -54
  43. package/dist/ocAdvisor.js.map +1 -1
  44. package/dist/typesafeGate.d.ts +89 -0
  45. package/dist/typesafeGate.d.ts.map +1 -0
  46. package/dist/typesafeGate.js +463 -0
  47. package/dist/typesafeGate.js.map +1 -0
  48. package/dist/typesafeState.d.ts +94 -0
  49. package/dist/typesafeState.d.ts.map +1 -0
  50. package/dist/typesafeState.js +301 -0
  51. package/dist/typesafeState.js.map +1 -0
  52. package/package.json +9 -3
package/README.md CHANGED
@@ -40,17 +40,24 @@ and the usage report accept both `advisor` and the pre-rename `ocAdvisor`.
40
40
 
41
41
  ## Configuration
42
42
 
43
- The advisor model and limits are configurable. Defaults are unchanged:
44
- `anthropic/claude-fable-5-1#max`, a 300 s generation timeout, and no
45
- transcript cap.
43
+ The advisor model and limits are configurable. Defaults:
44
+ `anthropic/claude-fable-5-1#xhigh`, a 300 s generation timeout, no
45
+ transcript cap, TypeSafe screening on when a key is present, and
46
+ model-capability evidence from a local Artificial Analysis snapshot when
47
+ one is available.
46
48
 
47
49
  | Option | Default | Meaning |
48
50
  |---|---|---|
49
51
  | `model` | `claude-fable-5-1` | Model id, or a full `provider/model#variant` reference |
50
52
  | `provider` | `anthropic` | Provider id (overrides the provider in `model`) |
51
- | `variant` | `max` | Reasoning-effort variant; `null` or `"none"` pins no variant |
53
+ | `variant` | `xhigh` | Reasoning-effort variant; `null` or `"none"` pins no variant |
52
54
  | `timeoutMs` | `300000` | Per-consultation generation timeout, in milliseconds |
53
55
  | `maxTranscriptChars` | `0` | Cap on transcript size (`0` = unlimited); the most recent tail is kept |
56
+ | `agentEffort` | `false` | Let the agent pick effort per call: `true` allows `high`, `xhigh`, `max`; an array or comma string sets an explicit allow-list |
57
+ | `disabledForModels` | `[]` | Exact caller `provider/model` IDs that must not see or invoke advisor. Applies to every effort variant of that ID. A string array is the documented form; a comma-separated string is also accepted |
58
+ | `typesafe` | enabled with a key | `false` disables screening; `true` or an object enables it (see below) |
59
+ | `benchmarks.path` | data-directory snapshot | Absolute path to the local Artificial Analysis snapshot file |
60
+ | `benchmarks.mappingsPath` | beside the snapshot | Absolute path to the local model-mapping overrides file |
54
61
 
55
62
  Set them as plugin options in `opencode.json`. Because a plugin loaded from
56
63
  the auto-discovered `plugin/` directory cannot receive options, list it
@@ -67,48 +74,319 @@ explicitly in the `plugins` array:
67
74
  }
68
75
  ```
69
76
 
77
+ To let the calling agent choose the reasoning effort per consultation:
78
+
79
+ ```jsonc
80
+ {
81
+ "plugins": [
82
+ {
83
+ "package": "@pfoundation/ocadvisor",
84
+ "options": { "agentEffort": true }
85
+ }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ With `agentEffort` enabled the tool accepts an optional `effort` argument
91
+ (one of the allowed levels); when the agent omits it, the configured
92
+ `variant` is used. A requested effort that is not a variant of the advisor
93
+ model fails the call with an `invalid_effort` error instead of silently
94
+ falling back.
95
+
96
+ To hide advisor from selected *caller* models (for example Astra), list
97
+ their exact `provider/model` IDs. This is independent of `model` /
98
+ `provider`, which configure the advisor itself:
99
+
100
+ ```jsonc
101
+ {
102
+ "$schema": "https://opencode.ai/config.json",
103
+ "plugins": [
104
+ {
105
+ "package": "@pfoundation/ocadvisor",
106
+ "options": {
107
+ "disabledForModels": ["openai/gpt-6-astra"]
108
+ }
109
+ }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ Matching is exact and case-sensitive on `provider/model`. Effort variants
115
+ are ignored, so `openai/gpt-6-astra#xhigh` is still excluded by
116
+ `openai/gpt-6-astra`. Nearby names (`openai/gpt-6-astra-preview`) and other
117
+ providers do not match. Gateway IDs keep the serving provider: list
118
+ `openrouter/openai/gpt-6-astra` to exclude that route, not
119
+ `openai/gpt-6-astra`. Bare names, `#variant` suffixes, and wildcards are
120
+ rejected at config load.
121
+
122
+ Plugin options replace the environment list rather than merging it. An
123
+ explicit `[]` (or a blank string) clears configurable exclusions even when
124
+ `OCADVISOR_DISABLED_FOR_MODELS` is set:
125
+
126
+ ```sh
127
+ OCADVISOR_DISABLED_FOR_MODELS=openai/gpt-6-astra,anthropic/claude-opus-5
128
+ ```
129
+
130
+ After changing plugin options, restart the background service
131
+ (`opencode service restart`) so the server reloads configuration.
132
+
70
133
  Environment variables work for any install and take
71
134
  lower precedence than plugin options: `OCADVISOR_MODEL` (accepts
72
135
  `provider/model#variant`), `OCADVISOR_PROVIDER`, `OCADVISOR_VARIANT`,
73
- `OCADVISOR_TIMEOUT_MS`, `OCADVISOR_MAX_TRANSCRIPT_CHARS`.
136
+ `OCADVISOR_TIMEOUT_MS`, `OCADVISOR_MAX_TRANSCRIPT_CHARS`,
137
+ `OCADVISOR_AGENT_EFFORT` (`true`, `false`, or a comma-separated allow-list),
138
+ `OCADVISOR_DISABLED_FOR_MODELS` (comma-separated exact `provider/model` IDs),
139
+ `OCADVISOR_BENCHMARKS_PATH`, and `OCADVISOR_BENCHMARK_MAPPINGS_PATH`.
74
140
 
75
141
  The "already the advisor model" skip is still keyed to Fable
76
142
  (`anthropic/claude-fable-*`); if you point the advisor at a different model,
77
- that self-consultation guard no longer matches it.
143
+ that self-consultation guard no longer matches it. Fable sessions stay
144
+ blocked even when they also appear in `disabledForModels`.
78
145
 
79
146
  ## Usage policy (what agents are told)
80
147
 
81
- Use `advisor` selectively on substantial, non-trivial work. Straightforward
82
- tasks normally need no consultation.
148
+ Use `advisor` when an independent perspective could improve the approach, help
149
+ resolve a problem, or strengthen an implementation review. On substantial
150
+ work, consider consulting before committing to an approach, when progress
151
+ stalls, or before completing meaningful changes. Additional consultations are
152
+ welcome as the work evolves — particularly when new evidence appears, the
153
+ approach changes, or another concern needs review. Straightforward tasks
154
+ usually need no consultation.
83
155
 
84
- - Normally **at most one consultation per task**, at the point where a
85
- second opinion has the most value — pick one stage, not all three:
156
+ Typical checkpoints:
86
157
 
87
158
  | Situation | Mode / trigger |
88
159
  |---|---|
89
- | Consequential unresolved design decision | `plan` / `before_approach` |
90
- | Blocker after two substantially different attempts | `debug` / `stuck` |
91
- | High-risk change with a specific unresolved correctness concern | `review` / `pre_complete` |
160
+ | Deciding an approach with real tradeoffs | `plan` / `before_approach` |
161
+ | Stalled progress or contradictory evidence | `debug` / `stuck` |
162
+ | Reviewing meaningful changes before declaring done | `review` / `pre_complete` |
92
163
 
93
164
  Rules enforced by the tool description and an injected session instruction:
94
165
 
95
166
  - Always pass a concrete `question` naming the decision or artifact.
96
- - A second consultation requires material new evidence, a distinct
97
- unresolved issue, or an explicit user request (`followup` trigger to
167
+ - Avoid repeating settled questions without new context (`followup` trigger to
98
168
  reconcile conflicts with primary-source evidence).
99
169
  - Give the advice serious weight; a passing self-test alone is not
100
170
  counter-evidence. Clear factual corrections do not need another
101
171
  confirmation call.
102
172
  - The tool is hidden in `anthropic/claude-fable-*` sessions (the current
103
- model is already Fable); calls there return a disabled notice.
173
+ model is already Fable) and in sessions whose caller model is listed in
174
+ `disabledForModels`. Direct calls there return a disabled notice and do
175
+ not run TypeSafe screening or advisor generation. Eligibility is
176
+ reevaluated per request, so switching models mid-session takes effect
177
+ immediately; a child session uses its own model, not the parent's.
178
+ - When the `agentEffort` plugin option is enabled, an optional `effort`
179
+ argument selects the reasoning effort for that consultation.
180
+ - When TypeSafe screening is active, a clearly unnecessary consultation
181
+ returns a skip notice instead of advice, and an omitted effort may be
182
+ chosen automatically. When model-capability evidence is available, it is
183
+ weighed as context, not as a rule: a strong requester can still benefit
184
+ from independent review and a weak requester still needs no advice for
185
+ trivial work.
186
+
187
+ ## TypeSafe screening (optional)
188
+
189
+ When `TYPESAFE_API_KEY` is available to the OpenCode server process, the
190
+ plugin can screen each consultation before paying for advisor generation.
191
+ One [TypeSafe](https://docs.typesafe.ai) System One request asks whether an
192
+ independent advisor would materially help at this point and — when the caller
193
+ did not pin an effort — which allowed effort fits. Only a clearly low need
194
+ probability skips generation; uncertain judgments preserve the consultation,
195
+ and any gate failure (timeout, transport, malformed answer) falls back to the
196
+ ordinary advisor call.
197
+
198
+ Screening is enabled automatically when the key is present. Configure it with
199
+ the `typesafe` plugin option:
200
+
201
+ ```jsonc
202
+ {
203
+ "plugins": [
204
+ {
205
+ "package": "@pfoundation/ocadvisor",
206
+ "options": {
207
+ "typesafe": {
208
+ "model": "jev-1.13.0",
209
+ "timeoutMs": 3000,
210
+ "skipBelow": 0.2,
211
+ "minEffortConfidence": 0.6,
212
+ "maxStateBytes": 16384,
213
+ "efforts": ["high", "xhigh", "max"]
214
+ }
215
+ }
216
+ }
217
+ ]
218
+ }
219
+ ```
220
+
221
+ | Option | Default | Meaning |
222
+ |---|---|---|
223
+ | `typesafe` | `true` when the key is set | `false` disables the gate completely; an object enables it with overrides |
224
+ | `model` | SDK default (`TYPESAFE_DEFAULT_MODEL`, else `jev-latest`) | Gate model; pin a version for reproducible decisions |
225
+ | `timeoutMs` | `3000` | Total gate budget, no retries |
226
+ | `skipBelow` | `0.20` | Skip only below this need probability |
227
+ | `minEffortConfidence` | `0.60` | Below this, keep the configured effort |
228
+ | `maxStateBytes` | `16384` | Byte budget for the compact decision state |
229
+ | `efforts` | `["high", "xhigh", "max"]` | Allowed automatic effort levels, intersected with the model's live variants |
230
+
231
+ Behavior notes:
232
+
233
+ - The gate runs only when an agent invokes `advisor`; ordinary model turns
234
+ never trigger TypeSafe calls.
235
+ - A missing or blank key, or `typesafe: false`, means no client is created and
236
+ no request is sent.
237
+ - Effort precedence when generation proceeds: an explicit tool `effort`, then
238
+ a confident gate selection, then the configured `variant`.
239
+ - A skip returns a visible `advisor consultation skipped (typesafe)` notice
240
+ and does not create, switch, or generate on the advisor session. On a
241
+ proceed or fallback the tool output gains one short line, for example
242
+ `gate: need=0.31, effort=xhigh (gate), decision=proceed` or
243
+ `gate: decision=fallback, timeout`; bypassed screening adds nothing.
244
+ - Gate metrics (decision, need probability, selected effort, latency, tokens)
245
+ are recorded alongside the consultation in `ocAdvisor-metrics.jsonl`.
246
+
247
+ ## Model-capability benchmarks (Artificial Analysis)
248
+
249
+ The gate judges how much an independent advisor could help *this* requesting
250
+ model. To do that it needs two things: the actual model that made the call —
251
+ including its reasoning effort — and measured capability data for both that
252
+ model and the advisor. The plugin reads a local snapshot of
253
+ [Artificial Analysis](https://artificialanalysis.ai/) results; ordinary
254
+ consultations never touch the network for benchmark data.
255
+
256
+ What the gate receives, in the same single TypeSafe request:
257
+
258
+ - `models.requester` — `provider`, `model`, effort variant, and where the
259
+ identity came from (`invocation_message`, `latest_message`,
260
+ `session_fallback`, or `unknown`). The requester is the model behind the
261
+ outgoing tool call, not the parent session or whatever the session row
262
+ points at after a later switch.
263
+ - `models.advisor` — `provider`, `model`, and the effort policy in force:
264
+ a pinned caller effort, gate-selectable candidates with their fallback, or
265
+ a fixed default.
266
+ - `benchmarks` — the snapshot source and age, one match per profile, and
267
+ comparisons for the shared metrics: Artificial Analysis Coding Index,
268
+ Intelligence Index, and up to two reasoning/math results (HLE, GPQA, Math
269
+ Index) when present. Code computes oriented differences
270
+ (`advisor_minus_requester`); the model never does arithmetic. Scores that
271
+ cannot be strictly compared carry an explicit `reason` (`effort_mismatch`,
272
+ `effort_unknown`, `missing_requester`, …) and no delta.
273
+
274
+ Policy rule baked into the gate instructions: scores estimate comparative
275
+ capability, not certainty about the task. A strong requester can still benefit
276
+ from independent review or a fresh perspective when stuck, and a weaker
277
+ requester still needs no advice for trivial work. Unknown, mismatched, or
278
+ stale data is uncertainty — never evidence against consultation. No
279
+ automatic skip is ever derived from scores alone.
280
+
281
+ ### Refreshing data between releases
282
+
283
+ The data lives in a local file the plugin reads at consultation time:
284
+
285
+ ```text
286
+ $XDG_DATA_HOME/opencode/ocadvisor/artificial-analysis.json # or ~/.local/share/...
287
+ ```
288
+
289
+ The `ocadvisor` CLI refreshes it independently of plugin releases:
290
+
291
+ ```sh
292
+ # Installed (npm bin):
293
+ ARTIFICIAL_ANALYSIS_API_KEY=... ocadvisor benchmarks update
294
+
295
+ # One-shot without a global install:
296
+ ARTIFICIAL_ANALYSIS_API_KEY=... bunx --package @pfoundation/ocadvisor ocadvisor benchmarks update
297
+
298
+ # Custom locations (absolute paths):
299
+ ocadvisor benchmarks update --path /srv/ocadvisor/artificial-analysis.json
300
+ ocadvisor benchmarks status --path /srv/ocadvisor/artificial-analysis.json --model openai/gpt-6-astra#xhigh
301
+ ```
302
+
303
+ - `benchmarks update` fetches the official models endpoint once, validates it,
304
+ and atomically replaces the snapshot. The old file is preserved byte-for-byte
305
+ if fetch, validation, or write fails; concurrent updaters coordinate through
306
+ a per-target lock. It prints the path, fetch time, model count, metric
307
+ coverage, and content hash.
308
+ - `benchmarks status` is fully offline. It reports the active source
309
+ (user file, bundled baseline, or unavailable), schema version, age, model
310
+ count, metric coverage, mapping counts, and — with `--model` — the exact
311
+ match or the reason a model is unresolved. Status describes what is on
312
+ disk; a running plugin keeps serving its last good in-memory copy until a
313
+ valid replacement is observed.
314
+ - Exit codes: `0` success; `1` update failed, no usable snapshot, or model
315
+ unresolved; `2` usage error.
316
+ - The plugin picks up a refreshed snapshot on the next consultation without a
317
+ restart. A consultation already in flight keeps the snapshot it started
318
+ with, so both models are always resolved against one consistent dataset.
319
+
320
+ The API key is read from the environment, trimmed, sent only as the
321
+ `x-api-key` header, and never stored or logged. The free Artificial Analysis
322
+ API is rate-limited, so refresh manually (for example between releases)
323
+ rather than in a loop. Benchmark data is provided by Artificial Analysis
324
+ (https://artificialanalysis.ai/).
325
+
326
+ A validated snapshot from the package release date ships inside the package
327
+ as the bundled baseline, so a fresh install has real coverage before the
328
+ first refresh. Regenerate the shipped data with
329
+ `ARTIFICIAL_ANALYSIS_API_KEY=... bun scripts/buildBenchmarkData.ts
330
+ --refresh-snapshot` (mappings only without the flag; the docs recommend
331
+ refreshing the user snapshot instead of rerunning this).
332
+
333
+ ### Model mappings
334
+
335
+ Published model IDs do not always line up with serving providers (gateways
336
+ add prefixes) or with the effort used in an evaluation. The plugin resolves
337
+ matches only through explicit bindings to stable Artificial Analysis IDs —
338
+ never fuzzy names, prefix stripping, or sibling substitution:
339
+
340
+ - `src/data/artificialAnalysis.mappings.json` ships baseline bindings
341
+ (curated in `scripts/buildBenchmarkData.ts`, every entry pointing at a
342
+ stable Artificial Analysis ID with its published evaluated effort).
343
+ - `model-mappings.json` beside the snapshot holds user overrides; these
344
+ survive refreshes and plugin upgrades.
345
+ - Binding keys are exact `(providerID, modelID, variant)` tuples. A null
346
+ variant covers unset variants only — it is never a wildcard. Variants are
347
+ independent: a `high` binding does not match an `xhigh` call.
348
+ - Local bindings override bundled ones per tuple and can cover a newly
349
+ released model with no plugin release:
350
+
351
+ ```jsonc
352
+ {
353
+ "schemaVersion": 1,
354
+ "bindings": [
355
+ {
356
+ "providerID": "openai",
357
+ "modelID": "gpt-6-astra",
358
+ "variant": "xhigh",
359
+ "aaModelID": "the-stable-artificial-analysis-uuid",
360
+ "evaluatedEffort": "max",
361
+ "evidenceURL": "https://artificialanalysis.ai/models/gpt-6-astra"
362
+ }
363
+ ]
364
+ }
365
+ ```
366
+
367
+ Set `evaluatedEffort` to the effort the evaluation actually ran at when the
368
+ source documents it, or `null` when it is unreported. Bindings with a null
369
+ evaluated effort still show scores but are labeled `effort_unknown`, and
370
+ their deltas are withheld from strict comparison. Use `benchmarks status
371
+ --model` to check exactly how a model resolves.
372
+
373
+ Unknown or unmapped models are not an error: consultations proceed with no
374
+ benchmark evidence for them, and the gate treats the gap as uncertainty.
104
375
 
105
376
  ## How it works
106
377
 
107
378
  - `src/index.ts` → `dist/index.js` is the published entrypoint (default
108
- export). `src/ocAdvisor.ts` holds the plugin implementation: it registers
109
- the `advisor` tool, injects a
110
- short selective-use instruction into eligible sessions via the `context` hook,
111
- and builds the transcript from the OpenCode SQLite database.
379
+ export); `index.js` at the repository root forwards to it so OpenCode's
380
+ local-directory loader can resolve a configured checkout.
381
+ `src/ocAdvisor.ts` holds the plugin implementation: it registers the
382
+ `advisor` tool, injects a short selective-use instruction into eligible
383
+ sessions via the `context` hook, and builds the transcript from the OpenCode
384
+ SQLite database. `src/typesafeGate.ts` and `src/typesafeState.ts` hold the
385
+ optional TypeSafe preflight. `src/benchmarkTypes.ts`, `src/artificialAnalysis.ts`,
386
+ `src/benchmarkUpdate.ts`, `src/benchmarkStore.ts`, `src/benchmarkMatch.ts`,
387
+ and `src/benchmarkEvidence.ts` hold the local Artificial Analysis snapshot,
388
+ its refresh transaction, the hot-reloading reader, and exact model mapping;
389
+ `src/cli.ts` exposes the maintenance commands.
112
390
  - The tool is registered as a direct tool (`options.codemode: false`).
113
391
  OpenCode 2 otherwise exposes plugin tools only through the `execute` Code
114
392
  Mode tool, whose tool log records each nested call's input but hides the
@@ -116,20 +394,29 @@ Rules enforced by the tool description and an injected session instruction:
116
394
  TUI. As a direct tool, the TUI's tool log shows the call's `mode`,
117
395
  `trigger`, and `question` fields followed by `output:` with the answer.
118
396
  Direct calls also avoid Code Mode's output-size truncation. The `context`
119
- hook can only hide the tool (Fable sessions), never add one, and the
120
- selective-use instruction is injected only when the tool is available to
121
- the request.
397
+ hook can only hide the tool (Fable sessions and `disabledForModels`
398
+ callers), never add one, and the selective-use instruction is injected
399
+ only when the tool is available to the request.
122
400
  - Before each consultation it checks OpenCode for support of the configured
123
- advisor model: the provider is enabled (`catalog.provider.get`), the model
124
- is available (`catalog.model.list`, configured variant when listed), and a
125
- connection exists (`integration.connection.active`).
401
+ advisor model: the provider is enabled (`provider.get`), the model is
402
+ available (`model.list`, configured variant when listed), and a connection
403
+ exists (`integration.connection.active`). The older `catalog.*` discovery
404
+ namespace remains as a compatibility fallback.
405
+ - Benchmark snapshots and mapping overrides are read through one cached
406
+ store per process. Before each gate-enabled consultation the store
407
+ re-stats both files and reloads only when their identity changed, so CLI
408
+ refreshes land on the next call without a restart; invalid replacements
409
+ keep the last good data and unmatched scenarios report explicit coverage
410
+ instead of guessing.
126
411
  - Consultations run as transient generations on a dedicated, reusable
127
412
  `advisor` session pinned to the configured model (default
128
- `anthropic/claude-fable-5-1#max`) via `session.create` +
413
+ `anthropic/claude-fable-5-1#xhigh`) via `session.create` +
129
414
  `session.switchModel` once, then `session.generate` per call. Transient
130
415
  generations do not mutate session history, so the advisor session stays
131
416
  empty while its stats attribute advisor spend. Title discovery also
132
- accepts the pre-rename `ocAdvisor` session title.
417
+ accepts the pre-rename `ocAdvisor` session title. When the agent requests
418
+ a different effort, the session is re-pinned to that variant first
419
+ (a no-op when it already matches).
133
420
  - The generation timeout wraps only the model call, not the time a call
134
421
  spends queued behind another consultation. Oversized transcripts are
135
422
  capped to the configured `maxTranscriptChars` (keeping the recent tail)
@@ -145,25 +432,55 @@ Rules enforced by the tool description and an injected session instruction:
145
432
  what is new since then.
146
433
  - Real failures (provider/model/connection issues, missing
147
434
  transcript/session) throw so OpenCode records them as errors instead of
148
- silent `completed` results. Fable skips still return the disabled notice.
435
+ silent `completed` results. Fable and configured-model skips still return
436
+ a disabled notice without TypeSafe or advisor requests.
149
437
 
150
438
  ## Metrics
151
439
 
152
440
  Every invocation appends one JSON line to
153
441
  `~/.local/share/opencode/ocAdvisor-metrics.jsonl` with timestamp, session,
154
- caller model/agent, mode, trigger, outcome (`advisor_response`,
155
- `skipped_fable`, `error`, `no_transcript`, `no_session`), error type
156
- (`provider_unavailable`, `model_unavailable`, `auth`, …), latency,
442
+ caller model/agent, mode, trigger, effective effort, outcome (`advisor_response`,
443
+ `skipped_fable`, `skipped_model`, `skipped_typesafe`, `error`, `no_transcript`,
444
+ `no_session`), error type
445
+ (`provider_unavailable`, `model_unavailable`, `invalid_effort`, `auth`, …), latency,
157
446
  transcript size, prior-consultation count, and transport (`via`).
158
447
  Token usage is `null`: OpenCode generation returns text only.
159
448
  Logging is best-effort and never breaks a call.
449
+ When the tool is hidden for the request, there is no invocation and no
450
+ metrics row. A direct call that still reaches the executor for an excluded
451
+ caller records one `skipped_model` line and no TypeSafe gate field.
452
+
453
+ Gate-enabled calls also carry a compact `benchmarks` object: snapshot source
454
+ (`user`/`seed`/`unavailable`), content hash, fetch time, hash-verified flag,
455
+ requester identity plus match status, advisor effort policy, the default
456
+ match, and — once generation runs — the final effort and its match. Rows
457
+ from before this feature simply lack the field; the report treats it as
458
+ optional and summarizes benchmark coverage when present.
160
459
 
161
460
  ## Activation
162
461
 
163
462
  The server loads plugin files once per process, so after installing or
164
- updating the plugin restart the background service
165
- (`opencode service restart`) or the old code keeps running. Location
166
- eviction does not reload plugin files.
463
+ updating the plugin, or after changing plugin options such as
464
+ `disabledForModels`, restart the background service
465
+ (`opencode service restart`) or the old code and config keep running.
466
+ Location eviction does not reload plugin files.
467
+
468
+ ## Local development install
469
+
470
+ A configured local checkout (the global `plugins` entry pointing at the
471
+ repository directory) is resolved through the root `index.js` shim, which
472
+ forwards the compiled `dist/index.js`. After changing `src/`, run
473
+ `bun run build` and restart the service:
474
+
475
+ ```sh
476
+ opencode plugin list
477
+ opencode api get '/api/plugin?location%5Bdirectory%5D=%2Fhome%2Fubuntu%2Fdev%2FocAdvisor'
478
+ ```
479
+
480
+ The first command lists CLI-managed package plugins; the second reports the
481
+ server plugins for an explicit location, where `oc-advisor` should appear
482
+ active. The config-directory watcher only tracks the `plugin/` and `plugins/`
483
+ directories, so changes to this checkout need an explicit service restart.
167
484
 
168
485
  ## Development
169
486
 
@@ -171,21 +488,81 @@ eviction does not reload plugin files.
171
488
  bun install # install dependencies (frozen lockfile in CI)
172
489
  bun test # unit tests
173
490
  bun run typecheck # typecheck (tsc --noEmit)
174
- bun run build # compile dist/ (runs automatically on npm pack/publish)
491
+ bun run build # compile dist/ + copy benchmark data (runs on npm pack/publish)
175
492
  bun run report # advisor usage over the last 30 days
176
493
  bun src/usageReport.ts --days 7
494
+ bun src/cli.ts benchmarks status # the shipped CLI, from source
177
495
  ```
178
496
 
179
497
  ## Evaluation
180
498
 
181
499
  The report combines the metrics log with the session database and shows
182
- invocation counts by outcome/mode/trigger plus eligibility coverage
183
- (sessions with ≥10 non-Fable tool calls vs. sessions that consulted).
184
- Re-run it after a few weeks of the new checkpoints to judge coverage and
185
- whether advice is changing outcomes. The usage report is a maintainer tool
186
- run from a source checkout; it is not shipped in the npm package.
500
+ generated advice, skips, gate decisions, and caller results plus eligibility
501
+ coverage (sessions with ≥10 non-Fable tool calls vs. sessions that received
502
+ generated advice). That coverage figure is a non-Fable activity proxy, not a
503
+ historical reconstruction of per-location `disabledForModels` settings.
504
+ Database and metrics views are independent and must not be
505
+ summed: a caller entry can fail while the plugin still recorded a generated
506
+ response, and a completed entry without visible output is reported as
507
+ unknown rather than success. Re-run it after a few weeks of the new
508
+ checkpoints to judge coverage and whether advice is changing outcomes. The
509
+ usage report is a maintainer tool run from a source checkout; it is not
510
+ shipped in the npm package.
511
+
512
+ ## Gate evaluation
513
+
514
+ `bun src/typesafeGate.eval.ts [--model jev-1.13.0]` runs labeled cases from
515
+ `src/fixtures/typesafeGate.cases.json` against the gate and prints per-case
516
+ decisions plus false-skip, unnecessary-proceed, fallback, latency, token, and
517
+ cost figures. Cases may carry synthetic benchmark profiles (fake model IDs
518
+ and scores) that exercise the full evidence wire format without touching real
519
+ snapshot files; the fixture file says so explicitly. It requires
520
+ `TYPESAFE_API_KEY` in the process environment and calls TypeSafe only — never
521
+ the advisor model. Neither the script nor its fixtures are published.
522
+
523
+ Initial run (2026-09-19, `jev-1.13.0`, six cases, 4,240 input tokens,
524
+ ≈$0.00018): no false skips on must-consult cases, no fallbacks, latency
525
+ p50/p95 266/626 ms.
526
+
527
+ Benchmark-aware run (same model and day, ten cases including four synthetic
528
+ benchmark profiles — stronger requester, weaker requester, unknown scores,
529
+ stale snapshot — 11,164 input tokens, ≈$0.00047): **0 false skips** on
530
+ must-consult cases, including the stronger-requester review and the
531
+ unknown-scores debug; the stale-snapshot review proceeded as well. Two
532
+ unnecessary proceeds on routine questions landed at need 0.23–0.33, above the
533
+ conservative default `skipBelow` (0.20): uncertainty preserves consultation
534
+ by design. No fallbacks; latency p50/p95 280/607 ms. Adjust the threshold
535
+ only from observed cases and re-run the evaluation after changing it.
536
+
537
+ ### End-to-end benchmark data flow
538
+
539
+ 1. Fresh install: the plugin reads the bundled snapshot shipped with the
540
+ package, so real coverage exists before any refresh. User files, when
541
+ present, take precedence over it.
542
+ 2. An operator with an Artificial Analysis key runs
543
+ `ocadvisor benchmarks update`. The validated snapshot lands in the data
544
+ directory and the next consultation logs its content hash.
545
+ 3. `ocadvisor benchmarks status --model ...` confirms which models now
546
+ resolve, and local `model-mappings.json` entries cover anything missing.
547
+ 4. A later plugin upgrade leaves the user snapshot and mappings untouched —
548
+ they live outside the package — so coverage persists across releases.
187
549
 
188
550
  ## Versioning
189
551
 
190
- Releases use calendar versioning (`YY.M.patch`, e.g. `26.9.0`). The Git tag
191
- (`v26.9.0`) must match `package.json` exactly; tag pushes publish to npm.
552
+ Releases use calendar versioning (`YY.M.patch`, e.g. `26.9.0`). The GitHub
553
+ Release tag (`v26.9.0`) is the source of truth: the publish workflow overwrites
554
+ `package.json` `version` from the tag. Do not bump `version` in `package.json`
555
+ for a release; leave it at the last published version.
556
+
557
+ ## Releasing (maintainers)
558
+
559
+ 1. Land the change on `master` and wait for CI to pass. Leave `package.json`
560
+ `version` as the last published release.
561
+ 2. Cut a GitHub Release on tag `vYY.M.patch`. Pre-releases (the release's
562
+ pre-release flag, or a `-suffix` version) publish under dist-tag `next`;
563
+ stable releases publish `--tag latest`. Pushing a tag alone publishes
564
+ nothing.
565
+ 3. The `publish` workflow sets the package version from the tag, re-runs
566
+ every gate, and publishes via OIDC trusted publishing (no npm token).
567
+ `v*` tag creation is restricted to maintainers by a ruleset.
568
+ 4. Verify: `npm view @pfoundation/ocadvisor@YY.M.patch`.
@@ -0,0 +1,4 @@
1
+ import { type BenchmarkSnapshot, type BenchmarkValidationError } from "./benchmarkTypes.js";
2
+ export declare const AA_LLM_MODELS_ENDPOINT = "https://artificialanalysis.ai/api/v2/data/llms/models";
3
+ export declare function normalizeAAResponse(body: unknown, fetchedAt: string): BenchmarkSnapshot | BenchmarkValidationError;
4
+ //# sourceMappingURL=artificialAnalysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artificialAnalysis.d.ts","sourceRoot":"","sources":["../src/artificialAnalysis.ts"],"names":[],"mappings":"AAIA,OAAO,EAQL,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,sBAAsB,0DACsB,CAAC;AAM1D,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,MAAM,GAChB,iBAAiB,GAAG,wBAAwB,CA0E9C"}
@@ -0,0 +1,77 @@
1
+ // Artificial Analysis response normalization: maps the documented LLM
2
+ // models payload onto the versioned benchmark snapshot. The response
3
+ // carries no structured effort, evaluation date, or methodology version,
4
+ // so those fields stay null rather than guessed.
5
+ import { BENCHMARK_SCHEMA_VERSION, BENCHMARK_SOURCE, BENCHMARK_SOURCE_URL, RECOGNIZED_METRICS, computeSnapshotHash, parseBenchmarkModel, } from "./benchmarkTypes.js";
6
+ export const AA_LLM_MODELS_ENDPOINT = "https://artificialanalysis.ai/api/v2/data/llms/models";
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ export function normalizeAAResponse(body, fetchedAt) {
11
+ if (!isRecord(body)) {
12
+ return { error: "artificial analysis response must be an object" };
13
+ }
14
+ if (typeof fetchedAt !== "string" ||
15
+ !Number.isFinite(Date.parse(fetchedAt))) {
16
+ return {
17
+ error: "artificial analysis refresh needs a valid fetchedAt timestamp",
18
+ };
19
+ }
20
+ if (!Array.isArray(body.data) || body.data.length === 0) {
21
+ return { error: "artificial analysis response has an empty data array" };
22
+ }
23
+ const seen = new Set();
24
+ const models = [];
25
+ for (let index = 0; index < body.data.length; index++) {
26
+ const entry = body.data[index];
27
+ const label = `entry ${index}`;
28
+ if (!isRecord(entry))
29
+ return { error: `${label} must be an object` };
30
+ const creator = isRecord(entry.model_creator) ? entry.model_creator : null;
31
+ const creatorID = creator && typeof creator.id === "string" ? creator.id.trim() : "";
32
+ if (!creatorID) {
33
+ return { error: `${label} is missing a model creator id` };
34
+ }
35
+ const parsed = parseBenchmarkModel({
36
+ id: entry.id,
37
+ creatorID,
38
+ name: entry.name,
39
+ slug: entry.slug,
40
+ evaluatedEffort: null,
41
+ evaluatedAt: null,
42
+ evaluations: entry.evaluations ?? {},
43
+ }, label);
44
+ if ("error" in parsed)
45
+ return parsed;
46
+ if (seen.has(parsed.id)) {
47
+ return { error: `duplicate benchmark model id ${parsed.id}` };
48
+ }
49
+ seen.add(parsed.id);
50
+ models.push(parsed);
51
+ }
52
+ const usable = models.some((model) => RECOGNIZED_METRICS.some((metric) => typeof model.evaluations[metric.key] === "number"));
53
+ if (!usable) {
54
+ return {
55
+ error: "artificial analysis response has no usable benchmark scores",
56
+ };
57
+ }
58
+ const snapshot = {
59
+ schemaVersion: BENCHMARK_SCHEMA_VERSION,
60
+ source: BENCHMARK_SOURCE,
61
+ sourceURL: BENCHMARK_SOURCE_URL,
62
+ endpoint: AA_LLM_MODELS_ENDPOINT,
63
+ fetchedAt,
64
+ contentHash: "",
65
+ methodologyVersion: null,
66
+ metricDefinitions: [...RECOGNIZED_METRICS],
67
+ models,
68
+ };
69
+ snapshot.contentHash = computeSnapshotHash({
70
+ source: snapshot.source,
71
+ endpoint: snapshot.endpoint,
72
+ methodologyVersion: snapshot.methodologyVersion,
73
+ models: snapshot.models,
74
+ });
75
+ return snapshot;
76
+ }
77
+ //# sourceMappingURL=artificialAnalysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artificialAnalysis.js","sourceRoot":"","sources":["../src/artificialAnalysis.ts"],"names":[],"mappings":"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"}