@bybrawe/opencode-loop 0.5.28 → 0.5.29

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -4
  4. package/scripts/command-lifecycle-test.mjs +47 -0
  5. package/scripts/command-router-test.mjs +175 -0
  6. package/scripts/compaction-runtime-test.mjs +128 -0
  7. package/scripts/comprehensive-test.mjs +11 -3
  8. package/scripts/executor-runtime-test.mjs +310 -0
  9. package/scripts/fixtures/opencode2-real-adapter-probe.js +70 -0
  10. package/scripts/goal-command-handlers-test.mjs +236 -0
  11. package/scripts/goal-policy-test.mjs +143 -0
  12. package/scripts/goal-runtime-test.mjs +203 -0
  13. package/scripts/goal-steering-test.mjs +153 -0
  14. package/scripts/host-goal-steering-canary.mjs +443 -0
  15. package/scripts/host-loop-canary.mjs +54 -12
  16. package/scripts/job-workspace-test.mjs +164 -0
  17. package/scripts/loop-command-handlers-test.mjs +279 -0
  18. package/scripts/loop-registration-test.mjs +220 -0
  19. package/scripts/opencode2-loop-canary.mjs +433 -0
  20. package/scripts/scheduler-runtime-test.mjs +147 -0
  21. package/scripts/session-activity-test.mjs +95 -0
  22. package/scripts/session-status-test.mjs +220 -0
  23. package/scripts/v2-command-adapter-test.mjs +113 -0
  24. package/scripts/v2-command-runtime-test.mjs +98 -0
  25. package/scripts/v2-host-contract-test.mjs +27 -0
  26. package/scripts/v2-plugin-sentinel-test.mjs +57 -8
  27. package/scripts/v2-prompt-interval-test.mjs +171 -0
  28. package/scripts/v2-prompt-runtime-test.mjs +267 -0
  29. package/scripts/v2-runtime-adapter-test.mjs +326 -0
  30. package/src/index.js +2751 -1594
  31. package/src/source/legacy-v1.js +168 -1860
  32. package/src/source/opencode/command-router.js +103 -0
  33. package/src/source/opencode/commands.js +83 -0
  34. package/src/source/opencode/goal-commands.js +107 -0
  35. package/src/source/opencode/loop-commands.js +195 -0
  36. package/src/source/opencode/loop-registration.js +100 -0
  37. package/src/source/opencode/messages.js +55 -0
  38. package/src/source/opencode2/capabilities.js +3 -1
  39. package/src/source/opencode2/commands.js +60 -0
  40. package/src/source/opencode2/event-bridge.js +19 -1
  41. package/src/source/opencode2/events.js +64 -0
  42. package/src/source/opencode2/experimental.js +32 -1
  43. package/src/source/opencode2/host-contract.js +26 -0
  44. package/src/source/opencode2/prompt-runtime.js +381 -0
  45. package/src/source/opencode2/runtime-adapter.js +56 -0
  46. package/src/source/opencode2/status.js +40 -0
  47. package/src/source/runtime/compaction.js +122 -0
  48. package/src/source/runtime/executor.js +558 -0
  49. package/src/source/runtime/goal-policy.js +77 -0
  50. package/src/source/runtime/goal-runtime.js +231 -0
  51. package/src/source/runtime/goal-steering.js +198 -0
  52. package/src/source/runtime/job-workspace.js +148 -0
  53. package/src/source/runtime/scheduler.js +212 -0
  54. package/src/source/runtime/session-activity.js +112 -0
  55. package/src/source/runtime/session-status.js +197 -0
  56. package/scripts/host-bridge-parity-test.mjs +0 -22
@@ -0,0 +1,143 @@
1
+ import assert from "node:assert/strict"
2
+ import { createGoalExecutionPolicy } from "../src/source/runtime/goal-policy.js"
3
+
4
+ function goal(id, overrides = {}) {
5
+ return {
6
+ id,
7
+ name: id,
8
+ kind: "goal",
9
+ goalStatus: "active",
10
+ enabled: true,
11
+ paused: false,
12
+ noProgressCount: 0,
13
+ goalProgress: [],
14
+ ...overrides,
15
+ }
16
+ }
17
+
18
+ assert.throws(() => createGoalExecutionPolicy({}), /runShellCommand/)
19
+
20
+ function policyHarness(shellResults = []) {
21
+ const shellCalls = []
22
+ const toasts = []
23
+ const logs = []
24
+ let clock = 1000
25
+ let shellIndex = 0
26
+ const policy = createGoalExecutionPolicy({
27
+ runShellCommand: async (command, directory, timeoutMs) => {
28
+ shellCalls.push({ command, directory, timeoutMs })
29
+ return shellResults[shellIndex++] || { code: 0, stdout: "ok", stderr: "" }
30
+ },
31
+ dangerousShell: (command) => command.includes("danger"),
32
+ toast: async (_client, message, level) => { toasts.push({ message, level }) },
33
+ appendLoopLog: async (directory, line, extra) => { logs.push({ directory, line, extra }) },
34
+ now: () => ++clock,
35
+ })
36
+ return { ...policy, shellCalls, toasts, logs }
37
+ }
38
+
39
+ {
40
+ const harness = policyHarness()
41
+ const before = goal("progress", { noProgressCount: 2 })
42
+ const after = goal("progress", {
43
+ noProgressCount: 2,
44
+ goalProgress: [{ time: "now", summary: "Implemented change" }],
45
+ })
46
+ const result = await harness.applyGoalNoProgressGuard("/work", {}, "session", after, before)
47
+ assert.equal(result.noProgressCount, 0)
48
+ assert.equal(result.lastProgressAt, 1001)
49
+ assert.equal(harness.logs.length, 0)
50
+ assert.equal(harness.toasts.length, 0)
51
+ }
52
+
53
+ {
54
+ const harness = policyHarness()
55
+ const before = goal("stall", { maxNoProgress: 3, noProgressCount: 1 })
56
+ const current = structuredClone(before)
57
+ const result = await harness.applyGoalNoProgressGuard("/work", {}, "session", current, before)
58
+ assert.equal(result.noProgressCount, 2)
59
+ assert.equal(result.lastNoProgressAt, 1001)
60
+ assert.equal(result.paused, false)
61
+ assert.deepEqual(harness.logs.map((item) => item.line), ["goal-no-progress"])
62
+ assert.equal(harness.toasts.length, 0)
63
+ }
64
+
65
+ {
66
+ const harness = policyHarness()
67
+ const before = goal("pause", { maxNoProgress: 2, noProgressCount: 1 })
68
+ const current = structuredClone(before)
69
+ const result = await harness.applyGoalNoProgressGuard("/work", {}, "session", current, before)
70
+ assert.equal(result.noProgressCount, 2)
71
+ assert.equal(result.paused, true)
72
+ assert.equal(result.lastNoProgressAt, 1001)
73
+ assert.equal(result.goalNoProgressPausedAt, 1002)
74
+ assert.match(result.goalNoProgressReason, /Paused after 2 turn/)
75
+ assert.deepEqual(harness.logs.map((item) => item.line), ["goal-no-progress", "goal-no-progress-paused"])
76
+ assert.deepEqual(harness.toasts.map((item) => item.level), ["warning"])
77
+ }
78
+
79
+ {
80
+ const harness = policyHarness()
81
+ const unchanged = goal("skip", { goalChecks: [] })
82
+ assert.equal(await harness.runGoalChecks("/work", "session", unchanged, {}), unchanged)
83
+ assert.equal(harness.shellCalls.length, 0)
84
+ assert.equal(harness.logs.length, 0)
85
+ }
86
+
87
+ {
88
+ const harness = policyHarness()
89
+ const checked = goal("danger", { safe: true, goalChecks: ["danger command"], failureCount: 0 })
90
+ const result = await harness.runGoalChecks("/work", "session", checked, {})
91
+ assert.equal(harness.shellCalls.length, 0)
92
+ assert.equal(result.lastGoalChecks.length, 1)
93
+ assert.equal(result.lastGoalChecks[0].code, -1)
94
+ assert.equal(result.failureCount, 1)
95
+ assert.match(result.lastVerifyFailure, /Blocked dangerous command in safe mode/)
96
+ assert.deepEqual(harness.toasts.map((item) => item.level), ["warning"])
97
+ assert.deepEqual(harness.logs.map((item) => item.line), ["goal-checks"])
98
+ }
99
+
100
+ {
101
+ const harness = policyHarness([
102
+ { code: 0, stdout: "tests passed", stderr: "" },
103
+ { code: 0, stdout: "types passed", stderr: "" },
104
+ ])
105
+ const checked = goal("pass", {
106
+ safe: true,
107
+ timeoutMs: 12345,
108
+ goalChecks: ["npm test", "npm run check"],
109
+ failureCount: 2,
110
+ goalCompleteWhenChecksPass: true,
111
+ })
112
+ const result = await harness.runGoalChecks("/work", "session", checked, {})
113
+ assert.deepEqual(harness.shellCalls, [
114
+ { command: "npm test", directory: "/work", timeoutMs: 12345 },
115
+ { command: "npm run check", directory: "/work", timeoutMs: 12345 },
116
+ ])
117
+ assert.equal(result.lastGoalCheckAt, 1001)
118
+ assert.equal(result.goalChecksPassedAt, 1002)
119
+ assert.equal(result.goalCompletedAt, 1003)
120
+ assert.equal(result.failureCount, 0)
121
+ assert.equal(result.goalStatus, "completed")
122
+ assert.equal(result.enabled, false)
123
+ assert.equal(result.paused, true)
124
+ assert.equal(result.goalSummary, "All configured goal checks passed.")
125
+ assert.equal(result.goalEvidence, "npm test: exit 0\nnpm run check: exit 0")
126
+ assert.deepEqual(harness.toasts.map((item) => item.level), ["success"])
127
+ assert.deepEqual(harness.logs.map((item) => item.line), ["goal-auto-complete", "goal-checks"])
128
+ }
129
+
130
+ {
131
+ const harness = policyHarness([{ code: 2, stdout: "", stderr: "compile failed" }])
132
+ const checked = goal("fail", { goalChecks: ["npm run build"], failureCount: 4 })
133
+ const result = await harness.runGoalChecks("/work", "session", checked, {})
134
+ assert.equal(result.lastGoalCheckAt, 1001)
135
+ assert.equal(result.failureCount, 5)
136
+ assert.match(result.lastVerifyFailure, /npm run build/)
137
+ assert.match(result.lastVerifyFailure, /exit=2/)
138
+ assert.match(result.lastVerifyFailure, /compile failed/)
139
+ assert.deepEqual(harness.toasts.map((item) => item.level), ["warning"])
140
+ assert.deepEqual(harness.logs.map((item) => item.line), ["goal-checks"])
141
+ }
142
+
143
+ console.log("goal policy tests passed")
@@ -0,0 +1,203 @@
1
+ import assert from "node:assert/strict"
2
+ import { promises as fs } from "node:fs"
3
+ import os from "node:os"
4
+ import path from "node:path"
5
+ import { readState, writeState } from "../src/source/core/state.js"
6
+ import {
7
+ buildGoalPrompt,
8
+ goalReportPath,
9
+ goalReportText,
10
+ writeGoalReport,
11
+ pickGoalJob,
12
+ parseGoalToolText,
13
+ hasConcreteGoalEvidence,
14
+ goalChecksPassed,
15
+ goalRequiresPassingChecks,
16
+ goalProgressSnapshot,
17
+ goalMadeMeaningfulProgress,
18
+ setGoalComplete,
19
+ setGoalBlocked,
20
+ setGoalProgress,
21
+ } from "../src/source/runtime/goal-runtime.js"
22
+
23
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-loop-goal-runtime-"))
24
+ try {
25
+ await fs.writeFile(path.join(directory, "GOAL.md"), "Ship the tested feature.\n", "utf8")
26
+ await fs.writeFile(path.join(directory, "EXTRA.md"), "Keep compatibility intact.\n", "utf8")
27
+ await fs.writeFile(path.join(directory, "CONTEXT.md"), "Current implementation context.\n", "utf8")
28
+
29
+ const job = {
30
+ id: "goal-1",
31
+ name: "feature",
32
+ kind: "goal",
33
+ goalStatus: "active",
34
+ enabled: true,
35
+ paused: false,
36
+ createdAt: new Date(0).toISOString(),
37
+ action: "Implement the feature safely.",
38
+ goalFile: "GOAL.md",
39
+ promptFile: "EXTRA.md",
40
+ includeFiles: ["CONTEXT.md"],
41
+ goalAcceptance: ["Tests pass", "Compatibility remains intact"],
42
+ goalChecks: ["npm test"],
43
+ verifyCommand: "npm run check",
44
+ lastGoalChecks: [{ command: "npm test", code: 1 }],
45
+ lastVerifyFailure: "npm run check failed before the fix",
46
+ goalCompletionRejectedReason: "configured goal checks have not passed yet",
47
+ maxNoProgress: 3,
48
+ noProgressCount: 1,
49
+ goalProgress: [{ time: "2026-08-14T00:00:00.000Z", summary: "Inspected the failing path", next: "Patch it" }],
50
+ }
51
+
52
+ const prompt = await buildGoalPrompt(directory, job)
53
+ assert.match(prompt, /EXPERIMENTAL OPENCODE GOAL MODE ITERATION/)
54
+ assert.ok(prompt.includes(path.resolve(directory)))
55
+ assert.ok(prompt.includes("Implement the feature safely."))
56
+ assert.ok(prompt.includes("Ship the tested feature."))
57
+ assert.ok(prompt.includes("Keep compatibility intact."))
58
+ assert.ok(prompt.includes("Current implementation context."))
59
+ assert.ok(prompt.includes("1. Tests pass"))
60
+ assert.ok(prompt.includes("npm test: exit 1"))
61
+ assert.ok(prompt.includes("1/3 recent turn(s) without recorded meaningful progress"))
62
+ assert.ok(prompt.includes("Inspected the failing path"))
63
+
64
+ const report = goalReportText({
65
+ ...job,
66
+ goalSummary: "Feature implemented.",
67
+ goalEvidence: "npm test passed and src/index.js was updated",
68
+ runCount: 4,
69
+ })
70
+ assert.ok(report.includes("Status: active"))
71
+ assert.ok(report.includes("Turns: 4"))
72
+ assert.ok(report.includes("## Summary"))
73
+ assert.ok(report.includes("Feature implemented."))
74
+ assert.ok(report.includes("## Latest checks"))
75
+ assert.ok(report.includes("npm test: exit 1"))
76
+
77
+ await writeGoalReport(directory, "ses-1", { ...job, goalSummary: "Saved report." })
78
+ const reportPath = goalReportPath(directory, "ses-1", job)
79
+ assert.equal(await fs.readFile(reportPath, "utf8"), goalReportText({ ...job, goalSummary: "Saved report." }))
80
+
81
+ const inactive = { id: "goal-old", name: "old", kind: "goal", goalStatus: "blocked", enabled: false }
82
+ const active = { id: "goal-current", name: "current", kind: "goal", goalStatus: "active", enabled: true }
83
+ const state = { jobs: [{ id: "loop", kind: "prompt" }, inactive, active] }
84
+ assert.equal(pickGoalJob(state), active)
85
+ assert.equal(pickGoalJob(state, "current"), active)
86
+ assert.equal(pickGoalJob(state, "1"), inactive)
87
+ assert.equal(pickGoalJob({ jobs: [] }), undefined)
88
+
89
+ assert.deepEqual(parseGoalToolText({ summary: " done ", evidence: null }, ["summary", "evidence"]), { summary: "done", evidence: "" })
90
+ assert.equal(hasConcreteGoalEvidence("done"), false)
91
+ assert.equal(hasConcreteGoalEvidence("marked complete by /loop-goal-done because I said so"), false)
92
+ assert.equal(hasConcreteGoalEvidence("npm test passed after updating src/index.js"), true)
93
+ assert.equal(hasConcreteGoalEvidence("A sufficiently detailed explanation of the concrete implementation and verification work that was completed successfully."), true)
94
+
95
+ assert.equal(goalChecksPassed({ lastGoalChecks: [{ code: 0 }, { code: "0" }] }), true)
96
+ assert.equal(goalChecksPassed({ lastGoalChecks: [{ code: 1 }] }), false)
97
+ assert.equal(goalChecksPassed({}), false)
98
+ assert.equal(goalRequiresPassingChecks({ goalChecks: ["npm test"] }), true)
99
+ assert.equal(goalRequiresPassingChecks({ goalChecks: ["npm test"], goalRequireChecksPass: false }), false)
100
+
101
+ const before = { goalStatus: "active", goalProgress: [], goalEvidence: "", lastGoalCheckAt: 0, lastVerifyAt: 0 }
102
+ assert.equal(goalMadeMeaningfulProgress(before, { ...before }), false)
103
+ assert.equal(goalMadeMeaningfulProgress(before, { ...before, goalProgress: [{ time: "now", summary: "changed" }] }), true)
104
+ assert.equal(goalMadeMeaningfulProgress(before, { ...before, goalEvidence: "npm test passed after updating src/index.js" }), true)
105
+ assert.equal(goalMadeMeaningfulProgress(before, { ...before, lastGoalChecks: [{ code: 0 }], lastGoalCheckAt: 10 }), true)
106
+ assert.equal(goalMadeMeaningfulProgress(before, { ...before, lastVerifyAt: 10, lastVerifyCode: 0 }), true)
107
+ assert.equal(goalMadeMeaningfulProgress(before, { ...before, goalStatus: "blocked" }), true)
108
+
109
+ assert.deepEqual(goalProgressSnapshot({ goalStatus: "active", goalProgress: [1, 2], goalEvidence: "x", goalChecksPassedAt: 2, lastGoalCheckAt: 3, lastVerifyAt: 4, lastVerifyCode: "0" }), {
110
+ status: "active",
111
+ progressCount: 2,
112
+ evidence: "x",
113
+ checksPassedAt: 2,
114
+ lastGoalCheckAt: 3,
115
+ lastVerifyAt: 4,
116
+ lastVerifyCode: 0,
117
+ })
118
+
119
+ const lifecycleGoal = (id, overrides = {}) => ({
120
+ id,
121
+ name: id,
122
+ kind: "goal",
123
+ goalStatus: "active",
124
+ enabled: true,
125
+ paused: false,
126
+ createdAt: new Date(0).toISOString(),
127
+ action: `Goal ${id}`,
128
+ goalChecks: ["npm test"],
129
+ lastGoalChecks: [{ command: "npm test", code: 0 }],
130
+ noProgressCount: 2,
131
+ ...overrides,
132
+ })
133
+
134
+ const completeSession = "ses-complete"
135
+ await writeState(directory, completeSession, { jobs: [lifecycleGoal("complete-goal")] })
136
+ const rejected = await setGoalComplete(directory, completeSession, { summary: "Too early", evidence: "done" })
137
+ assert.equal(rejected.ok, false)
138
+ assert.equal(rejected.rejected, true)
139
+ let persisted = await readState(directory, completeSession)
140
+ let persistedJob = persisted.jobs[0]
141
+ assert.equal(persistedJob.goalStatus, "active")
142
+ assert.equal(persistedJob.goalCompletionRejectedCount, 1)
143
+ assert.ok(persistedJob.goalCompletionRejectedAt > 0)
144
+ assert.match(persistedJob.goalCompletionRejectedReason, /concrete evidence is required/)
145
+
146
+ const completed = await setGoalComplete(directory, completeSession, {
147
+ summary: "Feature completed",
148
+ evidence: "npm test passed after updating src/index.js",
149
+ })
150
+ assert.equal(completed.ok, true)
151
+ persisted = await readState(directory, completeSession)
152
+ persistedJob = persisted.jobs[0]
153
+ assert.equal(persistedJob.goalStatus, "completed")
154
+ assert.equal(persistedJob.enabled, false)
155
+ assert.equal(persistedJob.paused, true)
156
+ assert.equal(persistedJob.goalSummary, "Feature completed")
157
+ assert.equal(persistedJob.goalEvidence, "npm test passed after updating src/index.js")
158
+ assert.equal(persistedJob.noProgressCount, 0)
159
+ assert.ok(persistedJob.goalCompletedAt > 0)
160
+
161
+ const progressSession = "ses-progress"
162
+ await writeState(directory, progressSession, { jobs: [lifecycleGoal("progress-goal")] })
163
+ const progress = await setGoalProgress(directory, progressSession, {
164
+ summary: " Patched runtime ",
165
+ next: " Run tests ",
166
+ evidence: " src/index.js updated and npm test prepared ",
167
+ })
168
+ assert.equal(progress.ok, true)
169
+ persisted = await readState(directory, progressSession)
170
+ persistedJob = persisted.jobs[0]
171
+ assert.equal(persistedJob.goalProgress.length, 1)
172
+ assert.equal(persistedJob.goalProgress[0].summary, "Patched runtime")
173
+ assert.equal(persistedJob.goalProgress[0].next, "Run tests")
174
+ assert.equal(persistedJob.goalProgress[0].evidence, "src/index.js updated and npm test prepared")
175
+ assert.equal(persistedJob.goalEvidence, "src/index.js updated and npm test prepared")
176
+ assert.equal(persistedJob.noProgressCount, 0)
177
+ assert.ok(persistedJob.lastProgressAt > 0)
178
+
179
+ const blockedSession = "ses-blocked"
180
+ await writeState(directory, blockedSession, { jobs: [lifecycleGoal("blocked-goal")] })
181
+ const blocked = await setGoalBlocked(directory, blockedSession, {
182
+ reason: "Need credential",
183
+ needed: "API token",
184
+ evidence: "npm test cannot reach service",
185
+ })
186
+ assert.equal(blocked.ok, true)
187
+ persisted = await readState(directory, blockedSession)
188
+ persistedJob = persisted.jobs[0]
189
+ assert.equal(persistedJob.goalStatus, "blocked")
190
+ assert.equal(persistedJob.enabled, false)
191
+ assert.equal(persistedJob.paused, true)
192
+ assert.equal(persistedJob.goalBlockedReason, "Need credential\nNeeded: API token")
193
+ assert.equal(persistedJob.goalEvidence, "npm test cannot reach service")
194
+ assert.ok(persistedJob.goalBlockedAt > 0)
195
+
196
+ const missing = await setGoalProgress(directory, "ses-missing", { summary: "nothing" })
197
+ assert.equal(missing.ok, false)
198
+ assert.match(missing.message, /No active experimental goal/)
199
+
200
+ console.log("goal runtime tests passed")
201
+ } finally {
202
+ await fs.rm(directory, { recursive: true, force: true })
203
+ }
@@ -0,0 +1,153 @@
1
+ import assert from "node:assert/strict"
2
+ import { createGoalSteeringRuntime } from "../src/source/runtime/goal-steering.js"
3
+ import { guardLoopOwnedUserMessage, loopOwnedUserMessageGuardActive, clearLoopOwnedUserMessageGuard } from "../src/source/opencode/messages.js"
4
+
5
+ function goal(id = "goal-1") {
6
+ return {
7
+ id,
8
+ name: "goal",
9
+ kind: "goal",
10
+ action: "keep working",
11
+ enabled: true,
12
+ paused: false,
13
+ goalStatus: "active",
14
+ }
15
+ }
16
+
17
+ function userEvent(sessionID = "ses-steering", messageID = "user-steering") {
18
+ return {
19
+ type: "message.updated",
20
+ properties: {
21
+ info: { id: messageID, sessionID, role: "user", time: { created: 1000 } },
22
+ },
23
+ }
24
+ }
25
+
26
+ function assistantEvent(sessionID = "ses-steering", parentID = "user-steering") {
27
+ return {
28
+ type: "message.updated",
29
+ properties: {
30
+ info: { id: "assistant-steering", parentID, sessionID, role: "assistant", time: { created: 1100 } },
31
+ },
32
+ }
33
+ }
34
+
35
+ {
36
+ const originalGoal = goal()
37
+ const state = { jobs: [structuredClone(originalGoal)] }
38
+ let activeRun = { jobId: originalGoal.id, job: structuredClone(originalGoal), startedAt: 900 }
39
+ let aborts = 0
40
+ let clears = 0
41
+ const logs = []
42
+ const client = { session: { abort: async () => { aborts += 1 } } }
43
+ const runtime = createGoalSteeringRuntime({
44
+ getActiveRun: () => activeRun,
45
+ clearActiveRun: () => { clears += 1; activeRun = undefined },
46
+ readState: async () => structuredClone(state),
47
+ appendLoopLog: async (_directory, type, detail) => logs.push({ type, detail }),
48
+ fireSdk: async (_client, _label, method) => await method(),
49
+ isLoopOwnedUserMessage: () => false,
50
+ now: () => 1000,
51
+ })
52
+
53
+ const result = await runtime.handleUserMessage("/workspace", client, { sessionID: "ses-steering", messageID: "user-steering" })
54
+ assert.equal(result.handled, true)
55
+ assert.equal(result.preempted, true)
56
+ assert.equal(aborts, 1, "chat.message steering should abort only the active Goal turn before the new prompt dispatches")
57
+ assert.equal(clears, 1, "the aborted Goal run must no longer be finalized as a completed loop run")
58
+ assert.equal(runtime.shouldSuppressIdle("ses-steering"), true, "abort-generated idle must be suppressed while steering is queued")
59
+ assert.deepEqual(state.jobs[0], originalGoal, "normal steering must not pause or rewrite the Goal contract")
60
+ assert.equal(logs.at(-1)?.type, "goal-user-steering")
61
+ assert.equal(logs.at(-1)?.detail?.preempted, true)
62
+
63
+ const duplicate = await runtime.handleEvent("/workspace", client, userEvent())
64
+ assert.equal(duplicate.duplicate, true, "message.updated must not abort a steering message already handled by chat.message")
65
+ assert.equal(aborts, 1)
66
+ assert.equal(clears, 1)
67
+
68
+ await runtime.handleEvent("/workspace", client, assistantEvent())
69
+ assert.equal(runtime.shouldSuppressIdle("ses-steering"), false, "matching steering assistant turn should release idle suppression")
70
+ }
71
+
72
+ {
73
+ const currentGoal = goal()
74
+ let aborts = 0
75
+ const runtime = createGoalSteeringRuntime({
76
+ getActiveRun: () => undefined,
77
+ clearActiveRun: () => assert.fail("no active run should be cleared"),
78
+ readState: async () => ({ jobs: [structuredClone(currentGoal)] }),
79
+ appendLoopLog: async () => {},
80
+ fireSdk: async () => { aborts += 1 },
81
+ isLoopOwnedUserMessage: () => false,
82
+ now: () => 2000,
83
+ })
84
+ const result = await runtime.handleUserMessage("/workspace", { session: { abort: async () => {} } }, { sessionID: "ses-idle", messageID: "user-idle" })
85
+ assert.equal(result.handled, true)
86
+ assert.equal(result.preempted, false)
87
+ assert.equal(aborts, 0, "an idle Goal should let the foreground user turn start normally")
88
+ assert.equal(runtime.shouldSuppressIdle("ses-idle"), false)
89
+ }
90
+
91
+ {
92
+ const currentGoal = goal()
93
+ const ordinary = { id: "loop-1", kind: "prompt", enabled: true, paused: false }
94
+ let aborts = 0
95
+ const runtime = createGoalSteeringRuntime({
96
+ getActiveRun: () => ({ jobId: ordinary.id, job: ordinary, startedAt: 1 }),
97
+ clearActiveRun: () => assert.fail("ordinary Loop work must not be preempted by Goal steering logic"),
98
+ readState: async () => ({ jobs: [structuredClone(currentGoal), ordinary] }),
99
+ appendLoopLog: async () => {},
100
+ fireSdk: async () => { aborts += 1 },
101
+ isLoopOwnedUserMessage: () => false,
102
+ now: () => 3000,
103
+ })
104
+ const result = await runtime.handleUserMessage("/workspace", { session: { abort: async () => {} } }, { sessionID: "ses-ordinary", messageID: "user-ordinary" })
105
+ assert.equal(result.handled, true)
106
+ assert.equal(result.preempted, false)
107
+ assert.equal(aborts, 0)
108
+ }
109
+
110
+ {
111
+ let reads = 0
112
+ const runtime = createGoalSteeringRuntime({
113
+ getActiveRun: () => undefined,
114
+ clearActiveRun: () => {},
115
+ readState: async () => { reads += 1; return { jobs: [goal()] } },
116
+ appendLoopLog: async () => {},
117
+ isLoopOwnedUserMessage: () => true,
118
+ })
119
+ const result = await runtime.handleUserMessage("/workspace", { session: {} }, { sessionID: "ses-owned", messageID: "owned-message" })
120
+ assert.equal(result.loopOwned, true)
121
+ assert.equal(reads, 0, "Loop-owned synthetic user messages must not be mistaken for human steering")
122
+ }
123
+
124
+ {
125
+ let clock = 4000
126
+ const currentGoal = goal()
127
+ const runtime = createGoalSteeringRuntime({
128
+ getActiveRun: () => ({ jobId: currentGoal.id, job: currentGoal, startedAt: 1 }),
129
+ clearActiveRun: () => {},
130
+ readState: async () => ({ jobs: [structuredClone(currentGoal)] }),
131
+ appendLoopLog: async () => {},
132
+ fireSdk: async (_client, _label, method) => await method(),
133
+ isLoopOwnedUserMessage: () => false,
134
+ now: () => clock,
135
+ suppressionMs: 100,
136
+ })
137
+ await runtime.handleUserMessage("/workspace", { session: { abort: async () => {} } }, { sessionID: "ses-expire", messageID: "user-expire" })
138
+ assert.equal(runtime.shouldSuppressIdle("ses-expire"), true)
139
+ clock += 101
140
+ assert.equal(runtime.shouldSuppressIdle("ses-expire"), false, "lost steering must not suppress autonomous continuation forever")
141
+ }
142
+
143
+ {
144
+ const sessionID = "ses-loop-owned-two-stage"
145
+ guardLoopOwnedUserMessage(sessionID)
146
+ assert.equal(loopOwnedUserMessageGuardActive(sessionID), true, "chat.message should recognize a pending synthetic prompt before OpenCode assigns its ID")
147
+ assert.equal(loopOwnedUserMessageGuardActive(sessionID, "synthetic-user-1"), true, "message.updated should consume the pending guard and bind the real synthetic message ID")
148
+ assert.equal(loopOwnedUserMessageGuardActive(sessionID, "synthetic-user-1"), true, "the bound synthetic message ID should remain Loop-owned for later duplicate events")
149
+ clearLoopOwnedUserMessageGuard(sessionID)
150
+ assert.equal(loopOwnedUserMessageGuardActive(sessionID, "synthetic-user-1"), false)
151
+ }
152
+
153
+ console.log("Goal steering runtime tests passed")