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
127 changes: 127 additions & 0 deletions packages/plugin/src/examples/goal-loop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test"
import fs from "fs/promises"
import path from "path"
import os from "os"

let tmpdir: string

function makeTool(fn) {
return { execute: fn }
}

mock.module("@opencode-ai/plugin", () => ({
tool: makeTool,
}))

const GoalLoopPlugin = (await import("./goal-loop.js")).default

function createTestPlugin() {
const hooks = GoalLoopPlugin.server({ directory: tmpdir, project: null, user: null })
return hooks.tool
}

beforeEach(async () => {
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "goal-loop-test-"))
})

afterEach(async () => {
await fs.rm(tmpdir, { recursive: true, force: true })
})

describe("goal tool", () => {
test("set and clear round-trips", async () => {
const { goal } = createTestPlugin()
const result = await goal.execute({ action: "set", condition: "say hello" })
expect(result).toContain("say hello")

const state = JSON.parse(await fs.readFile(path.join(tmpdir, ".opencode/harness/goal.json"), "utf-8"))
expect(state.active).toBe(true)
expect(state.condition).toBe("say hello")
expect(state.turns).toBe(0)

const clear = await goal.execute({ action: "clear" })
expect(clear).toBe("Goal cleared.")
})

test("status shows active goal", async () => {
const { goal } = createTestPlugin()
await goal.execute({ action: "set", condition: "check tests" })
const status = await goal.execute({ action: "status" })
expect(status).toContain("check tests")
expect(status).toContain("Turns: 0")
})

test("status when no goal", async () => {
const { goal } = createTestPlugin()
const status = await goal.execute({ action: "status" })
expect(status).toBe("No active goal.")
})

test("set without condition fails", async () => {
const { goal } = createTestPlugin()
const result = await goal.execute({ action: "set" })
expect(result).toBe("Provide a condition.")
})

test("set with maxTurns", async () => {
const { goal } = createTestPlugin()
const result = await goal.execute({ action: "set", condition: "fix bug", maxTurns: 10 })
expect(result).toContain("max 10 turns")

const state = JSON.parse(await fs.readFile(path.join(tmpdir, ".opencode/harness/goal.json"), "utf-8"))
expect(state.maxTurns).toBe(10)

const status = await goal.execute({ action: "status" })
expect(status).toContain("Max: 10")
})

test("set with oneTaskPerTurn", async () => {
const { goal } = createTestPlugin()
const result = await goal.execute({ action: "set", condition: "write tests", oneTaskPerTurn: true })
expect(result).toContain("one task per turn")

const state = JSON.parse(await fs.readFile(path.join(tmpdir, ".opencode/harness/goal.json"), "utf-8"))
expect(state.oneTaskPerTurn).toBe(true)

const status = await goal.execute({ action: "status" })
expect(status).toContain("One task/turn")
})
})

describe("loop tool", () => {
test("add and list round-trips", async () => {
const { loop } = createTestPlugin()
const result = await loop.execute({ action: "add", prompt: "check status", interval: "5m" })
expect(result).toContain("check status")
expect(result).toContain("5m")

const list = await loop.execute({ action: "list" })
expect(list).toContain("check status")
})

test("remove loop", async () => {
const { loop } = createTestPlugin()
await loop.execute({ action: "add", prompt: "check status", interval: "5m" })
const list = await loop.execute({ action: "list" })
const id = list.match(/\[(\w+)\]/)?.[1]
expect(id).toBeTruthy()

const removed = await loop.execute({ action: "remove", loopId: id })
expect(removed).toContain(id)

const afterRemove = await loop.execute({ action: "list" })
expect(afterRemove).toBe("No active loops.")
})

test("list when no loops", async () => {
const { loop } = createTestPlugin()
const list = await loop.execute({ action: "list" })
expect(list).toBe("No active loops.")
})

test("add without prompt fails", async () => {
const { loop } = createTestPlugin()
const result = await loop.execute({ action: "add" })
expect(result).toBe("Provide a prompt.")
})
})
121 changes: 121 additions & 0 deletions packages/plugin/src/examples/goal-loop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { tool } from "@opencode-ai/plugin"
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs"
import { join } from "path"

const HARNESS_DIR = ".opencode/harness"

function ensureDir(dir: string) {
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
}

function readJson<T>(file: string, fallback: T): T {
try {
const p = join(HARNESS_DIR, file)
if (existsSync(p)) return JSON.parse(readFileSync(p, "utf-8"))
} catch {}
return fallback
}

function writeJson(file: string, data: unknown) {
ensureDir(HARNESS_DIR)
writeFileSync(join(HARNESS_DIR, file), JSON.stringify(data, null, 2), "utf-8")
}

export default {
id: "goal-loop",
server: async ({ directory }: { directory: string }) => {
return {
tool: {
goal: tool({
description:
"Set, check, or clear an autonomous goal. goal({action:'set',condition:'...'}) to set. goal({action:'status'}) to check. goal({action:'clear'}) to clear.",
args: {
action: tool.schema.enum(["set", "status", "clear"]),
condition: tool.schema.string().optional(),
maxTurns: tool.schema.number().optional(),
oneTaskPerTurn: tool.schema.boolean().optional(),
},
async execute(args: {
action: "set" | "status" | "clear"
condition?: string
maxTurns?: number
oneTaskPerTurn?: boolean
}) {
if (args.action === "clear") {
writeJson("goal.json", { active: false })
return "Goal cleared."
}
if (args.action === "status") {
const g = readJson<{
condition: string
active: boolean
turns: number
maxTurns?: number
oneTaskPerTurn?: boolean
startedAt: string
} | null>("goal.json", null)
if (!g || !g.active) return "No active goal."
const limit = g.maxTurns ? ` | Max: ${g.maxTurns}` : ""
const taskMode = g.oneTaskPerTurn ? " | One task/turn" : ""
return `Goal: "${g.condition}" | Turns: ${g.turns || 0}${limit}${taskMode} | Since: ${g.startedAt}`
}
if (!args.condition) return "Provide a condition."
const goal = {
condition: args.condition,
active: true,
turns: 0,
maxTurns: args.maxTurns || null,
oneTaskPerTurn: args.oneTaskPerTurn || false,
startedAt: new Date().toISOString(),
}
writeJson("goal.json", goal)
const extras: string[] = []
if (goal.maxTurns) extras.push(`max ${goal.maxTurns} turns`)
if (goal.oneTaskPerTurn) extras.push("one task per turn")
const hint = extras.length ? ` (${extras.join(", ")})` : ""
return `Goal set: "${goal.condition}"${hint}. Work toward it. Call goal({action:'clear'}) when done.`
},
}),

loop: tool({
description:
"Schedule a recurring prompt. loop({action:'add',prompt:'...',interval:'5m'}) to add. loop({action:'list'}) to list. loop({action:'remove',loopId:'...'}) to remove.",
args: {
action: tool.schema.enum(["add", "list", "remove"]),
prompt: tool.schema.string().optional(),
interval: tool.schema.string().optional(),
loopId: tool.schema.string().optional(),
},
async execute(args: {
action: "add" | "list" | "remove"
prompt?: string
interval?: string
loopId?: string
}) {
if (args.action === "list") {
const loops = readJson<Array<{ id: string; interval: string; prompt: string }>>("loops.json", [])
if (!loops.length) return "No active loops."
return loops.map((l) => `[${l.id}] ${l.interval}: "${l.prompt}"`).join("\n")
}
if (args.action === "remove") {
let loops = readJson<Array<{ id: string }>>("loops.json", [])
loops = loops.filter((l) => l.id !== args.loopId)
writeJson("loops.json", loops)
return `Removed loop ${args.loopId}.`
}
if (!args.prompt) return "Provide a prompt."
const id = Math.random().toString(36).substring(2, 8)
const interval = args.interval || "10m"
const loops = readJson<Array<{ id: string; prompt: string; interval: string; createdAt: string }>>(
"loops.json",
[],
)
loops.push({ id, prompt: args.prompt, interval, createdAt: new Date().toISOString() })
writeJson("loops.json", loops)
return `Loop [${id}] scheduled every ${interval}: "${args.prompt}"`
},
}),
},
}
},
}
Loading