oh-my-opencode 4.9.0 → 4.9.2
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/.agents/skills/opencode-qa/SKILL.md +1 -0
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-branches.mjs +39 -0
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-events.mjs +106 -0
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-server.mjs +117 -0
- package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +716 -0
- package/dist/cli/doctor/checks/dependencies.d.ts +2 -2
- package/dist/cli/index.js +31742 -31476
- package/dist/cli-node/index.js +31742 -31476
- package/dist/config/schema/experimental.d.ts +1 -0
- package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
- package/dist/index.js +3067 -1344
- package/dist/oh-my-opencode.schema.json +3 -0
- package/dist/shared/internal-initiator-marker.d.ts +7 -0
- package/dist/shared/live-server-route.d.ts +24 -0
- package/dist/shared/module-resolution-failure.d.ts +7 -0
- package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +1 -0
- package/dist/testing/create-plugin-module.d.ts +4 -0
- package/package.json +12 -12
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/hooks.json +16 -16
- package/packages/omo-codex/plugin/package.json +1 -1
|
@@ -54,6 +54,7 @@ sandbox, free port, server start/stop, and an EXIT-trap cleanup). It requires
|
|
|
54
54
|
| Export a whole session as JSON | D | `scripts/export-roundtrip.sh <ses_id>` | `references/db-investigation.md` |
|
|
55
55
|
| Check the HTTP server / an endpoint | B | `scripts/server-smoke.sh` | `references/server-api.md` |
|
|
56
56
|
| Prove a hook / action / event fired | B | `scripts/sse-hook-probe.sh` | `references/events-hooks.md` |
|
|
57
|
+
| Prove serve-topology wake runner-split (reproduced/fixed) | B | `scripts/serve-wake-split-probe.sh --expect reproduced\|fixed --evidence-dir DIR` (self-test: `--self-test`; fake LLM: `scripts/lib/fake-openai-server.mjs`) | `references/events-hooks.md` |
|
|
57
58
|
| Smoke-test the TUI | C | `scripts/tui-smoke.sh` | `references/tui-tmux.md` |
|
|
58
59
|
| Write/run a test in the opencode source | - | (bun test) | `references/testing-harness.md` |
|
|
59
60
|
| Drive opencode from a Bun/TS script | - | (SDK) | `references/sdk.md` |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const branchCounts = {
|
|
2
|
+
title: 0,
|
|
3
|
+
"parent-tool-call": 0,
|
|
4
|
+
"parent-hold": 0,
|
|
5
|
+
child: 0,
|
|
6
|
+
wake: 0,
|
|
7
|
+
default: 0,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const latches = {
|
|
11
|
+
parentToolCallIssued: false,
|
|
12
|
+
parentHoldIssued: false,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function hasToolResult(inputStr) {
|
|
16
|
+
return (
|
|
17
|
+
inputStr.includes('"type":"function_call_output"') ||
|
|
18
|
+
inputStr.includes('"type": "function_call_output"') ||
|
|
19
|
+
inputStr.includes('"type":"tool_result"') ||
|
|
20
|
+
inputStr.includes('"type": "tool_result"') ||
|
|
21
|
+
inputStr.includes('"role":"tool"') ||
|
|
22
|
+
inputStr.includes('"role": "tool"')
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function selectBranch(inputStr) {
|
|
27
|
+
const isTitle = inputStr.includes("Generate a title")
|
|
28
|
+
const isSplitProbe = inputStr.includes("Run the split probe")
|
|
29
|
+
const isChild = inputStr.includes("SPLIT_CHILD_TASK")
|
|
30
|
+
const isWake = inputStr.includes("[BACKGROUND TASK")
|
|
31
|
+
const hasResult = hasToolResult(inputStr)
|
|
32
|
+
|
|
33
|
+
if (isTitle) return "title"
|
|
34
|
+
if (isChild && !isSplitProbe) return "child"
|
|
35
|
+
if (isWake) return "wake"
|
|
36
|
+
if (isSplitProbe && !hasResult && !latches.parentToolCallIssued) return "parent-tool-call"
|
|
37
|
+
if (isSplitProbe && (hasResult || latches.parentToolCallIssued) && !latches.parentHoldIssued) return "parent-hold"
|
|
38
|
+
return "default"
|
|
39
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
|
|
3
|
+
export function completedUsage() {
|
|
4
|
+
return {
|
|
5
|
+
input_tokens: 10,
|
|
6
|
+
output_tokens: 5,
|
|
7
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
8
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function sendSse(res, events) {
|
|
13
|
+
res.writeHead(200, {
|
|
14
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
15
|
+
"cache-control": "no-cache",
|
|
16
|
+
connection: "keep-alive",
|
|
17
|
+
})
|
|
18
|
+
for (const event of events) {
|
|
19
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`)
|
|
20
|
+
}
|
|
21
|
+
res.write("data: [DONE]\n\n")
|
|
22
|
+
res.end()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function textEvents(callCount, text) {
|
|
26
|
+
const id = `resp_${callCount}`
|
|
27
|
+
const item = `msg_${callCount}`
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
type: "response.created",
|
|
31
|
+
response: { id, created_at: Math.floor(Date.now() / 1000), model: "gpt-fake" },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "response.output_item.added",
|
|
35
|
+
output_index: 0,
|
|
36
|
+
item: { type: "message", id: item },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "response.output_text.delta",
|
|
40
|
+
item_id: item,
|
|
41
|
+
output_index: 0,
|
|
42
|
+
delta: text,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "response.output_item.done",
|
|
46
|
+
output_index: 0,
|
|
47
|
+
item: { type: "message", id: item },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: "response.completed",
|
|
51
|
+
response: { usage: completedUsage() },
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toolCallEvents(callCount, name, callId, argsObj) {
|
|
57
|
+
const id = `resp_${callCount}`
|
|
58
|
+
const fcId = `fc_${callCount}`
|
|
59
|
+
const argsStr = JSON.stringify(argsObj)
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
type: "response.created",
|
|
63
|
+
response: { id, created_at: Math.floor(Date.now() / 1000), model: "gpt-fake" },
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: "response.output_item.added",
|
|
67
|
+
output_index: 0,
|
|
68
|
+
item: {
|
|
69
|
+
type: "function_call",
|
|
70
|
+
id: fcId,
|
|
71
|
+
call_id: callId,
|
|
72
|
+
name,
|
|
73
|
+
arguments: "",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: "response.function_call_arguments.delta",
|
|
78
|
+
item_id: fcId,
|
|
79
|
+
output_index: 0,
|
|
80
|
+
delta: argsStr,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: "response.output_item.done",
|
|
84
|
+
output_index: 0,
|
|
85
|
+
item: {
|
|
86
|
+
type: "function_call",
|
|
87
|
+
id: fcId,
|
|
88
|
+
call_id: callId,
|
|
89
|
+
name,
|
|
90
|
+
arguments: argsStr,
|
|
91
|
+
status: "completed",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: "response.completed",
|
|
96
|
+
response: { usage: completedUsage() },
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function appendLog(logFile, line) {
|
|
102
|
+
try {
|
|
103
|
+
fs.appendFileSync(logFile, line)
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import http from "node:http"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import os from "node:os"
|
|
6
|
+
import { sendSse, textEvents, toolCallEvents, appendLog } from "./fake-openai-events.mjs"
|
|
7
|
+
import { branchCounts, latches, selectBranch } from "./fake-openai-branches.mjs"
|
|
8
|
+
|
|
9
|
+
const requestedPort = Number(process.env.FAKE_OPENAI_PORT ?? 0)
|
|
10
|
+
const logFile = process.env.FAKE_LLM_LOG ?? path.join(os.tmpdir(), "fake-llm.log")
|
|
11
|
+
|
|
12
|
+
let callCount = 0
|
|
13
|
+
|
|
14
|
+
function logBranch(branch, extra = {}) {
|
|
15
|
+
const now = new Date().toISOString()
|
|
16
|
+
const line = `[${now}] branch=${branch} call=${callCount}${Object.keys(extra).length ? " " + JSON.stringify(extra) : ""}\n`
|
|
17
|
+
appendLog(logFile, line)
|
|
18
|
+
process.stdout.write(line)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readBody(req) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const chunks = []
|
|
24
|
+
req.on("data", (chunk) => chunks.push(chunk))
|
|
25
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")))
|
|
26
|
+
req.on("error", reject)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sleep(ms) {
|
|
31
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const server = http.createServer(async (req, res) => {
|
|
35
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
36
|
+
res.writeHead(200, { "content-type": "text/plain" }).end("ok")
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (req.method !== "POST" || !req.url?.includes("/responses")) {
|
|
41
|
+
res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "not found" }))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
callCount++
|
|
46
|
+
const raw = await readBody(req)
|
|
47
|
+
let body
|
|
48
|
+
try { body = JSON.parse(raw) } catch { body = {} }
|
|
49
|
+
|
|
50
|
+
const inputStr = JSON.stringify(body.input ?? body.messages ?? body)
|
|
51
|
+
const branch = selectBranch(inputStr)
|
|
52
|
+
branchCounts[branch] = (branchCounts[branch] ?? 0) + 1
|
|
53
|
+
logBranch(branch)
|
|
54
|
+
|
|
55
|
+
if (branch === "title") {
|
|
56
|
+
sendSse(res, textEvents(callCount, "wake split probe session"))
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (branch === "child") {
|
|
61
|
+
sendSse(res, textEvents(callCount, "DONE"))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (branch === "wake") {
|
|
66
|
+
await sleep(3000)
|
|
67
|
+
sendSse(res, textEvents(callCount, `WAKE_ACK ${callCount}`))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (branch === "parent-tool-call") {
|
|
72
|
+
latches.parentToolCallIssued = true
|
|
73
|
+
sendSse(res, toolCallEvents(callCount, "task", `call_agent_${callCount}`, {
|
|
74
|
+
description: "split probe child",
|
|
75
|
+
prompt: "SPLIT_CHILD_TASK: reply exactly DONE",
|
|
76
|
+
subagent_type: "explore",
|
|
77
|
+
run_in_background: true,
|
|
78
|
+
load_skills: [],
|
|
79
|
+
}))
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (branch === "parent-hold") {
|
|
84
|
+
latches.parentHoldIssued = true
|
|
85
|
+
sendSse(res, toolCallEvents(callCount, "bash", `call_bash_${callCount}`, {
|
|
86
|
+
command: "i=0; while [ $i -lt 8 ]; do i=$((i+1)); sleep 1; done",
|
|
87
|
+
description: "hold turn",
|
|
88
|
+
}))
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (inputStr.includes("say exactly: TUI_NOREG_OK")) {
|
|
93
|
+
sendSse(res, textEvents(callCount, "TUI_NOREG_OK"))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
sendSse(res, textEvents(callCount, `fake response ${callCount}`))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
function logFinalCounts() {
|
|
100
|
+
const summary = Object.entries(branchCounts).map(([k, v]) => `${k}=${v}`).join(" ")
|
|
101
|
+
const line = `[${new Date().toISOString()}] FINAL_COUNTS ${summary}\n`
|
|
102
|
+
appendLog(logFile, line)
|
|
103
|
+
process.stdout.write(line)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
server.listen(requestedPort, "127.0.0.1", () => {
|
|
107
|
+
const addr = server.address()
|
|
108
|
+
const port = typeof addr === "object" && addr !== null ? addr.port : requestedPort
|
|
109
|
+
try {
|
|
110
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true })
|
|
111
|
+
appendLog(logFile, `[${new Date().toISOString()}] START port=${port}\n`)
|
|
112
|
+
} catch {}
|
|
113
|
+
process.stdout.write(`fake-openai listening on ${port}\n`)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
process.on("SIGTERM", () => { logFinalCounts(); server.close(() => process.exit(0)) })
|
|
117
|
+
process.on("SIGINT", () => { logFinalCounts(); server.close(() => process.exit(0)) })
|