Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions .github/workflows/opencode2-activation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ on:
pull_request:
paths:
- "src/source/opencode2/**"
- "src/source/runtime/**"
- "commands/**"
- ".github/workflows/opencode2-activation.yml"
push:
branches:
- main
paths:
- "src/source/opencode2/**"
- "src/source/runtime/**"
- "commands/**"
- ".github/workflows/opencode2-activation.yml"
workflow_dispatch:

Expand Down Expand Up @@ -99,21 +103,17 @@ jobs:
EOF
git -C "$project" add .opencode/plugins/opencode-loop-v2-probe.js
git -C "$project" commit --quiet -m "init"
git -C "$project" rev-parse --show-toplevel
cat "$plugin_dir/opencode-loop-v2-probe.js"

- name: Verify dependency-free setup runs in real OpenCode 2 host
shell: bash
run: |
project="$RUNNER_TEMP/opencode-loop-v2-activation"
marker="$project/v2-activation-marker.json"
rm -f "$marker"

opencode2 api get /api/command -H "x-opencode-directory: $project" > "$RUNNER_TEMP/opencode2-command.json" 2>&1 || true
if [ ! -f "$marker" ]; then
opencode2 api get /api/plugin -H "x-opencode-directory: $project" > "$RUNNER_TEMP/opencode2-plugin.json" 2>&1 || true
fi

for attempt in $(seq 1 50); do
if [ -f "$marker" ]; then
cat "$marker"
Expand All @@ -122,14 +122,66 @@ jobs:
fi
sleep 0.2
done

echo "OpenCode 2 did not execute the local plugin setup hook." >&2
echo "--- /api/command ---" >&2
cat "$RUNNER_TEMP/opencode2-command.json" >&2 || true
echo "--- /api/plugin ---" >&2
cat "$RUNNER_TEMP/opencode2-plugin.json" >&2 || true
exit 1

- name: Prepare real experimental Loop V2 adapter
shell: bash
run: |
project="$RUNNER_TEMP/opencode-loop-v2-real-adapter"
plugin_dir="$project/.opencode/plugins"
command_dir="$project/.opencode/commands"
mkdir -p "$plugin_dir" "$command_dir"
cp "$GITHUB_WORKSPACE"/commands/*.md "$command_dir"/
git init --quiet "$project"
git -C "$project" config user.email "opencode-loop-ci@example.invalid"
git -C "$project" config user.name "OpenCode Loop CI"
export LOOP_V2_PLUGIN_FILE="$plugin_dir/opencode-loop-v2-real.js"
node --input-type=module <<'EOF'
import { writeFile } from "node:fs/promises"
import { pathToFileURL } from "node:url"
import path from "node:path"
const source = pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, "src/source/opencode2/experimental.js")).href
const content = [
`import plugin from ${JSON.stringify(source)}`,
`import { writeFile } from "node:fs/promises"`,
`const marker = new URL("../../v2-real-adapter-marker.json", import.meta.url)`,
`export default {`,
` id: plugin.id,`,
` async setup(ctx) {`,
` await plugin.setup(ctx)`,
` await writeFile(marker, JSON.stringify({ id: plugin.id, activated: true }, null, 2), "utf8")`,
` },`,
`}`,
].join("\n") + "\n"
await writeFile(process.env.LOOP_V2_PLUGIN_FILE, content, "utf8")
EOF
git -C "$project" add .opencode
git -C "$project" commit --quiet -m "init"

- name: Verify real Loop V2 adapter setup and command discovery
shell: bash
run: |
project="$RUNNER_TEMP/opencode-loop-v2-real-adapter"
marker="$project/v2-real-adapter-marker.json"
command_json="$RUNNER_TEMP/opencode2-real-command.json"
plugin_json="$RUNNER_TEMP/opencode2-real-plugin.json"
rm -f "$marker"
opencode2 api get /api/command -H "x-opencode-directory: $project" > "$command_json" 2>&1 || true
opencode2 api get /api/plugin -H "x-opencode-directory: $project" > "$plugin_json" 2>&1 || true
for attempt in $(seq 1 50); do
if [ -f "$marker" ]; then break; fi
sleep 0.2
done
test -f "$marker"
cat "$marker"
cat "$command_json"
cat "$plugin_json"
node -e 'const fs=require("node:fs"); const value=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if(value.id!=="bybrawe.opencode-loop.v2.experimental" || value.activated!==true) process.exit(1)' "$marker"
node -e 'const fs=require("node:fs"); const text=fs.readFileSync(process.argv[1],"utf8"); if(!/\"loop\"/.test(text)) process.exit(1)' "$command_json"

- name: Dump OpenCode 2 logs on failure
if: failure()
shell: bash
Expand Down
48 changes: 29 additions & 19 deletions scripts/v2-plugin-sentinel-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,31 @@ assert.deepEqual(OPENCODE_LOOP_V2_RUNTIME_REQUIREMENTS, ["event.subscribe", "ses

let transforms = 0
let registered
let subscriptions = 0
async function* emptyEvents() {}

const currentContext = {
command: {
transform: async (callback) => {
transforms += 1
registered = callback
},
},
event: {
subscribe: async () => {
subscriptions += 1
return { stream: emptyEvents() }
},
},
session: {
prompt: async () => ({}),
},
}

const setupResult = await plugin.setup(currentContext)
assert.equal(setupResult, undefined)
assert.equal(transforms, 1)
assert.equal(subscriptions, 1)
assert.equal(typeof registered, "function")

const currentDraft = {
Expand All @@ -43,9 +57,9 @@ await registered(currentDraft)

assert.deepEqual(inspectOpenCode2Context(currentContext), {
commandTransform: true,
eventSubscribe: false,
eventSubscribe: true,
sessionHook: false,
sessionPrompt: false,
sessionPrompt: true,
toolTransform: false,
toolHook: false,
})
Expand All @@ -57,31 +71,27 @@ assert.deepEqual(inspectOpenCode2CommandDraft(currentDraft), {
})

const currentStatus = openCode2LoopRuntimeStatus(currentContext, currentDraft)
assert.deepEqual(currentStatus.hostBlockers, ["event.subscribe", "session.prompt"])
assert.deepEqual(currentStatus.blockers, ["event.subscribe", "session.prompt", "runtime.adapter"])
assert.equal(currentStatus.hostReady, false)
assert.deepEqual(currentStatus.hostBlockers, [])
assert.deepEqual(currentStatus.blockers, ["runtime.adapter"])
assert.equal(currentStatus.hostReady, true)
assert.equal(currentStatus.implementationReady, false)
assert.equal(currentStatus.ready, false)
assert.equal(currentStatus.commandSource, "file-definitions")

const futureContext = {
command: { transform: async () => {} },
event: { subscribe: async () => {} },
session: { prompt: async () => ({}) },
let missingEventFailed = false
try {
await plugin.setup({ session: { prompt: async () => ({}) } })
} catch (error) {
missingEventFailed = /event subscription is unavailable/.test(String(error))
}
const futureStatus = openCode2LoopRuntimeStatus(futureContext, currentDraft)
assert.deepEqual(futureStatus.hostBlockers, [])
assert.deepEqual(futureStatus.blockers, ["runtime.adapter"])
assert.equal(futureStatus.hostReady, true)
assert.equal(futureStatus.implementationReady, false)
assert.equal(futureStatus.ready, false)
assert.equal(missingEventFailed, true)

let missingCapabilityFailed = false
let missingPromptFailed = false
try {
await plugin.setup({})
await plugin.setup({ event: { subscribe: async () => ({ stream: emptyEvents() }) } })
} catch (error) {
missingCapabilityFailed = /command\.transform capability is unavailable/.test(String(error))
missingPromptFailed = /session prompt is unavailable/.test(String(error))
}
assert.equal(missingCapabilityFailed, true)
assert.equal(missingPromptFailed, true)

console.log("OpenCode 2 plugin sentinel contract passed")
23 changes: 14 additions & 9 deletions src/source/opencode2/experimental.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { define } from "@opencode-ai/plugin/v2/promise"
import { inspectOpenCode2Context } from "./capabilities.js"
import { createOpenCode2EventBridge } from "./event-bridge.js"

export const OPENCODE_LOOP_V2_PLUGIN_ID = "bybrawe.opencode-loop.v2.experimental"

export const OpenCodeLoopV2ExperimentalPlugin = define({
export async function setupOpenCodeLoopV2(ctx) {
const capabilities = inspectOpenCode2Context(ctx)
if (!capabilities.eventSubscribe) throw new Error("OpenCode 2 event subscription is unavailable")
if (!capabilities.sessionPrompt) throw new Error("OpenCode 2 session prompt is unavailable")

if (capabilities.commandTransform) await ctx.command.transform(() => {})

const bridge = createOpenCode2EventBridge({ directory: ctx?.options?.directory })
await bridge.attach(ctx.event.subscribe.bind(ctx.event))
}

export const OpenCodeLoopV2ExperimentalPlugin = Object.freeze({
id: OPENCODE_LOOP_V2_PLUGIN_ID,
async setup(ctx) {
const capabilities = inspectOpenCode2Context(ctx)
if (!capabilities.commandTransform) {
throw new Error("OpenCode 2 command.transform capability is unavailable")
}
await ctx.command.transform(() => {})
},
setup: setupOpenCodeLoopV2,
})

export default OpenCodeLoopV2ExperimentalPlugin
Loading