Merge pull request #3125 from kapelame/audit/ecc-opencode-output-contract

fix(opencode): write hook results through the output contract
This commit is contained in:
Affaan Mustafa
2026-09-19 19:58:40 -04:00
committed by GitHub
2 changed files with 69 additions and 9 deletions
+18 -7
View File
@@ -481,7 +481,7 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
* Triggers: Before shell command execution
* Action: Sets PROJECT_ROOT, PACKAGE_MANAGER, DETECTED_LANGUAGES, ECC_VERSION
*/
"shell.env": async () => {
"shell.env": async (_input: { cwd: string }, output: { env: Record<string, string> }) => {
const env: Record<string, string> = {
ECC_VERSION: getECCVersion(),
ECC_PLUGIN: "true",
@@ -523,7 +523,8 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
env.PRIMARY_LANGUAGE = detected[0]
}
return env
// OpenCode reads the supplied output object and ignores callback return values.
output.env = { ...output.env, ...env }
},
/**
@@ -531,9 +532,12 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
* OpenCode-specific: Control context compaction behavior
*
* Triggers: Before context compaction
* Action: Push ECC context block and custom compaction prompt
* Action: Push ECC context block and compaction guidance
*/
"experimental.session.compacting": async () => {
"experimental.session.compacting": async (
_input: { sessionID: string },
output: { context: string[]; prompt?: string }
) => {
const contextBlock = [
"# ECC Context (preserve across compaction)",
"",
@@ -558,9 +562,16 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({
contextBlock.push("")
}
return {
context: contextBlock.join("\n"),
compaction_prompt: "Focus on preserving: 1) Current task status and progress, 2) Key decisions made, 3) Files created/modified, 4) Remaining work items, 5) Any security concerns flagged. Discard: verbose tool outputs, intermediate exploration, redundant file listings.",
const eccContext = [
contextBlock.join("\n"),
"Focus on preserving: 1) Current task status and progress, 2) Key decisions made, 3) Files created/modified, 4) Remaining work items, 5) Any security concerns flagged. Discard: verbose tool outputs, intermediate exploration, redundant file listings.",
]
// OpenCode requires output assignment and skips context when a prompt is set.
if (output.prompt !== undefined) {
output.prompt = [output.prompt, ...eccContext].join("\n\n")
} else {
output.context = [...output.context, ...eccContext]
}
},
+51 -2
View File
@@ -179,9 +179,14 @@ async function main() {
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
const env = await hooks["shell.env"]()
const existingEnv = Object.freeze({ EXISTING_ENV: "preserved" })
const output = { env: existingEnv }
await hooks["shell.env"]({ cwd: projectDir }, output)
const { env } = output
assert.deepStrictEqual($.calls, [], `Unexpected shell probes: ${$.calls.join(", ")}`)
assert.strictEqual(env.EXISTING_ENV, "preserved")
assert.notStrictEqual(env, existingEnv)
assert.strictEqual(env.PROJECT_ROOT, projectDir)
assert.strictEqual(env.PACKAGE_MANAGER, "pnpm")
assert.strictEqual(env.DETECTED_LANGUAGES, "typescript,python")
@@ -243,9 +248,12 @@ async function main() {
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
const env = await hooks["shell.env"]()
const output = { env: {} }
await hooks["shell.env"]({ cwd: projectDir }, output)
const { env } = output
assert.deepStrictEqual($.calls, [], `Unexpected shell probes: ${$.calls.join(", ")}`)
assert.strictEqual(env.PROJECT_ROOT, projectDir)
assert.ok(!("PACKAGE_MANAGER" in env), "Lockfile directory should not set PACKAGE_MANAGER")
assert.ok(!("DETECTED_LANGUAGES" in env), "Marker directory should not set DETECTED_LANGUAGES")
assert.ok(!("PRIMARY_LANGUAGE" in env), "Marker directory should not set PRIMARY_LANGUAGE")
@@ -254,6 +262,47 @@ async function main() {
}
},
],
[
"compacting appends ECC context without replacing the host compaction prompt",
async () => withTempProject([], async (projectDir) => {
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
const existingContext = Object.freeze(["Existing plugin context"])
const output = { context: existingContext }
await hooks["experimental.session.compacting"]({ sessionID: "session-1" }, output)
assert.strictEqual(output.context[0], "Existing plugin context")
assert.notStrictEqual(output.context, existingContext)
const prompt = output.prompt ?? ["Default compaction prompt", ...output.context].join("\n\n")
assert.ok(prompt.includes("Default compaction prompt"))
assert.ok(prompt.includes("# ECC Context"))
assert.ok(prompt.includes("Current task status and progress"))
assert.deepStrictEqual($.calls, [])
}),
],
[
"compacting appends ECC guidance to custom prompts, including an empty prompt",
async () => withTempProject([], async (projectDir) => {
const client = createClient()
const $ = createFailingShell()
const hooks = await ECCHooksPlugin({ client, $, directory: projectDir })
for (const customPrompt of ["Another plugin's custom prompt", ""]) {
const existingContext = Object.freeze(["Existing plugin context"])
const output = { context: existingContext, prompt: customPrompt }
await hooks["experimental.session.compacting"]({ sessionID: "session-1" }, output)
const prompt = output.prompt ?? ["Default compaction prompt", ...output.context].join("\n\n")
assert.ok(prompt.startsWith(`${customPrompt}\n\n`))
assert.ok(prompt.includes("# ECC Context"))
assert.ok(prompt.includes("Current task status and progress"))
assert.strictEqual(output.context, existingContext)
}
assert.deepStrictEqual($.calls, [])
}),
],
[
"permission.ask handles read-only tools correctly",
async () => withTempProject(