opencode-codeops 1.10.2 → 1.11.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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to CodeOps are recorded here.
4
4
 
5
+ ## 1.11.0 — 2026-10-04
6
+
7
+ ### Features
8
+
9
+ - effort: map missing levels to the nearest exposed variant
10
+
11
+ ### Documentation
12
+
13
+ - effort: note the exposed model variant set
14
+ - trace: record live verification and close T-03
15
+
16
+ ## 1.10.3 — 2026-10-04
17
+
18
+ ### Fixes
19
+
20
+ - effort: prefer the model variant over the reasoning-support flag
21
+
5
22
  ## 1.10.2 — 2026-10-04
6
23
 
7
24
  ### Fixes
package/README.md CHANGED
@@ -166,8 +166,10 @@ Reasoning-heavy skills also accept `--auto-effort` (use the skill's recommended
166
166
  `--auto-effort=high` (explicit level), announce it, and clear it before the final run summary.
167
167
  Resolution order: dispatch marker, then session `--auto-effort`, then
168
168
  `routing.roles.<agent>.reasoning` in `codeops/codeops.json`, then the inherited parent variant.
169
- The levels are suggestions: no permission, verification step, or review gate ever reads them. The
170
- full contract is in `_shared/reasoning-effort.md`.
169
+ A level the active model does not expose resolves to the nearest exposed variant (`medium`
170
+ becomes `high` on a `low`/`high`/`max` model). The levels are suggestions: no permission,
171
+ verification step, or review gate ever reads them. The full contract is in
172
+ `_shared/reasoning-effort.md`.
171
173
 
172
174
  ## Project specialists
173
175
 
@@ -16,13 +16,18 @@ contract consumed by the plugin and the skills. It is advisory by design and nev
16
16
  | `high` | Correctness- or security-sensitive, cross-cutting, or planning/review work | Requirements, planning, correctness/security review, ambiguous implementation |
17
17
  | `max` | Adversarial or high-risk analysis where a missed detail is costly | Thorough preflight, complex or sensitive phases |
18
18
 
19
- The four levels are the complete suggestion vocabulary. A level is applied only when the
20
- runtime model exposes a matching variant, or when the model reports reasoning support but has no
21
- variant record (the level is then written directly as the provider reasoning option). A level
22
- the model does not expose leaves the request unchanged and never raises a provider error.
23
- Routing policy is project configuration, not a suggestion:
24
- `routing.roles.<agent>.reasoning` may name any value from the provider enum
25
- (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`).
19
+ The four levels are the complete suggestion vocabulary. A level is applied through the model's
20
+ own variant record: the exact variant when it exists, otherwise the nearest exposed level on the
21
+ provider scale, with ties resolving to the higher level. `none` stays exact-match only, because
22
+ mapping it to a reasoning level would enable reasoning the caller disabled. When no variant can
23
+ be chosen — or when the model exposes no variants and does not report reasoning support — the
24
+ request is left unchanged, and it never raises a provider error. Routing policy is project
25
+ configuration, not a suggestion: `routing.roles.<agent>.reasoning` may name any value from the
26
+ provider enum (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`).
27
+
28
+ The deepseek flash model in this environment exposes `low`, `high`, and `max`, so a `medium`
29
+ suggestion resolves to `high` there. Confirm the active model's set by enabling
30
+ `CODEOPS_EFFORT_TRACE=1` and reading `variantLevels` on an apply line.
26
31
 
27
32
  ## Marker grammar
28
33
 
@@ -99,6 +104,9 @@ already records:
99
104
  | Docs/config/rename-only phase with deterministic verification | `low` |
100
105
  | Any other non-trivial phase | `medium` |
101
106
 
107
+ The runtime maps a missing level to the nearest exposed variant, so the four levels remain valid
108
+ suggestions on every reasoning-capable model.
109
+
102
110
  The line is written as `> **Reasoning**: <level> — <one-line reason>`. It is a suggestion: the
103
111
  user may edit or delete it, and `exec-plan` never blocks on it. A plan without the line keeps
104
112
  the inherited behavior.
@@ -135,9 +143,9 @@ forces that level for the whole run, including every dispatch marker composed du
135
143
  Set `CODEOPS_EFFORT_TRACE=1` (or `true`) in the environment that starts OpenCode to record what
136
144
  the plugin does. One content-free JSON line is appended per marker capture and per request to
137
145
  `reasoning-effort-trace.jsonl` inside the session's scratch directory. Lines carry only a
138
- timestamp, event name, message id, agent name, level, source, and applied flag — never prompt
139
- text or file content. The trace lives with the rest of the session scratch and is removed with
140
- it. Tracing is off by default and never affects a request.
146
+ timestamp, event name, message id, agent name, level, source, chosen variant, and applied flag —
147
+ never prompt text or file content. The trace lives with the rest of the session scratch and is
148
+ removed with it. Tracing is off by default and never affects a request.
141
149
 
142
150
  ## Suggestion-only guarantee
143
151
 
@@ -84,6 +84,12 @@ export declare function extractModelVariants(
84
84
  /** Check whether a model advertises reasoning support. */
85
85
  export declare function modelSupportsReasoning(model: unknown): boolean
86
86
 
87
+ /** Choose the model variant key to apply for a requested level. */
88
+ export declare function selectEffortVariant(
89
+ level: unknown,
90
+ model: unknown
91
+ ): RoutingReasoning | undefined
92
+
87
93
  /** Merge the model's variant options for a level into the request options. */
88
94
  export declare function applyEffort(
89
95
  options: Record<string, unknown>,
@@ -303,15 +303,89 @@ export function modelSupportsReasoning(model) {
303
303
  return capabilities.reasoning === true
304
304
  }
305
305
 
306
+ /**
307
+ * Order of provider reasoning levels, weakest to strongest.
308
+ *
309
+ * The scale is used only to pick the nearest exposed variant when a model
310
+ * does not offer the exact requested level.
311
+ */
312
+ const EFFORT_RANK = Object.freeze({
313
+ none: 0,
314
+ minimal: 1,
315
+ low: 2,
316
+ medium: 3,
317
+ high: 4,
318
+ xhigh: 5,
319
+ max: 6,
320
+ })
321
+
322
+ /**
323
+ * Choose the model variant key to apply for a requested level.
324
+ *
325
+ * The exact level wins when the model exposes it. Otherwise the nearest
326
+ * exposed level on the provider scale is chosen, and ties resolve to the
327
+ * higher level so ordinary work is never under-powered. `none` is
328
+ * exact-match only: mapping it to a reasoning level would enable reasoning
329
+ * the caller explicitly disabled. Keys whose value is not a plain object are
330
+ * ignored, so a malformed variant can never reach the request options.
331
+ *
332
+ * @param level - Requested level (any routing enum value)
333
+ * @param model - Model object from the hook input
334
+ * @returns The variant key to apply, or `undefined` when none applies
335
+ *
336
+ * @example
337
+ * selectEffortVariant("medium", { variants: { low: {}, high: {} } }) // "high"
338
+ */
339
+ export function selectEffortVariant(level, model) {
340
+ if (!isRoutingReasoning(level)) return undefined
341
+ const variants = extractModelVariants(model)
342
+ if (variants === undefined) return undefined
343
+
344
+ if (level === "none") {
345
+ return isPlainObject(variants.none) ? "none" : undefined
346
+ }
347
+
348
+ const candidates = Object.keys(variants).filter(
349
+ (key) =>
350
+ key !== "none" &&
351
+ Object.prototype.hasOwnProperty.call(EFFORT_RANK, key) &&
352
+ isPlainObject(variants[key])
353
+ )
354
+ if (candidates.includes(level)) return level
355
+
356
+ let best
357
+ for (const key of candidates) {
358
+ if (best === undefined) {
359
+ best = key
360
+ continue
361
+ }
362
+ const distance = Math.abs(EFFORT_RANK[key] - EFFORT_RANK[level])
363
+ const bestDistance = Math.abs(EFFORT_RANK[best] - EFFORT_RANK[level])
364
+ if (
365
+ distance < bestDistance ||
366
+ (distance === bestDistance && EFFORT_RANK[key] > EFFORT_RANK[best])
367
+ ) {
368
+ best = key
369
+ }
370
+ }
371
+ return best
372
+ }
373
+
306
374
  /**
307
375
  * Merge the model's variant options for a level into the request options.
308
376
  *
309
377
  * The runtime model carries a `variants` record whose entries are the exact
310
378
  * provider options for each level (for example `reasoningEffort`, or a nested
311
379
  * `reasoning.effort`). This function is the only place that mapping is
312
- * consumed, so the plugin never hardcodes a provider key. The function is
313
- * pure: it returns a new object when a change applies and the original object
314
- * reference otherwise.
380
+ * consumed, so the plugin never hardcodes a provider key. The applied key is
381
+ * chosen by {@link selectEffortVariant}: exact match first, then the nearest
382
+ * exposed level. A matching variant applies even when the model's reasoning
383
+ * capability flag is absent or false, because the variant itself is the
384
+ * provider-known option set and the host applies inherited variants the same
385
+ * way. Only when no variants record exists does the documented
386
+ * `reasoningEffort` passthrough require an explicit reasoning capability. The
387
+ * function is pure: it returns a new object when a change applies and the
388
+ * original object reference otherwise.
315
389
  *
316
390
  * @param options - Current provider options
317
391
  * @param level - Candidate level from {@link resolveEffort}
@@ -320,16 +394,15 @@ export function modelSupportsReasoning(model) {
320
394
  */
321
395
  export function applyEffort(options, level, model) {
322
396
  if (!isRoutingReasoning(level)) return options
323
- if (!modelSupportsReasoning(model)) return options
324
397
 
325
398
  const variants = extractModelVariants(model)
326
399
  if (variants !== undefined) {
327
- if (!Object.prototype.hasOwnProperty.call(variants, level)) return options
328
- const variantOptions = variants[level]
329
- if (!isPlainObject(variantOptions)) return options
330
- return deepMergePlain(options, variantOptions)
400
+ const variantKey = selectEffortVariant(level, model)
401
+ if (variantKey === undefined) return options
402
+ return deepMergePlain(options, variants[variantKey])
331
403
  }
332
404
 
405
+ if (!modelSupportsReasoning(model)) return options
333
406
  return { ...options, reasoningEffort: level }
334
407
  }
335
408
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "1.10.2",
3
+ "version": "1.11.0",
4
4
  "description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
5
5
  "type": "module",
6
6
  "engines": {
package/plugin/index.ts CHANGED
@@ -12,13 +12,16 @@ import {
12
12
  import {
13
13
  appendEffortTrace,
14
14
  applyEffort,
15
+ extractModelVariants,
15
16
  findEffortMarker,
16
17
  isEffortLevel,
17
18
  isEffortTraceEnabled,
18
19
  isRoutingReasoning,
20
+ modelSupportsReasoning,
19
21
  readRoutingReasoning,
20
22
  readSessionEffort,
21
23
  resolveEffort,
24
+ selectEffortVariant,
22
25
  } from "../bin/lib/reasoning-effort.mjs"
23
26
  import type { EffortLevel } from "../bin/lib/reasoning-effort.mjs"
24
27
 
@@ -348,6 +351,9 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
348
351
  level,
349
352
  source,
350
353
  applied: changed,
354
+ variant: selectEffortVariant(level, input.model) ?? null,
355
+ reasoningSupported: modelSupportsReasoning(input.model),
356
+ variantLevels: Object.keys(extractModelVariants(input.model) ?? {}),
351
357
  })
352
358
  if (!changed) {
353
359
  if (marker !== undefined) {
@@ -45,10 +45,15 @@ comes from an execution plan's per-phase suggestion; a session level comes from
45
45
  when a role entry exists — with no entry the child inherits the parent variant.
46
46
 
47
47
  The suggestion vocabulary is `low`, `medium`, `high`, and `max`, while the routing field accepts
48
- the wider provider enum (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`). The deepseek
49
- flash model exposes `low`/`medium`/`high`/`max`; prefer `low` for mechanical work, `medium` for
50
- bounded implementation, `high` for planning and review, and `max` only for adversarial analysis.
51
- An unsupported value is skipped at runtime, so a routing entry never produces a provider error.
48
+ the wider provider enum (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`). A level the
49
+ model does not expose resolves to the nearest exposed variant (ties choose the higher level);
50
+ `none` is exact-match only. On the deepseek flash model in this environment
51
+ (`low`/`high`/`max`), `medium` therefore resolves to `high`. Confirm the active set with
52
+ `CODEOPS_EFFORT_TRACE=1` and `variantLevels` on an apply line (see
53
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md)). Prefer `low` for
54
+ mechanical work, `medium` (or the nearest exposed level) for bounded implementation, `high` for
55
+ planning and review, and `max` only for adversarial analysis. A value that cannot be mapped is
56
+ skipped, so a routing entry never produces a provider error.
52
57
 
53
58
  Model pins are optional per role. When omitted, OpenCode resolves the model from the explicit spawn, project defaults, and parent session. A missing pin must never block the workflow.
54
59