Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ac253ff
test: add real-host queued Goal steering canary
ByBrawe Aug 17, 2026
d8d070b
test: register real-host Goal steering canary
ByBrawe Aug 17, 2026
911db32
test: run queued Goal steering on real hosts
ByBrawe Aug 17, 2026
eca19eb
test: gate queued Goal steering on generated bundle
ByBrawe Aug 17, 2026
5f9c2ec
fix: gate autonomous dispatch during queued Goal steering
ByBrawe Aug 17, 2026
192e250
ci: add temporary queued steering bundle build
ByBrawe Aug 17, 2026
6eb9bd2
chore: trigger queued steering bundle build
ByBrawe Aug 17, 2026
99eaa6a
build: regenerate queued steering bundle
github-actions[bot] Aug 17, 2026
5756145
ci: verify queued Goal steering bundle gate
ByBrawe Aug 17, 2026
6ab8583
fix: preempt Goal turns from chat message hook
ByBrawe Aug 17, 2026
b0bd67a
fix: preempt queued Goal steering before prompt dispatch
ByBrawe Aug 17, 2026
8e48926
test: cover pre-dispatch Goal steering deduplication
ByBrawe Aug 17, 2026
8901bf3
ci: add temporary pre-dispatch steering build
ByBrawe Aug 17, 2026
d0ae7ea
chore: trigger pre-dispatch steering bundle build
ByBrawe Aug 17, 2026
5ca181c
build: regenerate pre-dispatch steering bundle
github-actions[bot] Aug 17, 2026
abc5fb2
chore: remove temporary steering bundle builder
ByBrawe Aug 17, 2026
a200105
chore: remove steering bundle trigger
ByBrawe Aug 17, 2026
eb748aa
fix: preserve synthetic message guard until OpenCode assigns an id
ByBrawe Aug 17, 2026
0470777
test: cover two-stage synthetic prompt ownership
ByBrawe Aug 17, 2026
b7e9bfb
ci: add temporary two-stage steering build
ByBrawe Aug 17, 2026
0073216
chore: trigger two-stage steering bundle build
ByBrawe Aug 17, 2026
0ff4b38
build: regenerate two-stage steering bundle
github-actions[bot] Aug 17, 2026
5136c39
chore: remove temporary two-stage steering builder
ByBrawe Aug 17, 2026
121f283
chore: remove two-stage steering trigger
ByBrawe Aug 17, 2026
889d67c
ci: run generated-bundle host canary before regression suite
ByBrawe Aug 17, 2026
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
13 changes: 9 additions & 4 deletions .github/workflows/bundle-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ jobs:
- name: Build a single-file stable plugin from source
run: npm run build:plugin

- name: Verify generated entry syntax and tests
- name: Verify generated entry syntax and import
run: |
node --check src/index.js
npm test
bun -e "await import('./src/index.js')"

- name: Install current OpenCode CLI
Expand All @@ -56,8 +55,14 @@ jobs:
- name: Show OpenCode version
run: npx --no-install opencode --version

- name: Run real-host canary against generated bundle
run: node scripts/host-loop-canary.mjs
- name: Verify real-host Loop and QUEUED Goal steering against clean generated bundle
run: npm run canary:host
env:
OPENCODE_PRINT_LOGS: "1"
OPENCODE_LOG_LEVEL: DEBUG

- name: Restore locked test dependencies
run: npm ci

- name: Run generated-bundle regression suite
run: npm test
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ jobs:
- name: Show OpenCode version
run: npx --no-install opencode --version

- name: Run real-host Loop canary
run: node scripts/host-loop-canary.mjs
- name: Run real-host Loop and queued Goal steering canaries
run: npm run canary:host
env:
OPENCODE_PRINT_LOGS: "1"
OPENCODE_LOG_LEVEL: DEBUG
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
"build:plugin": "bun build src/source/v1.js --outfile=src/index.js --target=bun --format=esm --external=@opencode-ai/plugin/tool",
"build:plugin:npm": "npm run build:plugin",
"prepack": "node --check src/index.js",
"check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/opencode/command-router.js && node --check src/source/opencode/goal-commands.js && node --check src/source/opencode/loop-commands.js && node --check src/source/opencode/loop-registration.js && node --check src/source/runtime/session-activity.js && node --check src/source/runtime/session-status.js && node --check src/source/runtime/compaction.js && node --check src/source/runtime/executor.js && node --check src/source/runtime/scheduler.js && node --check src/source/runtime/goal-runtime.js && node --check src/source/runtime/goal-policy.js && node --check src/source/runtime/goal-steering.js && node --check src/source/runtime/job-workspace.js && node --check src/source/opencode2/prompt-runtime.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check scripts/install-node.mjs && node --check scripts/loopd.mjs && node --check scripts/install-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/host-adapter-contract-test.mjs && node --check scripts/command-router-test.mjs && node --check scripts/goal-command-handlers-test.mjs && node --check scripts/loop-command-handlers-test.mjs && node --check scripts/loop-registration-test.mjs && node --check scripts/session-activity-test.mjs && node --check scripts/session-status-test.mjs && node --check scripts/compaction-runtime-test.mjs && node --check scripts/executor-runtime-test.mjs && node --check scripts/scheduler-runtime-test.mjs && node --check scripts/goal-runtime-test.mjs && node --check scripts/goal-policy-test.mjs && node --check scripts/goal-steering-test.mjs && node --check scripts/job-workspace-test.mjs && node --check scripts/v2-prompt-runtime-test.mjs && node --check scripts/v2-prompt-interval-test.mjs && node --check scripts/v2-command-runtime-test.mjs && node --check scripts/v2-command-adapter-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/host-loop-canary.mjs",
"check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/opencode/command-router.js && node --check src/source/opencode/goal-commands.js && node --check src/source/opencode/loop-commands.js && node --check src/source/opencode/loop-registration.js && node --check src/source/runtime/session-activity.js && node --check src/source/runtime/session-status.js && node --check src/source/runtime/compaction.js && node --check src/source/runtime/executor.js && node --check src/source/runtime/scheduler.js && node --check src/source/runtime/goal-runtime.js && node --check src/source/runtime/goal-policy.js && node --check src/source/runtime/goal-steering.js && node --check src/source/runtime/job-workspace.js && node --check src/source/opencode2/prompt-runtime.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check scripts/install-node.mjs && node --check scripts/loopd.mjs && node --check scripts/install-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/host-adapter-contract-test.mjs && node --check scripts/command-router-test.mjs && node --check scripts/goal-command-handlers-test.mjs && node --check scripts/loop-command-handlers-test.mjs && node --check scripts/loop-registration-test.mjs && node --check scripts/session-activity-test.mjs && node --check scripts/session-status-test.mjs && node --check scripts/compaction-runtime-test.mjs && node --check scripts/executor-runtime-test.mjs && node --check scripts/scheduler-runtime-test.mjs && node --check scripts/goal-runtime-test.mjs && node --check scripts/goal-policy-test.mjs && node --check scripts/goal-steering-test.mjs && node --check scripts/job-workspace-test.mjs && node --check scripts/v2-prompt-runtime-test.mjs && node --check scripts/v2-prompt-interval-test.mjs && node --check scripts/v2-command-runtime-test.mjs && node --check scripts/v2-command-adapter-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/host-loop-canary.mjs && node --check scripts/host-goal-steering-canary.mjs",
"test": "node scripts/command-router-test.mjs && node scripts/goal-command-handlers-test.mjs && node scripts/loop-command-handlers-test.mjs && node scripts/loop-registration-test.mjs && node scripts/session-activity-test.mjs && node scripts/session-status-test.mjs && node scripts/compaction-runtime-test.mjs && node scripts/executor-runtime-test.mjs && node scripts/scheduler-runtime-test.mjs && node scripts/goal-runtime-test.mjs && node scripts/goal-policy-test.mjs && node scripts/goal-steering-test.mjs && node scripts/job-workspace-test.mjs && node scripts/v2-prompt-runtime-test.mjs && node scripts/v2-prompt-interval-test.mjs && node scripts/v2-command-runtime-test.mjs && node scripts/v2-command-adapter-test.mjs && node scripts/install-test.mjs && node scripts/loopd-test.mjs && node scripts/smoke-test.mjs && node scripts/host-adapter-contract-test.mjs && node scripts/comprehensive-watchdog.mjs",
"canary:host": "node scripts/host-loop-canary.mjs",
"canary:host": "node scripts/host-loop-canary.mjs && node scripts/host-goal-steering-canary.mjs",
"install:global": "node scripts/install-node.mjs",
"pack:zip": "node scripts/make-zip.mjs"
},
Expand Down
30 changes: 23 additions & 7 deletions scripts/goal-steering-test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from "node:assert/strict"
import { createGoalSteeringRuntime } from "../src/source/runtime/goal-steering.js"
import { guardLoopOwnedUserMessage, loopOwnedUserMessageGuardActive, clearLoopOwnedUserMessageGuard } from "../src/source/opencode/messages.js"

function goal(id = "goal-1") {
return {
Expand Down Expand Up @@ -49,17 +50,22 @@ function assistantEvent(sessionID = "ses-steering", parentID = "user-steering")
now: () => 1000,
})

const result = await runtime.handleEvent("/workspace", client, userEvent())
const result = await runtime.handleUserMessage("/workspace", client, { sessionID: "ses-steering", messageID: "user-steering" })
assert.equal(result.handled, true)
assert.equal(result.preempted, true)
assert.equal(aborts, 1, "queued user steering should abort only the active Goal turn")
assert.equal(aborts, 1, "chat.message steering should abort only the active Goal turn before the new prompt dispatches")
assert.equal(clears, 1, "the aborted Goal run must no longer be finalized as a completed loop run")
assert.equal(runtime.shouldSuppressIdle("ses-steering"), true, "abort-generated idle must be suppressed while steering is queued")
assert.deepEqual(state.jobs[0], originalGoal, "normal steering must not pause or rewrite the Goal contract")
assert.equal(logs.at(-1)?.type, "goal-user-steering")
assert.equal(logs.at(-1)?.detail?.preempted, true)

runtime.handleEvent("/workspace", client, assistantEvent())
const duplicate = await runtime.handleEvent("/workspace", client, userEvent())
assert.equal(duplicate.duplicate, true, "message.updated must not abort a steering message already handled by chat.message")
assert.equal(aborts, 1)
assert.equal(clears, 1)

await runtime.handleEvent("/workspace", client, assistantEvent())
assert.equal(runtime.shouldSuppressIdle("ses-steering"), false, "matching steering assistant turn should release idle suppression")
}

Expand All @@ -75,7 +81,7 @@ function assistantEvent(sessionID = "ses-steering", parentID = "user-steering")
isLoopOwnedUserMessage: () => false,
now: () => 2000,
})
const result = await runtime.handleEvent("/workspace", { session: { abort: async () => {} } }, userEvent("ses-idle", "user-idle"))
const result = await runtime.handleUserMessage("/workspace", { session: { abort: async () => {} } }, { sessionID: "ses-idle", messageID: "user-idle" })
assert.equal(result.handled, true)
assert.equal(result.preempted, false)
assert.equal(aborts, 0, "an idle Goal should let the foreground user turn start normally")
Expand All @@ -95,7 +101,7 @@ function assistantEvent(sessionID = "ses-steering", parentID = "user-steering")
isLoopOwnedUserMessage: () => false,
now: () => 3000,
})
const result = await runtime.handleEvent("/workspace", { session: { abort: async () => {} } }, userEvent("ses-ordinary", "user-ordinary"))
const result = await runtime.handleUserMessage("/workspace", { session: { abort: async () => {} } }, { sessionID: "ses-ordinary", messageID: "user-ordinary" })
assert.equal(result.handled, true)
assert.equal(result.preempted, false)
assert.equal(aborts, 0)
Expand All @@ -110,7 +116,7 @@ function assistantEvent(sessionID = "ses-steering", parentID = "user-steering")
appendLoopLog: async () => {},
isLoopOwnedUserMessage: () => true,
})
const result = await runtime.handleEvent("/workspace", { session: {} }, userEvent("ses-owned", "owned-message"))
const result = await runtime.handleUserMessage("/workspace", { session: {} }, { sessionID: "ses-owned", messageID: "owned-message" })
assert.equal(result.loopOwned, true)
assert.equal(reads, 0, "Loop-owned synthetic user messages must not be mistaken for human steering")
}
Expand All @@ -128,10 +134,20 @@ function assistantEvent(sessionID = "ses-steering", parentID = "user-steering")
now: () => clock,
suppressionMs: 100,
})
await runtime.handleEvent("/workspace", { session: { abort: async () => {} } }, userEvent("ses-expire", "user-expire"))
await runtime.handleUserMessage("/workspace", { session: { abort: async () => {} } }, { sessionID: "ses-expire", messageID: "user-expire" })
assert.equal(runtime.shouldSuppressIdle("ses-expire"), true)
clock += 101
assert.equal(runtime.shouldSuppressIdle("ses-expire"), false, "lost steering must not suppress autonomous continuation forever")
}

{
const sessionID = "ses-loop-owned-two-stage"
guardLoopOwnedUserMessage(sessionID)
assert.equal(loopOwnedUserMessageGuardActive(sessionID), true, "chat.message should recognize a pending synthetic prompt before OpenCode assigns its ID")
assert.equal(loopOwnedUserMessageGuardActive(sessionID, "synthetic-user-1"), true, "message.updated should consume the pending guard and bind the real synthetic message ID")
assert.equal(loopOwnedUserMessageGuardActive(sessionID, "synthetic-user-1"), true, "the bound synthetic message ID should remain Loop-owned for later duplicate events")
clearLoopOwnedUserMessageGuard(sessionID)
assert.equal(loopOwnedUserMessageGuardActive(sessionID, "synthetic-user-1"), false)
}

console.log("Goal steering runtime tests passed")
Loading