@pfoundation/ocadvisor 26.9.1 → 26.9.3

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