opencode-codeops 1.10.0 → 1.10.1

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,12 @@
2
2
 
3
3
  All notable changes to CodeOps are recorded here.
4
4
 
5
+ ## 1.10.1 — 2026-10-04
6
+
7
+ ### Features
8
+
9
+ - effort: add opt-in applied-level trace
10
+
5
11
  ## 1.10.0 — 2026-10-04
6
12
 
7
13
  ### Documentation
@@ -126,6 +126,15 @@ Semantics:
126
126
  For `exec-plan`, a bare flag follows each phase's suggestion, while `--auto-effort=<level>`
127
127
  forces that level for the whole run, including every dispatch marker composed during it.
128
128
 
129
+ ## Optional tracing
130
+
131
+ Set `CODEOPS_EFFORT_TRACE=1` (or `true`) in the environment that starts OpenCode to record what
132
+ the plugin does. One content-free JSON line is appended per marker capture and per request to
133
+ `reasoning-effort-trace.jsonl` inside the session's scratch directory. Lines carry only a
134
+ timestamp, event name, message id, agent name, level, source, and applied flag — never prompt
135
+ text or file content. The trace lives with the rest of the session scratch and is removed with
136
+ it. Tracing is off by default and never affects a request.
137
+
129
138
  ## Suggestion-only guarantee
130
139
 
131
140
  | Allowed | Forbidden |
@@ -57,6 +57,19 @@ export declare function readSessionEffort(
57
57
  base?: string
58
58
  ): EffortLevel | undefined
59
59
 
60
+ /** Check whether the optional trace environment switch is on. */
61
+ export declare function isEffortTraceEnabled(value: unknown): boolean
62
+
63
+ /** Compute the trace-file path inside the session temp directory. */
64
+ export declare function sessionEffortTracePath(sessionID?: string, base?: string): string
65
+
66
+ /** Append one content-free trace entry to the session trace file. */
67
+ export declare function appendEffortTrace(
68
+ sessionID: string | undefined,
69
+ entry: unknown,
70
+ base?: string
71
+ ): boolean
72
+
60
73
  /** Read an agent's explicit reasoning entry from the project routing config. */
61
74
  export declare function readRoutingReasoning(
62
75
  config: unknown,
@@ -19,10 +19,10 @@
19
19
  * @module lib/reasoning-effort
20
20
  */
21
21
 
22
- import { lstatSync, readFileSync } from "node:fs"
22
+ import { appendFileSync, lstatSync, readFileSync } from "node:fs"
23
23
  import { join } from "node:path"
24
24
 
25
- import { sessionTmpDir } from "./tmp-hygiene.mjs"
25
+ import { ensureSessionTmpDir, sessionTmpDir } from "./tmp-hygiene.mjs"
26
26
 
27
27
  /**
28
28
  * The four levels a plan or skill may suggest.
@@ -198,6 +198,58 @@ export function readSessionEffort(sessionID, base) {
198
198
  }
199
199
  }
200
200
 
201
+ /**
202
+ * Check whether the optional trace environment switch is on.
203
+ *
204
+ * Tracing is opt-in and diagnostic: it is enabled only when the value is
205
+ * exactly `1` or `true`, compared case-insensitively. Every other value,
206
+ * including `undefined`, disables it.
207
+ *
208
+ * @param value - Raw `CODEOPS_EFFORT_TRACE` value
209
+ * @returns True only for `"1"` or `"true"` in any case
210
+ */
211
+ export function isEffortTraceEnabled(value) {
212
+ if (typeof value !== "string") return false
213
+ const normalized = value.trim().toLowerCase()
214
+ return normalized === "1" || normalized === "true"
215
+ }
216
+
217
+ /**
218
+ * Compute the trace-file path inside the session temp directory.
219
+ *
220
+ * @param sessionID - Session identifier
221
+ * @param base - Base temp directory (injectable for tests)
222
+ * @returns Absolute path to `reasoning-effort-trace.jsonl`
223
+ */
224
+ export function sessionEffortTracePath(sessionID, base) {
225
+ return join(sessionTmpDir(sessionID, base), "reasoning-effort-trace.jsonl")
226
+ }
227
+
228
+ /**
229
+ * Append one content-free trace entry to the session trace file.
230
+ *
231
+ * The entry is serialized as one compact JSON line. Nothing here inspects or
232
+ * interprets the entry; callers must never pass prompt text, file content, or
233
+ * secrets. The session directory is created when missing. Any failure — an
234
+ * unwritable path, an unserializable entry — returns false instead of
235
+ * throwing, because tracing must never affect a request.
236
+ *
237
+ * @param sessionID - Session identifier
238
+ * @param entry - Plain, content-free record to append
239
+ * @param base - Base temp directory (injectable for tests)
240
+ * @returns True when the line was appended, false otherwise
241
+ */
242
+ export function appendEffortTrace(sessionID, entry, base) {
243
+ try {
244
+ const line = `${JSON.stringify(entry)}\n`
245
+ ensureSessionTmpDir(sessionID, base)
246
+ appendFileSync(sessionEffortTracePath(sessionID, base), line, "utf-8")
247
+ return true
248
+ } catch {
249
+ return false
250
+ }
251
+ }
252
+
201
253
  /**
202
254
  * Read an agent's explicit reasoning entry from the project routing config.
203
255
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
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
@@ -10,8 +10,12 @@ import {
10
10
  removeSessionTmpDir,
11
11
  } from "../bin/lib/tmp-hygiene.mjs"
12
12
  import {
13
+ appendEffortTrace,
13
14
  applyEffort,
14
15
  findEffortMarker,
16
+ isEffortLevel,
17
+ isEffortTraceEnabled,
18
+ isRoutingReasoning,
15
19
  readRoutingReasoning,
16
20
  readSessionEffort,
17
21
  resolveEffort,
@@ -172,6 +176,20 @@ function readRoutingConfig(directory: string): unknown {
172
176
  }
173
177
  }
174
178
 
179
+ // ---------------------------------------------------------------------------
180
+ // Helper — append one content-free trace line when the optional
181
+ // CODEOPS_EFFORT_TRACE switch is on. Tracing is diagnostic only: it never
182
+ // affects a request and swallows its own failures.
183
+ // ---------------------------------------------------------------------------
184
+ function traceEffort(
185
+ enabled: boolean,
186
+ sessionID: string,
187
+ entry: Record<string, unknown>
188
+ ): void {
189
+ if (!enabled) return
190
+ appendEffortTrace(sessionID, { ts: new Date().toISOString(), ...entry })
191
+ }
192
+
175
193
  // ---------------------------------------------------------------------------
176
194
  // CodeOps plugin for OpenCode
177
195
  // Replaces: hooks/hooks.json + hook_session_context.sh + hook_marker_guard.sh
@@ -182,6 +200,7 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
182
200
  // deduplication set for unsupported-level warnings.
183
201
  const effortMarkers = new Map<string, { sessionID: string; level: EffortLevel }>()
184
202
  const warnedEffortLevels = new Set<string>()
203
+ const effortTraceEnabled = isEffortTraceEnabled(process.env.CODEOPS_EFFORT_TRACE)
185
204
 
186
205
  return {
187
206
  // -----------------------------------------------------------------------
@@ -280,6 +299,11 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
280
299
  const level = findEffortMarker(texts)
281
300
  if (level !== undefined) {
282
301
  effortMarkers.set(output.message.id, { sessionID: input.sessionID, level })
302
+ traceEffort(effortTraceEnabled, input.sessionID, {
303
+ event: "capture",
304
+ messageID: output.message.id,
305
+ level,
306
+ })
283
307
  }
284
308
  } catch {
285
309
  await warnContentFree(client, "Could not scan a message for a reasoning-effort marker.")
@@ -298,10 +322,36 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
298
322
  const session = readSessionEffort(input.sessionID)
299
323
  const routing = readRoutingReasoning(readRoutingConfig(directory), input.agent)
300
324
  const level = resolveEffort({ marker, session, routing })
301
- if (level === undefined) return
325
+ const source = isEffortLevel(marker)
326
+ ? "marker"
327
+ : isEffortLevel(session)
328
+ ? "session"
329
+ : isRoutingReasoning(routing)
330
+ ? "routing"
331
+ : "none"
332
+ if (level === undefined) {
333
+ traceEffort(effortTraceEnabled, input.sessionID, {
334
+ event: "apply",
335
+ messageID: input.message.id,
336
+ agent: input.agent,
337
+ level: null,
338
+ source,
339
+ applied: false,
340
+ })
341
+ return
342
+ }
302
343
 
303
344
  const applied = applyEffort(output.options, level, input.model)
304
- if (applied === output.options) {
345
+ const changed = applied !== output.options
346
+ traceEffort(effortTraceEnabled, input.sessionID, {
347
+ event: "apply",
348
+ messageID: input.message.id,
349
+ agent: input.agent,
350
+ level,
351
+ source,
352
+ applied: changed,
353
+ })
354
+ if (!changed) {
305
355
  if (marker !== undefined) {
306
356
  const warningKey = `${input.sessionID}:${marker}`
307
357
  if (!warnedEffortLevels.has(warningKey)) {