opencode-codeops 1.9.0 → 1.10.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,31 @@
2
2
 
3
3
  All notable changes to CodeOps are recorded here.
4
4
 
5
+ ## 1.10.0 — 2026-10-04
6
+
7
+ ### Documentation
8
+
9
+ - agents: refresh managed project facts
10
+ - routing: clarify runtime variant validation
11
+ - plan: fix phase 3 review wording gaps
12
+
13
+ ### Fixes
14
+
15
+ - effort: tolerate malformed message parts in marker scan
16
+ - effort: correct phase 2 review findings
17
+ - effort: harden helper totality and contract wording
18
+
19
+ ### Features
20
+
21
+ - docs: add auto-effort flags and reasoning documentation
22
+ - plan: carry reasoning suggestions through plan skills
23
+ - effort: add session CLI and plugin runtime hooks
24
+ - effort: add reasoning-effort contract and helper
25
+
26
+ ### Tests
27
+
28
+ - effort: add implementation tests for reasoning-effort helper
29
+
5
30
  ## 1.9.0 — 2026-10-04
6
31
 
7
32
  ### Features
package/README.md CHANGED
@@ -153,6 +153,22 @@ To pin specific models per role, use the `setup-routing` skill or add overrides
153
153
  }
154
154
  ```
155
155
 
156
+ ### Adaptive reasoning effort
157
+
158
+ CodeOps can pick a reasoning level per dispatch instead of always inheriting the parent session's
159
+ variant. A dispatch packet may carry one standalone marker line:
160
+
161
+ ```text
162
+ [codeops-effort: medium]
163
+ ```
164
+
165
+ Reasoning-heavy skills also accept `--auto-effort` (use the skill's recommended level) or
166
+ `--auto-effort=high` (explicit level), announce it, and clear it before the final run summary.
167
+ Resolution order: dispatch marker, then session `--auto-effort`, then
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`.
171
+
156
172
  ## Project specialists
157
173
 
158
174
  CodeOps can recommend project-specific specialist subagents when repository evidence shows a
@@ -147,6 +147,10 @@ Resolution order is:
147
147
  3. project `[agents]` defaults in `opencode.json`;
148
148
  4. the parent session's model and effort.
149
149
 
150
+ For the runtime reasoning-effort override (a dispatch marker, a session `--auto-effort` level, or
151
+ a routing role default), see [reasoning-effort.md](reasoning-effort.md); the static resolution
152
+ order above is unchanged.
153
+
150
154
  Use `python3 "${CODEOPS_PLUGIN_ROOT}/scripts/install_agents.py" --project . --roles ...` to create optional project agents. Generated agent files carry a CodeOps marker. The installer owns only marked files and preserves every hand-authored file. Use `--check` to detect missing or stale generated agents and `--dry-run` to preview changes. Project specialists are generated with `--custom <role>` from `codeops/specialists/<role>.md` and indexed into `AGENTS.md` with `--sync-agents-md`; their routing policy may also set `reasoning`.
151
155
 
152
156
  Dynamic packets are the correctness baseline. If a named agent is missing or a model pin is unavailable, spawn a generic subagent with the complete packet or run inline. Report the fallback and preserve required reviewer independence, sandbox intent, and every ambiguity/readiness/verification gate.
@@ -0,0 +1,135 @@
1
+ # Reasoning effort (shared contract)
2
+
3
+ > **CodeOps Artifact Schema**: 1
4
+
5
+ CodeOps subagents normally inherit the parent session's model variant, so a top-tier parent
6
+ would pay top-tier reasoning cost for every child dispatch. Adaptive reasoning effort lets an
7
+ explicit source pick a level per dispatch or per skill run. This document is the shipped
8
+ contract consumed by the plugin and the skills. It is advisory by design and never a gate.
9
+
10
+ ## Levels
11
+
12
+ | Level | Meaning | Typical work |
13
+ | ----- | ------- | ------------ |
14
+ | `low` | Mechanical, fully specified, deterministic verification | Docs and formatting edits, renames, config updates |
15
+ | `medium` | Ordinary bounded feature work with known patterns | Standard implementation phases, recon, single-file reviews |
16
+ | `high` | Correctness- or security-sensitive, cross-cutting, or planning/review work | Requirements, planning, correctness/security review, ambiguous implementation |
17
+ | `max` | Adversarial or high-risk analysis where a missed detail is costly | Thorough preflight, complex or sensitive phases |
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`).
26
+
27
+ ## Marker grammar
28
+
29
+ A dispatch marker is one standalone line in a dispatch message:
30
+
31
+ ```text
32
+ [codeops-effort: <level>]
33
+ ```
34
+
35
+ - The whole trimmed line must match `^\[codeops-effort:\s*(low|medium|high|max)\]\s*$`.
36
+ - The level is exactly lower-case. Unknown or malformed markers are ignored, never errors.
37
+ - Only user-role text parts of a message are scanned; tool output, diffs, and quoted documents
38
+ are never scanned.
39
+ - The first valid marker in message order wins; additional markers are ignored.
40
+ - The marker is deliberately distinct from the human-readable plan line
41
+ `> **Reasoning**: ...`, so a quoted plan line can never act as a machine directive.
42
+
43
+ For quality-agent packets the marker follows the dispatch header; for packet kinds without a
44
+ header it is the first line. `exec-plan` owns the composition rule.
45
+
46
+ ## Precedence
47
+
48
+ The most specific source wins:
49
+
50
+ ```text
51
+ dispatch marker > session auto-effort > routing.roles[<agent>].reasoning > inherit parent variant
52
+ ```
53
+
54
+ | Source | Scope | Owner |
55
+ | ------ | ----- | ----- |
56
+ | Dispatch marker | One child dispatch | `exec-plan` composes it from the phase suggestion or the run's forced level |
57
+ | Session auto-effort | One skill run in the user's session | `--auto-effort` through `scripts/codeops_effort.py` |
58
+ | Routing role default | Project policy for one agent name | `codeops/codeops.json` → `routing.roles.<agent>.reasoning` |
59
+ | Inherit | Everything else | The child keeps the parent model and variant |
60
+
61
+ Routing lookup applies only when the dispatching agent's name has an explicit `routing.roles`
62
+ entry. There are no built-in catalog defaults, hand-authored agents are untouched, and
63
+ generated specialists keep their embedded value unless the project adds a routing entry.
64
+
65
+ ## Skill recommendation table
66
+
67
+ Used for suggestions and for a bare `--auto-effort`:
68
+
69
+ | Skill | Recommended level | Notes |
70
+ | ----- | ----------------- | ----- |
71
+ | `make-requirements` | `high` | Structured discovery and gap expansion |
72
+ | `make-plan` | `high` | Decomposition plus the Zero-Ambiguity Gate |
73
+ | `preflight` | `high`; `max` with `--thorough` | Adversarial multi-dimension audit |
74
+ | `grill-me` | `high` | Branch-by-branch disambiguation |
75
+ | `exec-plan` | current phase's `Reasoning:` level | Bare flag follows the phase; `=<level>` forces a constant |
76
+ | `retro-requirements` | `high` | Nine-phase archaeology |
77
+ | `upgrade-plan` | `high` | Content gate plus structural migration |
78
+ | `setup-codeops`, `setup-routing` | `high` (reference only) | Structure and policy authoring |
79
+ | `techdocs`, `analyze-project`, `clean-comments`, `outcome-review` | `medium` (reference only) | Structured but bounded |
80
+ | `roadmap`, `git-commit`, `github-issues` | `low` (reference only) | Mechanical bookkeeping |
81
+
82
+ Skills that accept `--auto-effort` are `make-requirements`, `make-plan`, `preflight`,
83
+ `grill-me`, `exec-plan`, `retro-requirements`, and `upgrade-plan`. Other skills document their
84
+ recommended levels for reference only and do not implement the flag.
85
+
86
+ ## Plan suggestion derivation
87
+
88
+ `make-plan` derives an advisory level for every phase and every task mini-plan from signals it
89
+ already records:
90
+
91
+ | Signal | Suggested level |
92
+ | ------ | --------------- |
93
+ | Phase carries a complexity escalation approval, or a complex/sensitive tag | `max` |
94
+ | Phase carries a security, financial-integrity, concurrency, performance-critical, compiler-semantics, or migration lens/risk tag | `high` |
95
+ | Docs/config/rename-only phase with deterministic verification | `low` |
96
+ | Any other non-trivial phase | `medium` |
97
+
98
+ The line is written as `> **Reasoning**: <level> — <one-line reason>`. It is a suggestion: the
99
+ user may edit or delete it, and `exec-plan` never blocks on it. A plan without the line keeps
100
+ the inherited behavior.
101
+
102
+ ## Auto-effort option
103
+
104
+ Skills listed above accept the flag. Parsing follows the existing standalone-token pattern
105
+ (`--auto-design`, `--explore-scope`): exactly one occurrence before the first `--` sentinel,
106
+ removed before resolving targets, paths, or modes; zero means advise-only; more than one or an
107
+ invalid `=<level>` is an argument error.
108
+
109
+ | Flag | Behavior |
110
+ | ---- | -------- |
111
+ | *(absent)* | Print `Suggested reasoning: <level> — <reason>` once at start (`exec-plan`: before each phase); change nothing |
112
+ | `--auto-effort` | Use the skill's recommended level (`exec-plan`: the current phase level) |
113
+ | `--auto-effort=<level>` | Use the named level; reject values outside the four-level set |
114
+
115
+ Semantics:
116
+
117
+ 1. Announce `Auto-effort active — reasoning <level> applied for this run`.
118
+ 2. Record the level for the session run through `scripts/codeops_effort.py`.
119
+ 3. Clear it at run completion, before the final summary.
120
+ 4. Run-scoped: the level applies from the point it is set until cleared or the session ends.
121
+ 5. Fail-open: when the session temp directory is unavailable or the helper fails, print an
122
+ advise-only note and continue; the skill never blocks.
123
+ 6. Never a gate: effort affects cost and latency only. No readiness check, verification step,
124
+ reviewer requirement, or finding gate may read it.
125
+
126
+ For `exec-plan`, a bare flag follows each phase's suggestion, while `--auto-effort=<level>`
127
+ forces that level for the whole run, including every dispatch marker composed during it.
128
+
129
+ ## Suggestion-only guarantee
130
+
131
+ | Allowed | Forbidden |
132
+ | ------- | --------- |
133
+ | Printing a suggested level and its reason | Blocking a run because a level is missing or unsupported |
134
+ | Applying a level when a marker, flag, or routing entry explicitly asks | Overriding an explicit user plan edit with a derived default |
135
+ | Reporting the applied level and its source | Treating effort as an acceptance, review, or security control |
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Type declarations for the CodeOps adaptive reasoning-effort helper.
3
+ *
4
+ * The helper is plain JavaScript so `node --test` can exercise it directly;
5
+ * these declarations give the TypeScript plugin entry point typed access.
6
+ */
7
+
8
+ /** A level from the four-level suggestion vocabulary. */
9
+ export type EffortLevel = "low" | "medium" | "high" | "max"
10
+
11
+ /** A reasoning value accepted by `routing.roles.<agent>.reasoning`. */
12
+ export type RoutingReasoning =
13
+ | "none"
14
+ | "minimal"
15
+ | "low"
16
+ | "medium"
17
+ | "high"
18
+ | "xhigh"
19
+ | "max"
20
+
21
+ /** The four levels a plan or skill may suggest. */
22
+ export declare const EFFORT_LEVELS: readonly EffortLevel[]
23
+
24
+ /** Check whether a value is one of the four suggestion levels. */
25
+ export declare function isEffortLevel(value: unknown): value is EffortLevel
26
+
27
+ /** The reasoning values accepted by routing role entries. */
28
+ export declare const ROUTING_REASONING_VALUES: readonly RoutingReasoning[]
29
+
30
+ /** Check whether a value is a valid routing reasoning entry. */
31
+ export declare function isRoutingReasoning(value: unknown): value is RoutingReasoning
32
+
33
+ /** Find the first valid dispatch marker across message text parts. */
34
+ export declare function findEffortMarker(texts: unknown): EffortLevel | undefined
35
+
36
+ /** Candidate sources for {@link resolveEffort}. */
37
+ export interface ResolveEffortInput {
38
+ marker?: unknown
39
+ session?: unknown
40
+ routing?: unknown
41
+ }
42
+
43
+ /** Resolve the effective level from the explicit sources, most specific first. */
44
+ export declare function resolveEffort(
45
+ input?: ResolveEffortInput
46
+ ): EffortLevel | RoutingReasoning | undefined
47
+
48
+ /** Compute the session state-file path inside the session temp directory. */
49
+ export declare function sessionEffortPath(sessionID?: string, base?: string): string
50
+
51
+ /** Parse session state-file text into a validated level. */
52
+ export declare function parseStateFile(text: string): EffortLevel | undefined
53
+
54
+ /** Read the session's reasoning-effort state file. */
55
+ export declare function readSessionEffort(
56
+ sessionID?: string,
57
+ base?: string
58
+ ): EffortLevel | undefined
59
+
60
+ /** Read an agent's explicit reasoning entry from the project routing config. */
61
+ export declare function readRoutingReasoning(
62
+ config: unknown,
63
+ agent: unknown
64
+ ): RoutingReasoning | undefined
65
+
66
+ /** Extract the model's runtime variant record, when the host exposes one. */
67
+ export declare function extractModelVariants(
68
+ model: unknown
69
+ ): Record<string, unknown> | undefined
70
+
71
+ /** Check whether a model advertises reasoning support. */
72
+ export declare function modelSupportsReasoning(model: unknown): boolean
73
+
74
+ /** Merge the model's variant options for a level into the request options. */
75
+ export declare function applyEffort(
76
+ options: Record<string, unknown>,
77
+ level: unknown,
78
+ model: unknown
79
+ ): Record<string, unknown>
80
+
81
+ /** Recursively merge plain objects into a new object. */
82
+ export declare function deepMergePlain(
83
+ target: unknown,
84
+ source: unknown
85
+ ): Record<string, unknown>
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Adaptive reasoning-effort helper for CodeOps sessions.
4
+ *
5
+ * The plugin needs one place to answer a simple question per request: "which
6
+ * reasoning level should this request use, if any?" This module answers it
7
+ * without any framework dependency so `node --test` can exercise every edge:
8
+ *
9
+ * - marker scanning over message text parts;
10
+ * - source precedence (dispatch marker, session flag, routing default);
11
+ * - routing-config lookup with hostile-shape tolerance;
12
+ * - provider-option application through the model's own variant record; and
13
+ * - reading the per-session state file written by the skills.
14
+ *
15
+ * Every function is deliberately total: malformed input yields "no override",
16
+ * never a thrown error. The plugin hooks sit on the request path, where a
17
+ * crash would break the user's session, so safety beats strictness here.
18
+ *
19
+ * @module lib/reasoning-effort
20
+ */
21
+
22
+ import { lstatSync, readFileSync } from "node:fs"
23
+ import { join } from "node:path"
24
+
25
+ import { sessionTmpDir } from "./tmp-hygiene.mjs"
26
+
27
+ /**
28
+ * The four levels a plan or skill may suggest.
29
+ *
30
+ * These are the only values accepted in dispatch markers, session flags, and
31
+ * plan suggestions. Routing policy is project configuration and may name the
32
+ * wider provider enum instead.
33
+ */
34
+ export const EFFORT_LEVELS = Object.freeze(["low", "medium", "high", "max"])
35
+
36
+ /**
37
+ * The reasoning values accepted by `routing.roles.<agent>.reasoning` in the
38
+ * project configuration.
39
+ *
40
+ * The list mirrors the CodeOps config schema, which passes provider-native
41
+ * values through so a project can ask for `minimal` or `xhigh` even though the
42
+ * suggestion vocabulary only offers the four common levels.
43
+ */
44
+ export const ROUTING_REASONING_VALUES = Object.freeze([
45
+ "none",
46
+ "minimal",
47
+ "low",
48
+ "medium",
49
+ "high",
50
+ "xhigh",
51
+ "max",
52
+ ])
53
+
54
+ /**
55
+ * Largest state file accepted by {@link readSessionEffort}.
56
+ *
57
+ * The real file is a few dozen bytes. The cap keeps a hostile or corrupt file
58
+ * from being slurped into memory if the temp directory is ever tampered with.
59
+ */
60
+ const MAX_STATE_FILE_BYTES = 4096
61
+
62
+ /**
63
+ * One standalone marker line, anchored to the whole line.
64
+ *
65
+ * The marker is intentionally different from the human-readable plan line
66
+ * `> **Reasoning**: ...`, so quoting a plan can never act as a machine
67
+ * directive. Whitespace inside the brackets is tolerated; text before or after
68
+ * is not.
69
+ */
70
+ const MARKER_PATTERN = /^\[codeops-effort:\s*(low|medium|high|max)\]\s*$/
71
+
72
+ /**
73
+ * Check whether a value is one of the four suggestion levels.
74
+ *
75
+ * @param value - Value to inspect
76
+ * @returns True only for `"low"`, `"medium"`, `"high"`, or `"max"`
77
+ *
78
+ * @example
79
+ * isEffortLevel("high") // true
80
+ * isEffortLevel("xhigh") // false
81
+ */
82
+ export function isEffortLevel(value) {
83
+ return typeof value === "string" && EFFORT_LEVELS.includes(value)
84
+ }
85
+
86
+ /**
87
+ * Check whether a value is a valid routing reasoning entry.
88
+ *
89
+ * @param value - Value to inspect
90
+ * @returns True for any value in the routing enum
91
+ */
92
+ export function isRoutingReasoning(value) {
93
+ return typeof value === "string" && ROUTING_REASONING_VALUES.includes(value)
94
+ }
95
+
96
+ /**
97
+ * Find the first valid dispatch marker across a list of message text parts.
98
+ *
99
+ * Only string entries are inspected; non-string entries (tool parts, images,
100
+ * malformed structures) are skipped so a hostile part can never break the
101
+ * request path. Lines are split on `\n`, `\r\n`, and `\r`, then trimmed before
102
+ * the anchored pattern is applied, so surrounding whitespace is tolerated.
103
+ *
104
+ * @param texts - Candidate text parts (usually the text of a message's parts)
105
+ * @returns The first valid level, or `undefined` when none is present
106
+ *
107
+ * @example
108
+ * findEffortMarker(["run the task", "[codeops-effort: medium]"]) // "medium"
109
+ */
110
+ export function findEffortMarker(texts) {
111
+ if (!Array.isArray(texts)) return undefined
112
+ for (const text of texts) {
113
+ if (typeof text !== "string") continue
114
+ for (const line of text.split(/\r\n|\r|\n/)) {
115
+ const match = MARKER_PATTERN.exec(line.trim())
116
+ if (match) return match[1]
117
+ }
118
+ }
119
+ return undefined
120
+ }
121
+
122
+ /**
123
+ * Resolve the effective level from the three explicit sources.
124
+ *
125
+ * Precedence is "most specific wins": a dispatch marker beats a session
126
+ * flag, which beats a routing default. Each source is validated before it is
127
+ * accepted, so an invalid value is ignored rather than propagated. A
128
+ * non-object argument yields `undefined` instead of throwing.
129
+ *
130
+ * @param input - Candidate sources; all optional
131
+ * @returns The first valid value, or `undefined` when no source applies
132
+ *
133
+ * @example
134
+ * resolveEffort({ session: "high", routing: "low" }) // "high"
135
+ */
136
+ export function resolveEffort(input) {
137
+ if (!isPlainObject(input)) return undefined
138
+ const { marker, session, routing } = input
139
+ if (isEffortLevel(marker)) return marker
140
+ if (isEffortLevel(session)) return session
141
+ if (isRoutingReasoning(routing)) return routing
142
+ return undefined
143
+ }
144
+
145
+ /**
146
+ * Compute the session state-file path inside the session temp directory.
147
+ *
148
+ * @param sessionID - Session identifier
149
+ * @param base - Base temp directory (injectable for tests)
150
+ * @returns Absolute path to `reasoning-effort.json` for the session
151
+ */
152
+ export function sessionEffortPath(sessionID, base) {
153
+ return join(sessionTmpDir(sessionID, base), "reasoning-effort.json")
154
+ }
155
+
156
+ /**
157
+ * Parse session state-file text into a validated level.
158
+ *
159
+ * The expected payload is `{"schema":1,"reasoning":"<level>"}` with any
160
+ * unknown keys ignored. Anything else — malformed JSON, the wrong schema, a
161
+ * level outside the four-level allowlist — yields `undefined`.
162
+ *
163
+ * @param text - Raw file contents
164
+ * @returns The validated level, or `undefined`
165
+ */
166
+ export function parseStateFile(text) {
167
+ try {
168
+ const value = JSON.parse(text)
169
+ if (!isPlainObject(value)) return undefined
170
+ if (value.schema !== 1) return undefined
171
+ return isEffortLevel(value.reasoning) ? value.reasoning : undefined
172
+ } catch {
173
+ return undefined
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Read the session's reasoning-effort state file.
179
+ *
180
+ * The file is written by `scripts/codeops_effort.py` for `--auto-effort`
181
+ * runs. Only a small regular file is accepted: a missing file, a non-regular
182
+ * file (symlinks are never followed), an oversized file, and any read or parse
183
+ * failure all mean "no session effort" and return `undefined`.
184
+ *
185
+ * @param sessionID - Session identifier
186
+ * @param base - Base temp directory (injectable for tests)
187
+ * @returns The session level, or `undefined`
188
+ */
189
+ export function readSessionEffort(sessionID, base) {
190
+ try {
191
+ const path = sessionEffortPath(sessionID, base)
192
+ const info = lstatSync(path)
193
+ if (!info.isFile()) return undefined
194
+ if (info.size > MAX_STATE_FILE_BYTES) return undefined
195
+ return parseStateFile(readFileSync(path, "utf-8"))
196
+ } catch {
197
+ return undefined
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Read an agent's explicit reasoning entry from the project routing config.
203
+ *
204
+ * Only an own `routing.roles.<agent>.reasoning` property counts; unknown
205
+ * agents, missing sections, inherited properties, and malformed shapes all
206
+ * return `undefined` instead of throwing.
207
+ *
208
+ * @param config - Parsed `codeops/codeops.json` content (any shape)
209
+ * @param agent - Dispatching agent name
210
+ * @returns The configured routing value, or `undefined`
211
+ */
212
+ export function readRoutingReasoning(config, agent) {
213
+ if (!isPlainObject(config)) return undefined
214
+ const routing = config.routing
215
+ if (!isPlainObject(routing)) return undefined
216
+ const roles = routing.roles
217
+ if (!isPlainObject(roles)) return undefined
218
+ if (typeof agent !== "string" || agent.length === 0) return undefined
219
+ if (!Object.prototype.hasOwnProperty.call(roles, agent)) return undefined
220
+ const entry = roles[agent]
221
+ if (!isPlainObject(entry)) return undefined
222
+ return isRoutingReasoning(entry.reasoning) ? entry.reasoning : undefined
223
+ }
224
+
225
+ /**
226
+ * Extract the model's runtime variant record, when the host exposes one.
227
+ *
228
+ * The installed SDK type does not declare `variants`, but the running host
229
+ * attaches it to every model. The `in` check keeps this graceful when the
230
+ * property is absent, and a cast is never used.
231
+ *
232
+ * @param model - Model object from the hook input
233
+ * @returns The variants record when it is a plain object, else `undefined`
234
+ */
235
+ export function extractModelVariants(model) {
236
+ if (!isPlainObject(model)) return undefined
237
+ if (!("variants" in model)) return undefined
238
+ return isPlainObject(model.variants) ? model.variants : undefined
239
+ }
240
+
241
+ /**
242
+ * Check whether a model advertises reasoning support.
243
+ *
244
+ * @param model - Model object from the hook input
245
+ * @returns True only when `capabilities.reasoning` is exactly `true`
246
+ */
247
+ export function modelSupportsReasoning(model) {
248
+ if (!isPlainObject(model)) return false
249
+ const capabilities = model.capabilities
250
+ if (!isPlainObject(capabilities)) return false
251
+ return capabilities.reasoning === true
252
+ }
253
+
254
+ /**
255
+ * Merge the model's variant options for a level into the request options.
256
+ *
257
+ * The runtime model carries a `variants` record whose entries are the exact
258
+ * provider options for each level (for example `reasoningEffort`, or a nested
259
+ * `reasoning.effort`). This function is the only place that mapping is
260
+ * consumed, so the plugin never hardcodes a provider key. The function is
261
+ * pure: it returns a new object when a change applies and the original object
262
+ * reference otherwise.
263
+ *
264
+ * @param options - Current provider options
265
+ * @param level - Candidate level from {@link resolveEffort}
266
+ * @param model - Model object from the hook input
267
+ * @returns The original options, or a new merged object when a change applies
268
+ */
269
+ export function applyEffort(options, level, model) {
270
+ if (!isRoutingReasoning(level)) return options
271
+ if (!modelSupportsReasoning(model)) return options
272
+
273
+ const variants = extractModelVariants(model)
274
+ if (variants !== undefined) {
275
+ if (!Object.prototype.hasOwnProperty.call(variants, level)) return options
276
+ const variantOptions = variants[level]
277
+ if (!isPlainObject(variantOptions)) return options
278
+ return deepMergePlain(options, variantOptions)
279
+ }
280
+
281
+ return { ...options, reasoningEffort: level }
282
+ }
283
+
284
+ /**
285
+ * Recursively merge plain objects into a new object.
286
+ *
287
+ * Values that are not plain objects — arrays, class instances, primitives —
288
+ * are replaced, not merged. Neither input is mutated. The result is created
289
+ * with data properties, so a hostile `__proto__` key in a variant can never
290
+ * change the result's prototype chain.
291
+ *
292
+ * @param target - Base object
293
+ * @param source - Overrides to merge on top
294
+ * @returns A new merged plain object
295
+ */
296
+ export function deepMergePlain(target, source) {
297
+ const result = isPlainObject(target) ? { ...target } : {}
298
+ if (!isPlainObject(source)) return result
299
+
300
+ for (const key of Object.keys(source)) {
301
+ const incoming = source[key]
302
+ const current = Object.prototype.hasOwnProperty.call(result, key)
303
+ ? result[key]
304
+ : undefined
305
+ const merged =
306
+ isPlainObject(incoming) && isPlainObject(current)
307
+ ? deepMergePlain(current, incoming)
308
+ : incoming
309
+ Object.defineProperty(result, key, {
310
+ value: merged,
311
+ writable: true,
312
+ enumerable: true,
313
+ configurable: true,
314
+ })
315
+ }
316
+
317
+ return result
318
+ }
319
+
320
+ /**
321
+ * Check whether a value is a plain object (not an array, class instance, or
322
+ * `null`).
323
+ *
324
+ * @param value - Value to inspect
325
+ * @returns True for `{}`-shaped objects and objects with a null prototype
326
+ */
327
+ function isPlainObject(value) {
328
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false
329
+ const prototype = Object.getPrototypeOf(value)
330
+ return prototype === Object.prototype || prototype === null
331
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "1.9.0",
3
+ "version": "1.10.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
@@ -9,6 +9,14 @@ import {
9
9
  ensureSessionTmpDir,
10
10
  removeSessionTmpDir,
11
11
  } from "../bin/lib/tmp-hygiene.mjs"
12
+ import {
13
+ applyEffort,
14
+ findEffortMarker,
15
+ readRoutingReasoning,
16
+ readSessionEffort,
17
+ resolveEffort,
18
+ } from "../bin/lib/reasoning-effort.mjs"
19
+ import type { EffortLevel } from "../bin/lib/reasoning-effort.mjs"
12
20
 
13
21
  // ---------------------------------------------------------------------------
14
22
  // Package root — resolved at module load time so it is always the plugin's
@@ -137,11 +145,44 @@ async function warnOnVersionSkew(
137
145
  }
138
146
  }
139
147
 
148
+ // ---------------------------------------------------------------------------
149
+ // Helper — log one content-free warning. Logging is best effort: a failed log
150
+ // must never break a request.
151
+ // ---------------------------------------------------------------------------
152
+ async function warnContentFree(
153
+ client: Parameters<Plugin>[0]["client"],
154
+ message: string
155
+ ): Promise<void> {
156
+ try {
157
+ await client.app.log({ body: { service: "codeops", level: "warn", message } })
158
+ } catch {
159
+ // Best effort only.
160
+ }
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Helper — read the project routing config fresh on every request, so an edit
165
+ // applies without restarting the session. Any failure means "no routing".
166
+ // ---------------------------------------------------------------------------
167
+ function readRoutingConfig(directory: string): unknown {
168
+ try {
169
+ return JSON.parse(readFileSync(join(directory, "codeops", "codeops.json"), "utf8"))
170
+ } catch {
171
+ return {}
172
+ }
173
+ }
174
+
140
175
  // ---------------------------------------------------------------------------
141
176
  // CodeOps plugin for OpenCode
142
177
  // Replaces: hooks/hooks.json + hook_session_context.sh + hook_marker_guard.sh
143
178
  // ---------------------------------------------------------------------------
144
179
  export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
180
+ // Reasoning-effort state lives for the lifetime of this plugin instance:
181
+ // one entry per user message that carried a dispatch marker, plus a
182
+ // deduplication set for unsupported-level warnings.
183
+ const effortMarkers = new Map<string, { sessionID: string; level: EffortLevel }>()
184
+ const warnedEffortLevels = new Set<string>()
185
+
145
186
  return {
146
187
  // -----------------------------------------------------------------------
147
188
  // Hook 1 & 2: inject standards on session.created and session.compacted.
@@ -160,6 +201,13 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
160
201
  } else if (event.type === "session.deleted") {
161
202
  const info = (event.properties as { info: { id: string } }).info
162
203
  removeSessionTmpDir(info.id)
204
+ try {
205
+ for (const [messageID, entry] of effortMarkers) {
206
+ if (entry.sessionID === info.id) effortMarkers.delete(messageID)
207
+ }
208
+ } catch {
209
+ await warnContentFree(client, "Could not clear captured reasoning-effort markers.")
210
+ }
163
211
  } else if (event.type === "session.compacted") {
164
212
  const sessionId: string = (event.properties as { sessionID: string }).sessionID
165
213
  await injectStandards(client, sessionId)
@@ -218,5 +266,59 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
218
266
  )
219
267
  }
220
268
  },
269
+
270
+ // -----------------------------------------------------------------------
271
+ // Hook 6: capture a dispatch marker from an incoming user message. The
272
+ // marker travels in the dispatch packet text; storing it by message id
273
+ // lets the later chat.params hook apply it to the same request.
274
+ // -----------------------------------------------------------------------
275
+ "chat.message": async (input, output) => {
276
+ try {
277
+ const texts = output.parts.map((part) =>
278
+ part?.type === "text" ? part.text : undefined
279
+ )
280
+ const level = findEffortMarker(texts)
281
+ if (level !== undefined) {
282
+ effortMarkers.set(output.message.id, { sessionID: input.sessionID, level })
283
+ }
284
+ } catch {
285
+ await warnContentFree(client, "Could not scan a message for a reasoning-effort marker.")
286
+ }
287
+ },
288
+
289
+ // -----------------------------------------------------------------------
290
+ // Hook 7: resolve the request's reasoning level (dispatch marker, then
291
+ // session flag, then routing default) and merge the model's own variant
292
+ // options. Any failure leaves the request unchanged.
293
+ // -----------------------------------------------------------------------
294
+ "chat.params": async (input, output) => {
295
+ try {
296
+ const stored = effortMarkers.get(input.message.id)
297
+ const marker = stored && stored.sessionID === input.sessionID ? stored.level : undefined
298
+ const session = readSessionEffort(input.sessionID)
299
+ const routing = readRoutingReasoning(readRoutingConfig(directory), input.agent)
300
+ const level = resolveEffort({ marker, session, routing })
301
+ if (level === undefined) return
302
+
303
+ const applied = applyEffort(output.options, level, input.model)
304
+ if (applied === output.options) {
305
+ if (marker !== undefined) {
306
+ const warningKey = `${input.sessionID}:${marker}`
307
+ if (!warnedEffortLevels.has(warningKey)) {
308
+ warnedEffortLevels.add(warningKey)
309
+ await warnContentFree(
310
+ client,
311
+ `Reasoning effort ${marker} is not available for agent ${input.agent}; ` +
312
+ "request left unchanged."
313
+ )
314
+ }
315
+ }
316
+ return
317
+ }
318
+ output.options = applied
319
+ } catch {
320
+ await warnContentFree(client, "Could not apply a reasoning-effort level to a request.")
321
+ }
322
+ },
221
323
  }
222
324
  }
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ """Record and clear the opt-in reasoning-effort level for a CodeOps session run.
3
+
4
+ The skills call this helper when a user passes `--auto-effort`. It writes one
5
+ small JSON file inside the session's CodeOps temp directory; the plugin reads
6
+ that file on every request and applies the level. The command fails closed:
7
+ an invalid level or a directory outside the CodeOps temp root exits with
8
+ status 2 and writes nothing.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import sys
17
+ import tempfile
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+
22
+ #: The only levels a session flag may record.
23
+ EFFORT_LEVELS = ("low", "medium", "high", "max")
24
+
25
+ #: Name of the state file the plugin reads.
26
+ STATE_FILE_NAME = "reasoning-effort.json"
27
+
28
+
29
+ def codeops_tmp_root() -> Path:
30
+ """Return the CodeOps-owned temp root for the active temp directory.
31
+
32
+ The root mirrors the layout `bin/lib/tmp-hygiene.mjs` owns, so the helper
33
+ and the plugin resolve the same per-session directory.
34
+
35
+ Returns:
36
+ Resolved path of the CodeOps temp root.
37
+ """
38
+ return Path(tempfile.gettempdir()).resolve() / "opencode" / "codeops"
39
+
40
+
41
+ def resolve_session_dir(raw_dir: str) -> Path | None:
42
+ """Validate a session directory argument against the CodeOps temp root.
43
+
44
+ A directory is accepted only when it exists, resolves strictly inside the
45
+ CodeOps temp root, and is not the root itself. Symlinks are resolved, so a
46
+ link that points outside the root is rejected.
47
+
48
+ Args:
49
+ raw_dir: The `--dir` argument as provided by the caller.
50
+
51
+ Returns:
52
+ The resolved session directory, or None when it is not acceptable.
53
+ """
54
+ try:
55
+ resolved = Path(raw_dir).resolve(strict=True)
56
+ except (OSError, RuntimeError):
57
+ # RuntimeError covers a symlink loop on Python 3.12, which the
58
+ # filesystem reports as ELOOP rather than a plain OSError.
59
+ return None
60
+ root = codeops_tmp_root()
61
+ if resolved == root or root not in resolved.parents:
62
+ return None
63
+ if not resolved.is_dir():
64
+ return None
65
+ return resolved
66
+
67
+
68
+ def state_path(session_dir: Path) -> Path:
69
+ """Return the state-file path inside a validated session directory.
70
+
71
+ Args:
72
+ session_dir: A directory accepted by `resolve_session_dir`.
73
+
74
+ Returns:
75
+ Path of the reasoning-effort state file.
76
+ """
77
+ return session_dir / STATE_FILE_NAME
78
+
79
+
80
+ def set_level(session_dir: Path, level: str) -> int:
81
+ """Write the session level atomically and report it.
82
+
83
+ The payload is written to a temporary file in the same directory and then
84
+ moved over the final name with `os.replace`, so a reader never observes a
85
+ partially written file.
86
+
87
+ Args:
88
+ session_dir: A validated session directory.
89
+ level: One of `EFFORT_LEVELS`.
90
+
91
+ Returns:
92
+ Process exit code 0.
93
+ """
94
+ payload = {
95
+ "schema": 1,
96
+ "reasoning": level,
97
+ "setAt": datetime.now(timezone.utc).isoformat(timespec="seconds"),
98
+ }
99
+ path = state_path(session_dir)
100
+ descriptor, temporary_name = tempfile.mkstemp(
101
+ prefix=f"{STATE_FILE_NAME}.", suffix=".tmp", dir=session_dir
102
+ )
103
+ try:
104
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
105
+ json.dump(payload, handle, separators=(",", ":"), sort_keys=True)
106
+ handle.write("\n")
107
+ os.replace(temporary_name, path)
108
+ except OSError:
109
+ try:
110
+ os.unlink(temporary_name)
111
+ except OSError:
112
+ pass
113
+ print("Error: could not write the reasoning-effort state file.", file=sys.stderr)
114
+ return 2
115
+ print(f"Reasoning effort set: {level} for this session run.")
116
+ return 0
117
+
118
+
119
+ def clear_level(session_dir: Path) -> int:
120
+ """Remove the session level when present and report the cleanup.
121
+
122
+ Args:
123
+ session_dir: A validated session directory.
124
+
125
+ Returns:
126
+ Process exit code 0.
127
+ """
128
+ try:
129
+ state_path(session_dir).unlink()
130
+ except FileNotFoundError:
131
+ pass
132
+ except OSError:
133
+ print("Error: could not remove the reasoning-effort state file.", file=sys.stderr)
134
+ return 2
135
+ print("Reasoning effort cleared.")
136
+ return 0
137
+
138
+
139
+ def show_status(session_dir: Path) -> int:
140
+ """Print the stored session level, or the empty-state message.
141
+
142
+ Args:
143
+ session_dir: A validated session directory.
144
+
145
+ Returns:
146
+ Process exit code 0.
147
+ """
148
+ level = None
149
+ try:
150
+ payload = json.loads(state_path(session_dir).read_text(encoding="utf-8"))
151
+ if (
152
+ isinstance(payload, dict)
153
+ and payload.get("schema") == 1
154
+ and payload.get("reasoning") in EFFORT_LEVELS
155
+ ):
156
+ level = payload["reasoning"]
157
+ except (OSError, json.JSONDecodeError):
158
+ level = None
159
+ if level is None:
160
+ print("No session reasoning effort set.")
161
+ else:
162
+ print(f"Session reasoning effort: {level}")
163
+ return 0
164
+
165
+
166
+ def parse_args() -> argparse.Namespace:
167
+ """Parse the command-line arguments.
168
+
169
+ Returns:
170
+ The parsed argparse namespace.
171
+ """
172
+ parser = argparse.ArgumentParser(description=__doc__)
173
+ sub = parser.add_subparsers(dest="command", required=True)
174
+
175
+ set_parser = sub.add_parser("set", help="record a session reasoning level")
176
+ set_parser.add_argument("--dir", required=True, help="session temp directory")
177
+ set_parser.add_argument("--reasoning", required=True, help="level to record")
178
+
179
+ clear_parser = sub.add_parser("clear", help="remove the session reasoning level")
180
+ clear_parser.add_argument("--dir", required=True, help="session temp directory")
181
+
182
+ status_parser = sub.add_parser("status", help="print the session reasoning level")
183
+ status_parser.add_argument("--dir", required=True, help="session temp directory")
184
+
185
+ return parser.parse_args()
186
+
187
+
188
+ def main() -> int:
189
+ """Run the requested command.
190
+
191
+ Returns:
192
+ Process exit code: 0 on success, 2 on invalid input.
193
+ """
194
+ args = parse_args()
195
+ session_dir = resolve_session_dir(args.dir)
196
+ if session_dir is None:
197
+ print(
198
+ "Error: --dir must be an existing session directory inside the CodeOps temp root.",
199
+ file=sys.stderr,
200
+ )
201
+ return 2
202
+ if args.command == "set":
203
+ if args.reasoning not in EFFORT_LEVELS:
204
+ print(
205
+ "Error: --reasoning must be one of: " + ", ".join(EFFORT_LEVELS) + ".",
206
+ file=sys.stderr,
207
+ )
208
+ return 2
209
+ return set_level(session_dir, args.reasoning)
210
+ if args.command == "clear":
211
+ return clear_level(session_dir)
212
+ return show_status(session_dir)
213
+
214
+
215
+ if __name__ == "__main__":
216
+ raise SystemExit(main())
@@ -36,6 +36,18 @@ do not report or implement optional additions raised during execution or review.
36
36
  Exploration may create `SE-*` proposals, but only the user may choose `Keep` and authorize a plan
37
37
  update.
38
38
 
39
+ ## Auto-effort option
40
+
41
+ If `$ARGUMENTS` contains exactly one exact standalone `--auto-effort` or `--auto-effort=<level>`
42
+ token before the first `--` sentinel, remove it before resolving targets, paths, or modes; zero
43
+ occurrences means each phase's `Reasoning:` level is printed as a suggestion only, more than one
44
+ or an invalid level is an argument error; announce
45
+ `Auto-effort active — reasoning <level> applied for this run`; then read and apply
46
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Auto-effort option. A bare
47
+ flag follows each phase's suggestion; `--auto-effort=<level>` forces that level for the whole run,
48
+ including every dispatch marker composed during it. The run clears the level before its final
49
+ summary.
50
+
39
51
  Execute the implementation plan at `plans/$ARGUMENTS/99-execution-plan.md`. The first
40
52
  argument is the feature name; an optional flag selects the commit mode.
41
53
 
@@ -70,6 +70,10 @@ scope baseline; missing mode context fails closed to strict scope.
70
70
  When opt-in outcome metrics are enabled, record only a content-free execution-stage event through
71
71
  `codeops_outcomes.py`; metrics never gate execution.
72
72
 
73
+ When the phase header carries `> **Reasoning**: <level> — <reason>`, record it as the phase's
74
+ advisory level for dispatch markers, inline reporting, and applied-level reporting. It is a
75
+ suggestion: no gate reads it, and its absence means "inherit".
76
+
73
77
  **Spec-author dispatch (profile-gated).** Tasks marked `[spec-author]` dispatch the
74
78
  spec-test-author agent — packet per `_shared/quality-profile.md` — BEFORE any implementation
75
79
  task of that phase, and the red phase is confirmed from its report. A spec test that cannot be
@@ -300,12 +304,46 @@ receives nothing else and must not need anything else:
300
304
  - the scope mode (`strict` or `explore`) and confirmed product scope baseline; missing or invalid
301
305
  scope context fails closed to strict mode. Missing or invalid original-goal or smallest-design
302
306
  context blocks dispatch;
303
- - the target file paths and the project's verify command.
307
+ - the target file paths and the project's verify command;
308
+ - the phase's reasoning marker line (`[codeops-effort: <level>]`), resolved from the run's forced
309
+ `--auto-effort=<level>` level, else the phase's `Reasoning:` suggestion; omitted when neither
310
+ exists.
304
311
 
305
312
  Excerpting owned content into a packet is the intended retrieval mechanism, not restatement. The
306
313
  quoted AR/ST/spec content is context for the executor's *understanding* — it must not surface as a
307
314
  citation in shipped code (the executor carries the same doc-standard ban and self-check).
308
315
 
316
+ **Reasoning marker.** Every dispatched unit — executor, reviewer, auditor, spec-test author,
317
+ specialist, or scout — receives one standalone marker line in its packet:
318
+
319
+ ```text
320
+ [codeops-effort: medium]
321
+ ```
322
+
323
+ Place it immediately after the `[codeops-dispatch …]` header for quality agents, or as the first
324
+ line for packets without a header. When neither the forced run level nor the phase suggestion
325
+ exists, add no marker: routing policy still applies, otherwise the child inherits the parent
326
+ variant. The marker is packet context; it never appears in shipped code comments. The
327
+ complexity-gate design challenger is excluded because its independence contract forbids extra
328
+ packet shaping.
329
+
330
+ **Applied-level reporting.** For every dispatch, report the level and its source in the dispatch
331
+ commentary, for example `Dispatch: executor — reasoning: medium (phase suggestion)`,
332
+ `— reasoning: high (routing default)`, or `— inherited`. Reporting is observational; it never
333
+ gates a dispatch.
334
+
335
+ **Inline phases.** When a phase runs inline (the default):
336
+
337
+ 1. Print `Suggested reasoning: <level> — <reason>` before the phase's first task when the phase
338
+ header carries the line.
339
+ 2. Without `--auto-effort`, change nothing else — the session keeps its own variant.
340
+ 3. With `--auto-effort`, set the session level for the phase through
341
+ `python3 "${CODEOPS_PLUGIN_ROOT}/scripts/codeops_effort.py" set --dir "$CODEOPS_TMPDIR" --reasoning <level>`
342
+ and update it when the next phase's suggestion differs; with `--auto-effort=<level>`, set that
343
+ level once and keep it for the whole run. A phase without a `Reasoning:` line leaves the
344
+ session level unchanged.
345
+ 4. When `$CODEOPS_TMPDIR` is empty or the helper fails, print an advise-only note and continue.
346
+
309
347
  **Division of labor.** The PARENT — never the executor — updates `99-execution-plan.md`
310
348
  (two-stage marks), the Progress header, and the roadmap. The executor implements task-by-task,
311
349
  runs verify per the Verify-output capture rule, and reports per task. Mark `[~]` as the executor
@@ -404,8 +442,10 @@ otherwise still `[~]` — with the progress counter and Last Updated stamp curre
404
442
  2. **🚨 First: update `99-execution-plan.md`** with ALL completed tasks (before anything else).
405
443
  3. Run the verify command (output captured per the Verify-output capture rule).
406
444
  4. Handle the commit per the active commit mode (see [commit-modes.md](commit-modes.md)).
407
- 5. Report the session summary (must include `Execution Plan Updated: ✅`).
408
- 6. **Cleanup:** delete every temporary artifact this session created — verify logs under
445
+ 5. If this run set a session level through `--auto-effort`, clear it before the summary:
446
+ `python3 "${CODEOPS_PLUGIN_ROOT}/scripts/codeops_effort.py" clear --dir "$CODEOPS_TMPDIR"`.
447
+ 6. Report the session summary (must include `Execution Plan Updated: ✅`).
448
+ 7. **Cleanup:** delete every temporary artifact this session created — verify logs under
409
449
  `$CODEOPS_TMPDIR`, scratch directories, temporary diffs — per
410
450
  `_shared/workspace-hygiene.md`, and report `Cleanup: done` or name what was kept and why.
411
451
 
@@ -21,6 +21,16 @@ implementation work begins.
21
21
 
22
22
  > **CodeOps Artifact Schema**: 1
23
23
 
24
+ ## Auto-effort option
25
+
26
+ If `$ARGUMENTS` contains exactly one exact standalone `--auto-effort` or `--auto-effort=<level>`
27
+ token before the first `--` sentinel, remove it before resolving targets, paths, or modes; zero
28
+ occurrences means this skill's recommended level (`high`) is printed as a suggestion only, more
29
+ than one or an invalid level is an argument error; announce
30
+ `Auto-effort active — reasoning <level> applied for this run`; then read and apply
31
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Auto-effort option. The
32
+ run clears the level before its final summary.
33
+
24
34
  ## Core Directive
25
35
 
26
36
  > **Interview the user relentlessly about every aspect of the topic until you reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one.**
@@ -24,6 +24,16 @@ If `$ARGUMENTS` contains exactly one exact standalone `--explore-scope` token be
24
24
  do not report or plan optional additions. Exploration may propose `SE-*` items but
25
25
  never accepts them; only the user may choose `Keep`.
26
26
 
27
+ ## Auto-effort option
28
+
29
+ If `$ARGUMENTS` contains exactly one exact standalone `--auto-effort` or `--auto-effort=<level>`
30
+ token before the first `--` sentinel, remove it before resolving targets, paths, or modes; zero
31
+ occurrences means this skill's recommended level (`high`) is printed as a suggestion only, more
32
+ than one or an invalid level is an argument error; announce
33
+ `Auto-effort active — reasoning <level> applied for this run`; then read and apply
34
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Auto-effort option. The
35
+ run clears the level before its final summary.
36
+
27
37
  ## Plan readiness proof
28
38
 
29
39
  A plan is not ready merely because its documents exist. Before presenting it as executable,
@@ -106,6 +116,7 @@ Mini-plan shape:
106
116
 
107
117
  > **Type**: Task (lightweight) · **Feature**: search · **CodeOps Artifact Schema**: 1
108
118
  > **Progress**: 0/3 tasks (0%)
119
+ > **Reasoning**: medium — bounded UI change reusing existing patterns
109
120
 
110
121
  ## Objective
111
122
  Debounce the search box to 300ms to cut redundant queries.
@@ -121,6 +132,12 @@ shared debounce subsystem.
121
132
  **Verify**: [project verify command]
122
133
  ```
123
134
 
135
+ Every phase and every task mini-plan carries the same advisory
136
+ `> **Reasoning**: <level> — <reason>` line. Derive the level from the signals the plan already
137
+ records, using [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Plan
138
+ suggestion derivation, and keep the reason a short plain-language phrase. The line is a
139
+ suggestion: no gate reads it, deleting it is valid, and its absence means "inherit".
140
+
124
141
  Everything below is the **full feature** pipeline; skip it for tasks.
125
142
 
126
143
  ## Project configuration
@@ -458,6 +458,8 @@ task-size criteria in [quality-checklist.md](quality-checklist.md))
458
458
  > committed, staged, unstaged, and untracked phase-start state)_
459
459
  > **Lenses**: [add-on lenses — include this line only when the target repo carries a quality
460
460
  > profile; informational: activation stays profile-driven]
461
+ > **Reasoning**: [advisory level — `low`, `medium`, `high`, or `max`, with a one-line reason;
462
+ > derivation in `../../_shared/reasoning-effort.md` §Plan suggestion derivation]
461
463
 
462
464
  ### Step 1.1: [Step Objective]
463
465
 
@@ -27,6 +27,16 @@ requirements decisions under that policy and propagate its downward-only context
27
27
  invoked supported children; an unsupported child fails closed. This mode does not grant action permission or scope expansion. **Normal mode:** without the exact token, every material choice
28
28
  still requires an explicit user decision; historical delegated records must not infer delegated authority.
29
29
 
30
+ ## Auto-effort option
31
+
32
+ If `$ARGUMENTS` contains exactly one exact standalone `--auto-effort` or `--auto-effort=<level>`
33
+ token before the first `--` sentinel, remove it before resolving targets, paths, or modes; zero
34
+ occurrences means this skill's recommended level (`high`) is printed as a suggestion only, more
35
+ than one or an invalid level is an argument error; announce
36
+ `Auto-effort active — reasoning <level> applied for this run`; then read and apply
37
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Auto-effort option. The
38
+ run clears the level before its final summary.
39
+
30
40
  Transform a rough project idea into a structured, complete set of formal
31
41
  **requirement documents (RDs)**. This skill is upstream of, and independent
32
42
  from, the make-plan skill — neither requires the other.
@@ -33,6 +33,16 @@ If `$ARGUMENTS` contains exactly one exact standalone `--explore-scope` token be
33
33
  do not report optional additions as findings or suggestions. Exploration records them
34
34
  as separate `SE-*` proposals; finding resolution never chooses `Keep`.
35
35
 
36
+ ## Auto-effort option
37
+
38
+ If `$ARGUMENTS` contains exactly one exact standalone `--auto-effort` or `--auto-effort=<level>`
39
+ token before the first `--` sentinel, remove it before resolving targets, paths, or modes; zero
40
+ occurrences means this skill's recommended level (`high`, or `max` with `--thorough`) is printed
41
+ as a suggestion only, more than one or an invalid level is an argument error; announce
42
+ `Auto-effort active — reasoning <level> applied for this run`; then read and apply
43
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Auto-effort option. The
44
+ run clears the level before its final summary.
45
+
36
46
  Run a rigorous quality audit of the artifact named in `$ARGUMENTS`, **grounded in the actual
37
47
  codebase**. Find every issue, ambiguity, contradiction, gap, and risk; verify every claim and
38
48
  assumption against the real code; present each finding with options + a recommendation; iterate
@@ -20,6 +20,16 @@ description: >-
20
20
 
21
21
  > **CodeOps Artifact Schema**: 1
22
22
 
23
+ ## Auto-effort option
24
+
25
+ If `$ARGUMENTS` contains exactly one exact standalone `--auto-effort` or `--auto-effort=<level>`
26
+ token before the first `--` sentinel, remove it before resolving targets, paths, or modes; zero
27
+ occurrences means this skill's recommended level (`high`) is printed as a suggestion only, more
28
+ than one or an invalid level is an argument error; announce
29
+ `Auto-effort active — reasoning <level> applied for this run`; then read and apply
30
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Auto-effort option. The
31
+ run clears the level before its final summary.
32
+
23
33
  Analyze an existing codebase — any language, any framework — and produce a
24
34
  structured **reconstruction brief** that can be fed to the make-requirements
25
35
  skill to generate formal requirement documents capable of rebuilding the entire
@@ -50,6 +50,8 @@ Present:
50
50
 
51
51
  - detected domains and concrete evidence;
52
52
  - phase tag → capability/effort policy;
53
+ - reasoning-effort defaults per role when risk signals justify them (see
54
+ [routing.md](routing.md) §Reasoning effort policy);
53
55
  - required specialist reviewers;
54
56
  - proposed concurrency limit;
55
57
  - whether custom TOML agents add value over dynamic packets; and
@@ -27,7 +27,7 @@ CodeOps routing lives under the optional `routing` and `quality` fields in `code
27
27
 
28
28
  Allowed effort values follow the active OpenCode release. Prefer `medium` for bounded reconnaissance, `high` for correctness/security review, and higher supported levels only for genuinely demanding semantic or architectural work.
29
29
 
30
- An optional per-role `reasoning` field sets the provider reasoning-effort passthrough (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`). Generated project specialists default to `max` through their brief or the embedded `reasoningEffort`; a routing value wins over the brief. A model that rejects the option is overridden here — there is no automatic provider-capability detection.
30
+ An optional per-role `reasoning` field sets the provider reasoning-effort passthrough (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`). Generated project specialists default to `max` through their brief or the embedded `reasoningEffort`; a routing value wins over the brief and is embedded without provider-capability detection. At runtime the plugin applies a level only when the active model exposes a matching variant; an unsupported value leaves the request unchanged (see the policy below).
31
31
 
32
32
  ```json
33
33
  "roles": {
@@ -35,6 +35,21 @@ An optional per-role `reasoning` field sets the provider reasoning-effort passth
35
35
  }
36
36
  ```
37
37
 
38
+ ## Reasoning effort policy
39
+
40
+ A role's `reasoning` entry is a project default, not the only source. The runtime resolution
41
+ order is `dispatch marker > session auto-effort > routing role default > inherit parent variant`
42
+ (see [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md)). A dispatch marker
43
+ comes from an execution plan's per-phase suggestion; a session level comes from an explicit
44
+ `--auto-effort` run. Both override the routing default for their scope, and routing applies only
45
+ when a role entry exists — with no entry the child inherits the parent variant.
46
+
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.
52
+
38
53
  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.
39
54
 
40
55
  Reviewer selection is driven by risk tags:
@@ -7,6 +7,16 @@ description: Upgrade an existing CodeOps requirements set, specification, plan,
7
7
 
8
8
  The current CodeOps artifact schema is `1`. Historical Claude CodeOps `3.x` stamps describe the producing skill release, not this schema. Treat them as legacy input requiring assessment, not as numeric predecessors of schema 1.
9
9
 
10
+ ## Auto-effort option
11
+
12
+ If `$ARGUMENTS` contains exactly one exact standalone `--auto-effort` or `--auto-effort=<level>`
13
+ token before the first `--` sentinel, remove it before resolving targets, paths, or modes; zero
14
+ occurrences means this skill's recommended level (`high`) is printed as a suggestion only, more
15
+ than one or an invalid level is an argument error; announce
16
+ `Auto-effort active — reasoning <level> applied for this run`; then read and apply
17
+ [../../_shared/reasoning-effort.md](../../_shared/reasoning-effort.md) §Auto-effort option. The
18
+ run clears the level before its final summary.
19
+
10
20
  ## Scope
11
21
 
12
22
  Upgrade content and structure in place. Layout moves belong to `setup-codeops`. Never combine a layout migration and semantic/schema upgrade into one irreversible operation.