opencode-codeops 1.10.3 → 2.0.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 +21 -0
- package/README.md +36 -20
- package/_shared/reasoning-effort.md +25 -13
- package/_shared/workspace-hygiene.md +10 -6
- package/bin/index.mjs +7 -1
- package/bin/lib/opencode-plugin.mjs +122 -24
- package/bin/lib/reasoning-effort.d.mts +11 -0
- package/bin/lib/reasoning-effort.mjs +118 -11
- package/package.json +7 -6
- package/plugin/index.ts +236 -211
- package/scripts/__pycache__/install_agents.cpython-312.pyc +0 -0
- package/skills/setup-routing/routing.md +9 -4
- package/standards/coding-standards-full.md +2 -2
package/plugin/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Plugin } from "@opencode/plugin"
|
|
2
|
+
import { randomUUID } from "node:crypto"
|
|
2
3
|
import { readFileSync } from "node:fs"
|
|
3
4
|
import { homedir } from "node:os"
|
|
4
|
-
import {
|
|
5
|
+
import { dirname, join } from "node:path"
|
|
5
6
|
import { fileURLToPath } from "node:url"
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -17,10 +18,11 @@ import {
|
|
|
17
18
|
isEffortLevel,
|
|
18
19
|
isEffortTraceEnabled,
|
|
19
20
|
isRoutingReasoning,
|
|
20
|
-
|
|
21
|
+
normalizeModelVariants,
|
|
21
22
|
readRoutingReasoning,
|
|
22
23
|
readSessionEffort,
|
|
23
24
|
resolveEffort,
|
|
25
|
+
selectEffortVariant,
|
|
24
26
|
} from "../bin/lib/reasoning-effort.mjs"
|
|
25
27
|
import type { EffortLevel } from "../bin/lib/reasoning-effort.mjs"
|
|
26
28
|
|
|
@@ -51,7 +53,8 @@ function readPackageVersion(): string {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
// ---------------------------------------------------------------------------
|
|
54
|
-
// Load standards at startup (once). Both files are injected into every
|
|
56
|
+
// Load standards at startup (once). Both files are injected into every request
|
|
57
|
+
// as system instructions, so they also survive context compaction.
|
|
55
58
|
// ---------------------------------------------------------------------------
|
|
56
59
|
const codingStandards = readFileSync(
|
|
57
60
|
join(PACKAGE_ROOT, "standards", "coding-standards.md"),
|
|
@@ -64,50 +67,49 @@ const outputStyle = readFileSync(
|
|
|
64
67
|
const standardsText = `${codingStandards}\n\n${outputStyle}`
|
|
65
68
|
|
|
66
69
|
// ---------------------------------------------------------------------------
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
70
|
+
// OpenCode 2 does not expose a session identifier to shell hooks, so the plugin
|
|
71
|
+
// owns one scratch directory per plugin runtime instead of one per session.
|
|
72
|
+
// Skills and subagents still receive it as CODEOPS_TMPDIR and delete their own
|
|
73
|
+
// scratch; the plugin removes the runtime directory on unload and sweeps
|
|
74
|
+
// directories abandoned by earlier runs. Two sessions served by the same
|
|
75
|
+
// runtime share this directory; see _shared/workspace-hygiene.md.
|
|
71
76
|
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
77
|
+
const runtimeID = `runtime-${randomUUID()}`
|
|
78
|
+
|
|
79
|
+
/** Heading prefixed to the standards block injected into compaction summaries. */
|
|
80
|
+
const STANDARDS_HEADING = "## CodeOps Standards (always active)"
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
83
|
+
* Check whether a value is a plain object (not an array, class instance, or
|
|
84
|
+
* `null`). Used to read unknown hook payloads without unsafe casts.
|
|
85
|
+
*
|
|
86
|
+
* @param value - Value to inspect
|
|
87
|
+
* @returns True for `{}`-shaped objects
|
|
77
88
|
*/
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
sweptStaleTmpDirs = true
|
|
81
|
-
try {
|
|
82
|
-
cleanStaleTmpDirs()
|
|
83
|
-
} catch {
|
|
84
|
-
// Best effort only.
|
|
85
|
-
}
|
|
89
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
90
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await client.session.prompt({
|
|
97
|
-
path: { id: sessionId },
|
|
98
|
-
body: {
|
|
99
|
-
noReply: true,
|
|
100
|
-
parts: [{ type: "text", text: standardsText }],
|
|
101
|
-
},
|
|
102
|
-
})
|
|
93
|
+
/**
|
|
94
|
+
* Log one content-free warning. Logging is best effort: a failed log must never
|
|
95
|
+
* break a request.
|
|
96
|
+
*
|
|
97
|
+
* @param message - Warning text that never contains prompt or file content
|
|
98
|
+
*/
|
|
99
|
+
function warnContentFree(message: string): void {
|
|
100
|
+
console.warn(`CodeOps: ${message}`)
|
|
103
101
|
}
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Read the version recorded in an installed skills marker, if any.
|
|
105
|
+
*
|
|
106
|
+
* The marker (`.opencode-codeops.json`) is written by the skills installer.
|
|
107
|
+
* Its absence means the skills are not managed — for example a development
|
|
108
|
+
* symlink — so there is no version to compare against.
|
|
109
|
+
*
|
|
110
|
+
* @param skillsDir - Directory that may hold the installer marker
|
|
111
|
+
* @returns The recorded version, or `undefined`
|
|
112
|
+
*/
|
|
111
113
|
function installedSkillsVersion(skillsDir: string): string | undefined {
|
|
112
114
|
try {
|
|
113
115
|
const marker = JSON.parse(
|
|
@@ -119,16 +121,15 @@ function installedSkillsVersion(skillsDir: string): string | undefined {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
): Promise<void> {
|
|
124
|
+
/**
|
|
125
|
+
* Warn (non-blocking) when the installed skills were written by a different
|
|
126
|
+
* CodeOps version than this plugin. The plugin and the files are installed by
|
|
127
|
+
* separate commands, so their versions can drift; a mismatch usually means the
|
|
128
|
+
* skills need `npx opencode-codeops update` again.
|
|
129
|
+
*
|
|
130
|
+
* @param directory - Project directory whose local skills should be checked
|
|
131
|
+
*/
|
|
132
|
+
function warnOnVersionSkew(directory: string): void {
|
|
132
133
|
const skillsDirs = [
|
|
133
134
|
join(homedir(), ".config", "opencode", "skills"),
|
|
134
135
|
join(directory, ".opencode", "skills"),
|
|
@@ -138,38 +139,21 @@ async function warnOnVersionSkew(
|
|
|
138
139
|
const installed = installedSkillsVersion(skillsDir)
|
|
139
140
|
if (!installed || installed === packageVersion) continue
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`CodeOps skills at ${skillsDir} are version ${installed}, ` +
|
|
147
|
-
`but the plugin is version ${packageVersion}. ` +
|
|
148
|
-
`Run \`npx opencode-codeops@${packageVersion} update\` to match them.`,
|
|
149
|
-
},
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
// Helper — log one content-free warning. Logging is best effort: a failed log
|
|
156
|
-
// must never break a request.
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
async function warnContentFree(
|
|
159
|
-
client: Parameters<Plugin>[0]["client"],
|
|
160
|
-
message: string
|
|
161
|
-
): Promise<void> {
|
|
162
|
-
try {
|
|
163
|
-
await client.app.log({ body: { service: "codeops", level: "warn", message } })
|
|
164
|
-
} catch {
|
|
165
|
-
// Best effort only.
|
|
142
|
+
warnContentFree(
|
|
143
|
+
`CodeOps skills at ${skillsDir} are version ${installed}, ` +
|
|
144
|
+
`but the plugin is version ${packageVersion}. ` +
|
|
145
|
+
`Run \`npx opencode-codeops@${packageVersion} update\` to match them.`
|
|
146
|
+
)
|
|
166
147
|
}
|
|
167
148
|
}
|
|
168
149
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Read the project routing config fresh on every request, so an edit applies
|
|
152
|
+
* without restarting the session. Any failure means "no routing".
|
|
153
|
+
*
|
|
154
|
+
* @param directory - Project directory holding `codeops/codeops.json`
|
|
155
|
+
* @returns The parsed config, or an empty object
|
|
156
|
+
*/
|
|
173
157
|
function readRoutingConfig(directory: string): unknown {
|
|
174
158
|
try {
|
|
175
159
|
return JSON.parse(readFileSync(join(directory, "codeops", "codeops.json"), "utf8"))
|
|
@@ -178,149 +162,139 @@ function readRoutingConfig(directory: string): unknown {
|
|
|
178
162
|
}
|
|
179
163
|
}
|
|
180
164
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Append one content-free trace line when the optional CODEOPS_EFFORT_TRACE
|
|
167
|
+
* switch is on. Tracing is diagnostic only: it never affects a request and
|
|
168
|
+
* swallows its own failures.
|
|
169
|
+
*
|
|
170
|
+
* @param enabled - Whether tracing is enabled for this runtime
|
|
171
|
+
* @param sessionID - Session the entry belongs to
|
|
172
|
+
* @param entry - Content-free trace record
|
|
173
|
+
*/
|
|
186
174
|
function traceEffort(
|
|
187
175
|
enabled: boolean,
|
|
188
176
|
sessionID: string,
|
|
189
177
|
entry: Record<string, unknown>
|
|
190
178
|
): void {
|
|
191
179
|
if (!enabled) return
|
|
192
|
-
appendEffortTrace(
|
|
180
|
+
appendEffortTrace(runtimeID, { ts: new Date().toISOString(), sessionID, ...entry })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Look up the active model's variant record from the model registry.
|
|
185
|
+
*
|
|
186
|
+
* OpenCode 2 request hooks carry only a model reference (provider and model
|
|
187
|
+
* id), while the provider-specific option sets live on the registered model as
|
|
188
|
+
* a `variants` array. Any lookup failure means "no variants", which leaves the
|
|
189
|
+
* request unchanged.
|
|
190
|
+
*
|
|
191
|
+
* @param ctx - Plugin context whose model registry is queried
|
|
192
|
+
* @param ref - Model reference from the request hook
|
|
193
|
+
* @returns A variant record keyed by variant id, or an empty record
|
|
194
|
+
*/
|
|
195
|
+
async function readModelVariants(
|
|
196
|
+
ctx: Plugin.Context,
|
|
197
|
+
ref: { providerID: string; id: string }
|
|
198
|
+
): Promise<Record<string, unknown>> {
|
|
199
|
+
try {
|
|
200
|
+
const listed = await ctx.model.list()
|
|
201
|
+
const info = listed.data.find(
|
|
202
|
+
(model) => model.providerID === ref.providerID && model.id === ref.id
|
|
203
|
+
)
|
|
204
|
+
if (info === undefined) return {}
|
|
205
|
+
return normalizeModelVariants(info.variants)
|
|
206
|
+
} catch {
|
|
207
|
+
return {}
|
|
208
|
+
}
|
|
193
209
|
}
|
|
194
210
|
|
|
195
211
|
// ---------------------------------------------------------------------------
|
|
196
|
-
// CodeOps plugin for OpenCode
|
|
212
|
+
// CodeOps plugin for OpenCode 2
|
|
197
213
|
// Replaces: hooks/hooks.json + hook_session_context.sh + hook_marker_guard.sh
|
|
198
214
|
// ---------------------------------------------------------------------------
|
|
199
|
-
export
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const effortMarkers = new Map<string, EffortLevel>()
|
|
204
|
-
const warnedEffortLevels = new Set<string>()
|
|
205
|
-
const effortTraceEnabled = isEffortTraceEnabled(process.env.CODEOPS_EFFORT_TRACE)
|
|
215
|
+
export default Plugin.define({
|
|
216
|
+
id: "opencode-codeops",
|
|
217
|
+
async setup(ctx) {
|
|
218
|
+
const directory = ctx.location.directory
|
|
206
219
|
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// Also the hygiene lifecycle: a new top-level session sweeps abandoned
|
|
214
|
-
// temp directories once, and a deleted session's temp directory is removed.
|
|
215
|
-
// -----------------------------------------------------------------------
|
|
216
|
-
event: async ({ event }) => {
|
|
217
|
-
if (event.type === "session.created") {
|
|
218
|
-
const info = (event.properties as { info: { id: string; parentID?: string } }).info
|
|
219
|
-
await injectStandards(client, info.id)
|
|
220
|
-
await warnOnVersionSkew(client, directory)
|
|
221
|
-
if (!info.parentID) sweepStaleTmpDirsOnce()
|
|
222
|
-
} else if (event.type === "session.deleted") {
|
|
223
|
-
const info = (event.properties as { info: { id: string } }).info
|
|
224
|
-
removeSessionTmpDir(info.id)
|
|
225
|
-
try {
|
|
226
|
-
effortMarkers.delete(info.id)
|
|
227
|
-
} catch {
|
|
228
|
-
await warnContentFree(client, "Could not clear captured reasoning-effort markers.")
|
|
229
|
-
}
|
|
230
|
-
} else if (event.type === "session.compacted") {
|
|
231
|
-
const sessionId: string = (event.properties as { sessionID: string }).sessionID
|
|
232
|
-
await injectStandards(client, sessionId)
|
|
233
|
-
}
|
|
234
|
-
},
|
|
220
|
+
// Reasoning-effort state lives for the lifetime of this plugin instance:
|
|
221
|
+
// the latest dispatch-marker level per session, plus a deduplication set
|
|
222
|
+
// for unsupported-level warnings.
|
|
223
|
+
const effortMarkers = new Map<string, EffortLevel>()
|
|
224
|
+
const warnedEffortLevels = new Set<string>()
|
|
225
|
+
const effortTraceEnabled = isEffortTraceEnabled(process.env.CODEOPS_EFFORT_TRACE)
|
|
235
226
|
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
standardsText
|
|
244
|
-
)
|
|
245
|
-
},
|
|
227
|
+
// Sweep scratch directories abandoned by earlier interrupted runs, once.
|
|
228
|
+
// Cleanup is best effort: a failure must never block a session.
|
|
229
|
+
try {
|
|
230
|
+
cleanStaleTmpDirs()
|
|
231
|
+
} catch {
|
|
232
|
+
// Best effort only.
|
|
233
|
+
}
|
|
246
234
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"shell.env": async (input, output) => {
|
|
256
|
-
output.env.CODEOPS_PLUGIN_ROOT = PACKAGE_ROOT
|
|
235
|
+
warnOnVersionSkew(directory)
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------
|
|
238
|
+
// Session lifecycle: drop captured markers when a session is deleted, so
|
|
239
|
+
// a long-lived runtime does not remember ended sessions.
|
|
240
|
+
// ---------------------------------------------------------------------
|
|
241
|
+
const controller = new AbortController()
|
|
242
|
+
void (async () => {
|
|
257
243
|
try {
|
|
258
|
-
|
|
244
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
245
|
+
if (event.type === "session.deleted") {
|
|
246
|
+
effortMarkers.delete(event.data.sessionID)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
259
249
|
} catch {
|
|
260
|
-
//
|
|
250
|
+
// The stream ended or the plugin unloaded; nothing to do.
|
|
261
251
|
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// -----------------------------------------------------------------------
|
|
265
|
-
// Hook 5: advisory guard — warn (non-blocking) if any edit tool targets
|
|
266
|
-
// codeops/.codeops.yml, which is owned exclusively by the setup-codeops
|
|
267
|
-
// skill. Equivalent to Codex PreToolUse hook_marker_guard.sh.
|
|
268
|
-
// args are on the output parameter per the OpenCode plugin type signature.
|
|
269
|
-
// -----------------------------------------------------------------------
|
|
270
|
-
"tool.execute.before": async (input, output) => {
|
|
271
|
-
const editTools = ["write", "edit", "apply_patch"]
|
|
272
|
-
if (!editTools.includes(input.tool)) return
|
|
252
|
+
})()
|
|
273
253
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
254
|
+
// ---------------------------------------------------------------------
|
|
255
|
+
// Standards injection: add the CodeOps standards to every agent-loop
|
|
256
|
+
// request and to every compaction summary, so every agent and every turn
|
|
257
|
+
// sees them and they cannot be lost to compaction.
|
|
258
|
+
// ---------------------------------------------------------------------
|
|
259
|
+
await ctx.session.hook("context", (event) => {
|
|
260
|
+
event.system.push({ type: "text", text: standardsText })
|
|
261
|
+
})
|
|
279
262
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"owned by setup-codeops. Edit it only through the setup/migration " +
|
|
284
|
-
"workflow (run the setup-codeops skill).\n"
|
|
285
|
-
)
|
|
286
|
-
}
|
|
287
|
-
},
|
|
263
|
+
await ctx.session.hook("compaction", (event) => {
|
|
264
|
+
event.system.push({ type: "text", text: `${STANDARDS_HEADING}\n\n${standardsText}` })
|
|
265
|
+
})
|
|
288
266
|
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
"chat.message": async (input, output) => {
|
|
267
|
+
// ---------------------------------------------------------------------
|
|
268
|
+
// Effort capture: a dispatch marker travels in the incoming user prompt.
|
|
269
|
+
// It is remembered for the whole session, because the later request hook
|
|
270
|
+
// sees the same session rather than the message that carried it.
|
|
271
|
+
// ---------------------------------------------------------------------
|
|
272
|
+
await ctx.session.hook("prompt", (event) => {
|
|
296
273
|
try {
|
|
297
|
-
const
|
|
298
|
-
part?.type === "text" ? part.text : undefined
|
|
299
|
-
)
|
|
300
|
-
const level = findEffortMarker(texts)
|
|
274
|
+
const level = findEffortMarker([event.prompt.text])
|
|
301
275
|
if (level !== undefined) {
|
|
302
|
-
effortMarkers.set(
|
|
303
|
-
traceEffort(effortTraceEnabled,
|
|
276
|
+
effortMarkers.set(event.sessionID, level)
|
|
277
|
+
traceEffort(effortTraceEnabled, event.sessionID, {
|
|
304
278
|
event: "capture",
|
|
305
|
-
messageID:
|
|
279
|
+
messageID: event.messageID,
|
|
306
280
|
level,
|
|
307
281
|
})
|
|
308
282
|
}
|
|
309
283
|
} catch {
|
|
310
|
-
|
|
284
|
+
warnContentFree("Could not scan a message for a reasoning-effort marker.")
|
|
311
285
|
}
|
|
312
|
-
}
|
|
286
|
+
})
|
|
313
287
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// session flag, then routing default) and merge the model's own
|
|
317
|
-
// options. Any failure leaves the request unchanged.
|
|
318
|
-
//
|
|
319
|
-
|
|
288
|
+
// ---------------------------------------------------------------------
|
|
289
|
+
// Effort apply: resolve the request's reasoning level (dispatch marker,
|
|
290
|
+
// then session flag, then routing default) and merge the model's own
|
|
291
|
+
// variant options. Any failure leaves the request unchanged.
|
|
292
|
+
// ---------------------------------------------------------------------
|
|
293
|
+
await ctx.session.hook("context", async (event) => {
|
|
320
294
|
try {
|
|
321
|
-
const marker = effortMarkers.get(
|
|
322
|
-
const session = readSessionEffort(
|
|
323
|
-
const routing = readRoutingReasoning(readRoutingConfig(directory),
|
|
295
|
+
const marker = effortMarkers.get(event.sessionID)
|
|
296
|
+
const session = readSessionEffort(runtimeID)
|
|
297
|
+
const routing = readRoutingReasoning(readRoutingConfig(directory), event.agent)
|
|
324
298
|
const level = resolveEffort({ marker, session, routing })
|
|
325
299
|
const source = isEffortLevel(marker)
|
|
326
300
|
? "marker"
|
|
@@ -330,10 +304,9 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
330
304
|
? "routing"
|
|
331
305
|
: "none"
|
|
332
306
|
if (level === undefined) {
|
|
333
|
-
traceEffort(effortTraceEnabled,
|
|
307
|
+
traceEffort(effortTraceEnabled, event.sessionID, {
|
|
334
308
|
event: "apply",
|
|
335
|
-
|
|
336
|
-
agent: input.agent,
|
|
309
|
+
agent: event.agent,
|
|
337
310
|
level: null,
|
|
338
311
|
source,
|
|
339
312
|
applied: false,
|
|
@@ -341,36 +314,88 @@ export const CodeOpsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
341
314
|
return
|
|
342
315
|
}
|
|
343
316
|
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
317
|
+
const variants = await readModelVariants(ctx, event.model)
|
|
318
|
+
const model = { variants }
|
|
319
|
+
const applied = applyEffort(event.options, level, model)
|
|
320
|
+
const changed = applied !== event.options
|
|
321
|
+
traceEffort(effortTraceEnabled, event.sessionID, {
|
|
347
322
|
event: "apply",
|
|
348
|
-
|
|
349
|
-
agent: input.agent,
|
|
323
|
+
agent: event.agent,
|
|
350
324
|
level,
|
|
351
325
|
source,
|
|
352
326
|
applied: changed,
|
|
353
|
-
|
|
354
|
-
variantLevels: Object.keys(extractModelVariants(
|
|
327
|
+
variant: selectEffortVariant(level, model) ?? null,
|
|
328
|
+
variantLevels: Object.keys(extractModelVariants(model) ?? {}),
|
|
355
329
|
})
|
|
356
330
|
if (!changed) {
|
|
357
331
|
if (marker !== undefined) {
|
|
358
|
-
const warningKey = `${
|
|
332
|
+
const warningKey = `${event.sessionID}:${marker}`
|
|
359
333
|
if (!warnedEffortLevels.has(warningKey)) {
|
|
360
334
|
warnedEffortLevels.add(warningKey)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
`Reasoning effort ${marker} is not available for agent ${input.agent}; ` +
|
|
335
|
+
warnContentFree(
|
|
336
|
+
`Reasoning effort ${marker} is not available for agent ${event.agent}; ` +
|
|
364
337
|
"request left unchanged."
|
|
365
338
|
)
|
|
366
339
|
}
|
|
367
340
|
}
|
|
368
341
|
return
|
|
369
342
|
}
|
|
370
|
-
|
|
343
|
+
Object.assign(event.options, applied)
|
|
371
344
|
} catch {
|
|
372
|
-
|
|
345
|
+
warnContentFree("Could not apply a reasoning-effort level to a request.")
|
|
373
346
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------
|
|
350
|
+
// Shell environment: export CODEOPS_PLUGIN_ROOT so skills can call
|
|
351
|
+
// scripts as: python3 "${CODEOPS_PLUGIN_ROOT}/scripts/codeops_plan.py".
|
|
352
|
+
// CODEOPS_TMPDIR is this runtime's scratch directory; skills and agents
|
|
353
|
+
// put every scratch file there and delete it when done.
|
|
354
|
+
// ---------------------------------------------------------------------
|
|
355
|
+
await ctx.shell.hook("create.before", (event) => {
|
|
356
|
+
event.env.CODEOPS_PLUGIN_ROOT = PACKAGE_ROOT
|
|
357
|
+
try {
|
|
358
|
+
event.env.CODEOPS_TMPDIR = ensureSessionTmpDir(runtimeID)
|
|
359
|
+
} catch {
|
|
360
|
+
// Best effort: a temp-directory failure must never block a shell.
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------
|
|
365
|
+
// Advisory guard: warn (non-blocking) if any edit tool targets
|
|
366
|
+
// codeops/.codeops.yml, which is owned exclusively by the setup-codeops
|
|
367
|
+
// skill.
|
|
368
|
+
// ---------------------------------------------------------------------
|
|
369
|
+
await ctx.tool.hook("execute.before", (event) => {
|
|
370
|
+
const editTools = ["write", "edit", "apply_patch", "multiedit", "patch"]
|
|
371
|
+
if (!editTools.includes(event.tool)) return
|
|
372
|
+
|
|
373
|
+
const input = isRecord(event.input) ? event.input : undefined
|
|
374
|
+
const filePath =
|
|
375
|
+
(typeof input?.filePath === "string" ? input.filePath : undefined) ??
|
|
376
|
+
(typeof input?.path === "string" ? input.path : undefined) ??
|
|
377
|
+
""
|
|
378
|
+
|
|
379
|
+
if (filePath.includes("codeops/.codeops.yml")) {
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
"CodeOps warning: codeops/.codeops.yml is the layout marker and is " +
|
|
382
|
+
"owned by setup-codeops. Edit it only through the setup/migration " +
|
|
383
|
+
"workflow (run the setup-codeops skill).\n"
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------
|
|
389
|
+
// Plugin cleanup: stop listening for events and remove the runtime's
|
|
390
|
+
// scratch directory. Both are best effort.
|
|
391
|
+
// ---------------------------------------------------------------------
|
|
392
|
+
return () => {
|
|
393
|
+
controller.abort()
|
|
394
|
+
try {
|
|
395
|
+
removeSessionTmpDir(runtimeID)
|
|
396
|
+
} catch {
|
|
397
|
+
// Best effort only.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
})
|
|
Binary file
|
|
@@ -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`).
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
|
@@ -54,8 +54,8 @@ These apply to all code I write unless this project's `AGENTS.md` overrides a sp
|
|
|
54
54
|
|
|
55
55
|
- **Own your temporary artifacts.** Verification logs, diffs, patch files, commit-message files,
|
|
56
56
|
scratch directories, fixture trees, and downloaded artifacts are borrowed space. Put them under
|
|
57
|
-
`$CODEOPS_TMPDIR` when the CodeOps plugin exports it (one directory per
|
|
58
|
-
the OS temp directory. Never write temporary files into the repository.
|
|
57
|
+
`$CODEOPS_TMPDIR` when the CodeOps plugin exports it (one directory per OpenCode runtime);
|
|
58
|
+
otherwise use the OS temp directory. Never write temporary files into the repository.
|
|
59
59
|
- **Delete at completion.** Before reporting any task, phase, or session complete, delete every
|
|
60
60
|
temporary artifact the run created — including verification logs. If the user asked for an
|
|
61
61
|
artifact to be kept, name its exact path and keep that file only.
|