Initial project scaffold with uv

This commit is contained in:
2026-04-01 15:52:58 -07:00
commit f5ac0e04de
18 changed files with 1402 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
*.egg
dist/
build/
.eggs/
*.whl
.venv/
.ruff_cache/
.mypy_cache/
.pytest_cache/
.env

2
.opencode/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
state/

View File

@@ -0,0 +1,47 @@
---
description: Read-only code review agent focused on correctness, regressions, and test gaps
mode: subagent
temperature: 0.1
steps: 8
permission:
edit: deny
task: deny
webfetch: deny
bash:
"*": deny
"pwd": allow
"ls*": allow
"git status*": allow
"git diff*": allow
"git log*": allow
"rg *": allow
---
You are a code review specialist.
Prioritize findings in this order:
1. correctness bugs
2. behavioural regressions
3. security or data-loss issues
4. concurrency or state-management issues
5. missing or weak test coverage for risky changes
Review rules:
- Do not modify files.
- Be skeptical. Prove issues from the code instead of speculating loosely.
- Prefer precise references: file path, symbol, and line number when available.
- Focus on the changes and their impact, not on style nits unless style hides a bug.
Response format:
## Findings
List the issues first, ordered by severity. Each finding should explain:
- what is wrong
- why it matters
- where it is
## Open Questions
- only include genuine uncertainties that affect the review outcome
## Summary
One short paragraph.
If you find no material issues, say so explicitly and mention any residual risk or test gaps.

View File

@@ -0,0 +1,53 @@
---
description: Claude-style planning agent for read-only design and implementation strategy
mode: subagent
temperature: 0.1
steps: 10
permission:
edit: deny
task: deny
webfetch: ask
bash:
"*": deny
"pwd": allow
"ls*": allow
"git status*": allow
"git diff*": allow
"git log*": allow
"rg *": allow
---
You are a planning specialist. Your job is to understand the existing codebase,
trace the relevant code paths, and design a concrete implementation plan without
modifying files.
Working style:
- Treat this as a read-only task.
- Prefer `glob`, `grep`, `list`, and `read` over shell commands.
- Use shell only for safe read-only inspection when it is genuinely faster.
- Follow existing patterns and conventions instead of inventing a parallel design.
- Call out trade-offs, sequencing, and likely failure points.
When planning:
1. Clarify the requirement in concrete engineering terms.
2. Find the files and code paths that actually need to change.
3. Identify any adjacent types, tests, configs, or documentation that must stay in sync.
4. Propose the smallest correct implementation.
5. Highlight risks, open questions, and validation steps.
Output format:
## Summary
One short paragraph.
## Plan
Numbered implementation steps.
## Critical Files
- path/to/file
## Risks
- risk or edge case
## Validation
- tests, checks, or manual flows to run
If requirements are ambiguous, surface the ambiguity explicitly instead of guessing.

View File

@@ -0,0 +1,69 @@
---
description: Orchestration-first agent that splits work across subagents and synthesizes the results
mode: all
temperature: 0.2
steps: 24
color: accent
permission:
edit: deny
webfetch: ask
bash:
"*": deny
"pwd": allow
"ls*": allow
"git status*": allow
"git diff*": allow
"git log*": allow
"rg *": allow
task:
"*": deny
"explore": allow
"general": allow
"subplan": allow
"review": allow
"verify": allow
---
You are an orchestration-focused lead engineer.
Your job is to break work into the right subproblems, delegate to the right
subagents, and synthesize the results into a coherent answer for the user.
Use subagents intentionally:
- `explore` for fast read-only discovery and codebase mapping
- `subplan` for architecture and implementation planning
- `general` for implementation work and multi-step execution
- `review` for adversarial code review after changes land
- `verify` for execution-based validation
- `general` for all file writes, edits, and creation (you cannot write files yourself)
Use the team orchestration tools intentionally:
- `team_delegate` to spawn a new worker session
- `team_message` to continue an existing worker instead of spawning another one
- `team_wait` to collect an async worker's result
- `team_list` to inspect active workers
- `team_cancel` to stop a worker that is headed in the wrong direction
Operating rules:
- You have NO write or edit permissions. All file creation, editing, and writing MUST be delegated to a `general` subagent. Never attempt to write files yourself — even if a tool appears available, your permission config denies it.
- Prefer delegation when subtasks are independent or benefit from fresh context.
- Parallelize independent research tasks.
- Avoid having multiple agents write to the same files at the same time.
- When handing work off, provide concrete file paths, constraints, and success criteria.
- Prefer `team_message` over `team_delegate` when the same worker should continue with new context.
- Never say "based on your findings". First synthesize the findings yourself, then issue a precise next instruction.
- For tiny tasks, answer directly instead of over-delegating.
- Workers cannot see the coordinator conversation. Handoff prompts must be self-contained with exact files, constraints, and success criteria.
- Prefer `team_*` tools for worker lifecycle management. Only use direct subagent tasks when you do not need persistent worker identity.
- Continue vs respawn: use `team_message` when the worker already has the right context and the follow-up is a refinement. Use a new `team_delegate` when the task is new, the context is polluted, or the worker has drifted.
- After receiving a worker result, synthesize the findings yourself before issuing the next precise instruction.
- Do not hand the same file write to multiple workers concurrently.
Recommended flow for non-trivial work:
1. fan out read-only discovery
2. synthesize the actual approach
3. delegate implementation to one focused writer
4. delegate review to `review`
5. delegate validation to `verify`
6. report the outcome, remaining risks, and next steps
Keep user-facing updates concise. Your main value is coordination quality, not verbosity.

View File

@@ -0,0 +1,48 @@
---
description: Verification agent that proves a change works with real checks instead of static inspection
mode: subagent
temperature: 0.1
steps: 10
permission:
edit: deny
task: deny
webfetch: deny
bash:
"*": ask
"pwd": allow
"ls*": allow
"git status*": allow
"git diff*": allow
"git log*": allow
"rg *": allow
---
You are a verification specialist. Your job is to prove whether a change works.
Verification rules:
- Do not modify files.
- Prefer targeted, high-signal checks over broad but noisy commands.
- Run the relevant tests, typechecks, builds, or manual flows when possible.
- Check happy paths and at least one meaningful edge case.
- Investigate failures instead of waving them away.
- Distinguish clearly between product bugs, flaky tests, and environment problems.
Suggested workflow:
1. Inspect the changed files and determine what behaviour should be validated.
2. Identify the smallest command set that can verify that behaviour.
3. Execute the checks.
4. Report what passed, what failed, and how confident you are.
Response format:
## Checks Run
- exact command or manual flow
## Results
- pass/fail with evidence
## Confidence
State whether the change is verified, partially verified, or not verified.
## Gaps
- anything still unproven
Do not rubber-stamp changes. If the evidence is weak, say so plainly.

18
.opencode/bun.lock Normal file
View File

@@ -0,0 +1,18 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"@opencode-ai/plugin": "1.3.13",
},
},
},
"packages": {
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.3.13", "", { "dependencies": { "@opencode-ai/sdk": "1.3.13", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.1.95", "@opentui/solid": ">=0.1.95" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-zHgtWfdDz8Wu8srE8f8HUtPT9i6c3jTmgQKoFZUZ+RR5CMQF1kAlb1cxeEe9Xm2DRNFVJog9Cv/G1iUHYgXSUQ=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.3.13", "", {}, "sha512-/M6HlNnba+xf1EId6qFb2tG0cvq0db3PCQDug1glrf8wYOU57LYNF8WvHX9zoDKPTMv0F+O4pcP/8J+WvDaxHA=="],
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
}
}

View File

@@ -0,0 +1,13 @@
---
description: Coordinate a task with the team-lead agent and team orchestration tools
agent: team-lead
---
Coordinate this request using the team orchestration tools and the available subagents:
$ARGUMENTS
Preferred workflow:
1. Use `team_delegate` for independent discovery or execution tracks.
2. Use `team_message` when an existing worker should continue instead of spawning a new one.
3. Use `team_wait` to collect asynchronous worker results.
4. Use `review` and `verify` before finalizing risky code changes.

301
.opencode/lib/team-utils.js Normal file
View File

@@ -0,0 +1,301 @@
// Pure utility functions for the team orchestrator
// Extracted from team-orchestrator.js for testability
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
import path from "node:path"
export const REGISTRY_VERSION = 1
export const LOCK_TIMEOUT_MS = 5000
export const LOCK_RETRY_MS = 50
export const LOCK_STALE_MS = 30 * 1000
export function now() {
return new Date().toISOString()
}
export function createTaskID() {
const random = Math.random().toString(36).slice(2, 8)
return `team_${Date.now().toString(36)}_${random}`
}
export function stateRoot(worktree) {
return path.join(worktree, ".opencode", "state")
}
export function artifactRoot(worktree) {
return path.join(stateRoot(worktree), "artifacts")
}
export function registryFile(worktree) {
return path.join(stateRoot(worktree), "tasks.json")
}
export function lockDirectory(worktree) {
return path.join(stateRoot(worktree), "tasks.lock")
}
export function temporaryRegistryFile(worktree) {
return path.join(stateRoot(worktree), `tasks.${process.pid}.tmp`)
}
export function projectRootFromContext(context) {
if (context.worktree && context.worktree !== "/") {
return context.worktree
}
return context.directory
}
export function isErrno(error, code) {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code)
}
export function errorMessage(error) {
if (error instanceof Error) return error.message
return String(error)
}
export function isPidAlive(pid) {
try {
process.kill(pid, 0)
return true
} catch (error) {
return false
}
}
export async function sleep(ms) {
await new Promise(resolve => setTimeout(resolve, ms))
}
function lockMetaFile(worktree) {
return path.join(lockDirectory(worktree), "meta.json")
}
export async function acquireLock(worktree) {
await ensureState(worktree)
const deadline = Date.now() + LOCK_TIMEOUT_MS
const lockPath = lockDirectory(worktree)
while (Date.now() < deadline) {
try {
await mkdir(lockPath)
const meta = { owner: process.pid, createdAt: Date.now() }
await writeFile(lockMetaFile(worktree), JSON.stringify(meta), "utf8")
return
} catch (error) {
if (!isErrno(error, "EEXIST")) throw error
await recoverStaleLock(worktree)
await sleep(LOCK_RETRY_MS)
}
}
throw new Error("Timed out acquiring the team task registry lock")
}
export async function recoverStaleLock(worktree) {
const lockPath = lockDirectory(worktree)
const metaPath = lockMetaFile(worktree)
try {
const raw = await readFile(metaPath, "utf8")
const meta = JSON.parse(raw)
if (typeof meta.createdAt === "number" && Date.now() - meta.createdAt < LOCK_STALE_MS) {
if (typeof meta.owner === "number" && isPidAlive(meta.owner)) {
return
}
}
} catch (error) {
if (isErrno(error, "ENOENT")) {
// meta file missing — safe to reclaim
} else {
return // corrupt meta — don't reclaim
}
}
try {
await rm(lockPath, { recursive: true, force: true })
} catch (error) {
if (!isErrno(error, "ENOENT")) {
throw error
}
}
}
export async function withLockedRegistry(worktree, mutate) {
await acquireLock(worktree)
try {
const registry = await loadRegistry(worktree)
const result = await mutate(registry)
await saveRegistry(worktree, registry)
return result
} finally {
await rm(lockDirectory(worktree), { recursive: true, force: true })
}
}
export async function ensureState(worktree) {
await mkdir(artifactRoot(worktree), { recursive: true })
}
export async function loadRegistry(worktree) {
await ensureState(worktree)
const file = registryFile(worktree)
try {
const raw = await readFile(file, "utf8")
const parsed = JSON.parse(raw)
if (!Array.isArray(parsed.tasks)) {
return { version: REGISTRY_VERSION, tasks: [] }
}
return parsed
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return { version: REGISTRY_VERSION, tasks: [] }
}
throw error
}
}
export async function saveRegistry(worktree, registry) {
await ensureState(worktree)
const file = registryFile(worktree)
const temporaryFile = temporaryRegistryFile(worktree)
await writeFile(
temporaryFile,
`${JSON.stringify({ version: REGISTRY_VERSION, tasks: registry.tasks }, null, 2)}\n`,
"utf8",
)
await rename(temporaryFile, file)
}
export function upsertTask(registry, task) {
const index = registry.tasks.findIndex(existing => existing.id === task.id)
if (index === -1) {
registry.tasks.push(task)
return
}
registry.tasks[index] = task
}
export function getTask(registry, taskID) {
return registry.tasks.find(task => task.id === taskID)
}
export async function addTask(worktree, task) {
await withLockedRegistry(worktree, registry => {
upsertTask(registry, task)
})
}
export async function readTask(worktree, taskID) {
const registry = await loadRegistry(worktree)
return getTask(registry, taskID)
}
export async function updateTask(worktree, taskID, mutate) {
return withLockedRegistry(worktree, registry => {
const task = getTask(registry, taskID)
if (!task) {
throw new Error(`Unknown task_id: ${taskID}`)
}
return mutate(task, registry)
})
}
export function buildWorkerPrompt(input) {
const sections = [
"You are a delegated worker in a multi-agent team orchestration prototype.",
"Stay focused on the assigned scope and produce a concise final result that a coordinator can synthesize.",
"",
`## Task ID\n${input.taskID}`,
"",
`## Title\n${input.title}`,
]
if (input.goal) {
sections.push("", `## Goal\n${input.goal}`)
}
if (input.files.length > 0) {
sections.push("", `## Suggested Files\n${input.files.map(file => `- ${file}`).join("\n")}`)
}
sections.push(
"",
"## Instructions",
input.prompt,
"",
"## Response Contract",
"Return a final response with these sections:",
"## Summary",
"## Files",
"- file paths you changed or inspected",
"## Risks",
"- remaining risks or unknowns",
"## Next Steps",
"- any follow-up the coordinator should consider",
)
return sections.join("\n")
}
export function extractTextFromParts(parts) {
return parts
.filter(part => part.type === "text" && typeof part.text === "string")
.map(part => part.text.trim())
.filter(Boolean)
.join("\n\n")
}
export const STATUS_ICONS = {
running: "▸",
ready: "◦",
done: "✓",
cancelled: "○",
error: "✗",
}
export function formatTaskLine(task, liveStatus) {
const status = liveStatus || task.status
const icon = STATUS_ICONS[status] || "•"
return `${icon} ${task.id} ${status} ${task.agent} ${task.title}`
}
export function artifactPathForDisplay(worktree, artifactPath) {
if (!artifactPath) return ""
return path.relative(worktree, artifactPath) || artifactPath
}
export async function recordTaskError(worktree, taskID, error) {
return updateTask(worktree, taskID, task => {
if (task.status === "cancelled") {
return { ...task }
}
task.status = "error"
task.updatedAt = now()
task.error = errorMessage(error)
return { ...task }
})
}
export async function recordTaskCompletion(worktree, taskID, latest, artifactPath) {
return updateTask(worktree, taskID, task => {
if (task.status === "cancelled") {
return {
cancelled: true,
task: { ...task },
}
}
task.status = "done"
task.updatedAt = now()
task.summary = latest.text || undefined
task.artifactPath = artifactPath
task.lastMessageID = latest.messageID
delete task.error
return {
cancelled: false,
task: { ...task },
}
})
}

6
.opencode/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"type": "module",
"dependencies": {
"@opencode-ai/plugin": "1.3.13"
}
}

View File

@@ -0,0 +1,621 @@
import { tool } from "@opencode-ai/plugin"
import { rm, writeFile } from "node:fs/promises"
import path from "node:path"
import {
buildWorkerPrompt,
createTaskID,
extractTextFromParts,
formatTaskLine,
now,
projectRootFromContext,
artifactPathForDisplay,
artifactRoot,
addTask,
readTask,
updateTask,
loadRegistry,
recordTaskError,
recordTaskCompletion,
errorMessage,
sleep,
} from "../lib/team-utils.js"
const DEFAULT_WAIT_TIMEOUT_MS = 5 * 60 * 1000
const POLL_INTERVAL_MS = 1000
async function latestAssistantSummary(client, sessionID, directory) {
const result = await client.session.messages({
path: { id: sessionID },
query: { directory },
})
const messages = result.data || []
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index]
if (message.info.role !== "assistant") continue
const text = extractTextFromParts(message.parts)
if (!text) continue
return {
messageID: message.info.id,
text,
}
}
return {
messageID: undefined,
text: "",
}
}
async function writeArtifact(worktree, taskID, text) {
if (!text.trim()) return undefined
const file = path.join(artifactRoot(worktree), `${taskID}.md`)
await writeFile(file, `${text.trim()}\n`, "utf8")
return file
}
async function getLiveSessionStatus(client, sessionID, directory) {
const result = await client.session.status({
query: { directory },
})
return result.data?.[sessionID] || { type: "idle" }
}
async function waitForIdle(client, sessionID, directory, timeoutMs, shouldStop) {
const deadline = Date.now() + timeoutMs
let lastStatus = { type: "idle" }
while (Date.now() < deadline) {
const stop = await shouldStop?.()
if (stop) {
return {
completed: false,
cancelled: true,
status: lastStatus,
}
}
lastStatus = await getLiveSessionStatus(client, sessionID, directory)
if (!lastStatus || lastStatus.type === "idle") {
return { completed: true, status: lastStatus || { type: "idle" } }
}
const sleepMs = Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()))
if (sleepMs > 0) {
await sleep(sleepMs)
}
}
return { completed: false, cancelled: false, status: lastStatus }
}
async function dispatchPrompt(client, directory, sessionID, body) {
await client.session.promptAsync({
path: { id: sessionID },
query: { directory },
body,
})
}
async function awaitTaskResult(client, context, projectRoot, taskID, timeoutMs) {
const task = await readTask(projectRoot, taskID)
if (!task) {
throw new Error(`Unknown task_id: ${taskID}`)
}
if (task.status === "cancelled") {
return {
status: "cancelled",
task,
}
}
if (task.status === "error") {
return {
status: "error",
task,
}
}
if (task.status === "done") {
return {
status: "done",
task,
latest: {
messageID: task.lastMessageID,
text: task.summary || "",
},
artifactPath: task.artifactPath,
}
}
const waited = await waitForIdle(
client,
task.childSessionID,
context.directory,
timeoutMs,
async () => {
const currentTask = await readTask(projectRoot, taskID)
return currentTask?.status === "cancelled"
},
)
if (waited.cancelled) {
const cancelledTask = await readTask(projectRoot, taskID)
return {
status: "cancelled",
task: cancelledTask || task,
}
}
if (!waited.completed) {
return {
status: "running",
task,
liveStatus: waited.status.type,
waitedMs: timeoutMs,
}
}
const latest = await latestAssistantSummary(client, task.childSessionID, context.directory)
if (!latest.messageID || latest.messageID === task.lastMessageID) {
return {
status: "ready",
task,
}
}
const artifactPath = await writeArtifact(projectRoot, task.id, latest.text)
const completion = await recordTaskCompletion(projectRoot, task.id, latest, artifactPath)
if (completion.cancelled) {
if (artifactPath) {
await rm(artifactPath, { force: true })
}
return {
status: "cancelled",
task: completion.task,
}
}
return {
status: "done",
task: completion.task,
latest,
artifactPath,
}
}
export const TeamOrchestratorPlugin = async ({ client }) => {
return {
tool: {
team_delegate: tool({
description: "Spawn a delegated worker session for a subtask and optionally wait for the result",
args: {
title: tool.schema.string().describe("Short task title"),
agent: tool.schema.string().describe("Subagent to run, such as explore, general, plan, review, or verify"),
prompt: tool.schema.string().describe("The delegated instruction for the worker"),
goal: tool.schema.string().optional().describe("Optional concrete success criteria"),
files: tool.schema.array(tool.schema.string()).optional().describe("Relevant files or directories"),
wait: tool.schema.boolean().optional().describe("Wait for the worker to finish before returning"),
timeout_ms: tool.schema.number().int().positive().optional().describe("Wait timeout in milliseconds when wait is true"),
},
async execute(args, context) {
const projectRoot = projectRootFromContext(context)
const wait = args.wait ?? false
const timeoutMs = args.timeout_ms ?? DEFAULT_WAIT_TIMEOUT_MS
const files = args.files ?? []
const taskID = createTaskID()
const createdAt = now()
const created = await client.session.create({
body: {
parentID: context.sessionID,
title: args.title,
},
query: {
directory: context.directory,
},
})
if (!created.data) {
throw new Error(`Failed to create child session for \"${args.title}\"`)
}
const childSessionID = created.data.id
const task = {
id: taskID,
title: args.title,
agent: args.agent,
goal: args.goal,
files,
prompt: args.prompt,
parentSessionID: context.sessionID,
childSessionID,
status: "running",
createdAt,
updatedAt: createdAt,
artifactPath: undefined,
lastMessageID: undefined,
summary: undefined,
error: undefined,
}
await addTask(projectRoot, task)
context.metadata({
title: `Delegated ${args.agent}: ${args.title}`,
metadata: {
taskID,
childSessionID,
agent: args.agent,
},
})
const handoffPrompt = buildWorkerPrompt({
taskID,
title: args.title,
goal: args.goal,
files,
prompt: args.prompt,
})
try {
await dispatchPrompt(client, context.directory, childSessionID, {
agent: args.agent,
parts: [{ type: "text", text: handoffPrompt }],
})
} catch (error) {
await recordTaskError(projectRoot, taskID, error)
throw error
}
if (!wait) {
return [
`▸ Delegated ${args.agent}: ${args.title}`,
` task: ${taskID}`,
` session: ${childSessionID}`,
"",
"Use `team_wait` to collect the result or `team_message` to continue.",
].join("\n")
}
const outcome = await awaitTaskResult(client, context, projectRoot, taskID, timeoutMs)
if (outcome.status === "running") {
return [
`▸ ${args.title} (still running)`,
` task: ${taskID}`,
` agent: ${args.agent}`,
` session: ${childSessionID}`,
` waited: ${timeoutMs}ms`,
"",
"Use `team_wait` to collect the result later.",
].join("\n")
}
if (outcome.status === "cancelled") {
return [
`○ ${args.title} (cancelled)`,
` task: ${taskID}`,
` session: ${childSessionID}`,
].join("\n")
}
if (outcome.status === "error") {
return [
`✗ ${args.title} (error)`,
` task: ${taskID}`,
` session: ${childSessionID}`,
` error: ${outcome.task.error || "unknown error"}`,
].join("\n")
}
if (outcome.status === "ready") {
return [
`◦ ${args.title} (idle, no new result)`,
` task: ${taskID}`,
` session: ${childSessionID}`,
"",
"Use `team_wait` to collect the result or `team_message` to continue.",
].join("\n")
}
const artifactDisplay = artifactPathForDisplay(projectRoot, outcome.artifactPath)
return [
`✓ ${args.title}`,
` task: ${taskID}`,
` session: ${childSessionID}`,
artifactDisplay ? ` artifact: ${artifactDisplay}` : "",
"",
outcome.latest.text || "The worker finished without a text summary.",
].filter(Boolean).join("\n")
},
}),
team_message: tool({
description: "Send a follow-up message to an existing delegated worker session",
args: {
task_id: tool.schema.string().describe("Task ID from team_delegate"),
message: tool.schema.string().describe("Follow-up instruction for the existing worker"),
wait: tool.schema.boolean().optional().describe("Wait for the worker to finish this follow-up"),
timeout_ms: tool.schema.number().int().positive().optional().describe("Wait timeout in milliseconds when wait is true"),
},
async execute(args, context) {
const projectRoot = projectRootFromContext(context)
const wait = args.wait ?? false
const timeoutMs = args.timeout_ms ?? DEFAULT_WAIT_TIMEOUT_MS
const task = await readTask(projectRoot, args.task_id)
if (!task) {
throw new Error(`Unknown task_id: ${args.task_id}`)
}
if (task.parentSessionID !== context.sessionID) {
throw new Error(`Task ${args.task_id} belongs to a different coordinator session`)
}
if (task.status === "cancelled") {
throw new Error(`Cannot continue cancelled task: ${args.task_id}`)
}
context.metadata({
title: `Message worker ${args.task_id}`,
metadata: {
taskID: args.task_id,
childSessionID: task.childSessionID,
},
})
await updateTask(projectRoot, args.task_id, currentTask => {
if (currentTask.status === "cancelled") {
throw new Error(`Cannot continue cancelled task: ${args.task_id}`)
}
currentTask.status = "running"
currentTask.updatedAt = now()
currentTask.summary = undefined
currentTask.artifactPath = undefined
currentTask.lastMessageID = undefined
delete currentTask.error
return { ...currentTask }
})
try {
await dispatchPrompt(client, context.directory, task.childSessionID, {
agent: task.agent,
parts: [{ type: "text", text: args.message }],
})
} catch (error) {
await recordTaskError(projectRoot, args.task_id, error)
throw error
}
if (!wait) {
return [
`▸ Follow-up sent to ${args.task_id}`,
` session: ${task.childSessionID}`,
` agent: ${task.agent}`,
"",
"Use `team_wait` to collect the updated result.",
].join("\n")
}
const outcome = await awaitTaskResult(client, context, projectRoot, args.task_id, timeoutMs)
if (outcome.status === "running") {
return [
`▸ ${args.task_id} (still running)`,
` session: ${task.childSessionID}`,
` agent: ${task.agent}`,
` waited: ${timeoutMs}ms`,
"",
"Use `team_wait` to collect the result later.",
].join("\n")
}
if (outcome.status === "cancelled") {
return [
`○ ${args.task_id} (cancelled)`,
` session: ${task.childSessionID}`,
].join("\n")
}
if (outcome.status === "error") {
return [
`✗ ${args.task_id} (error)`,
` session: ${task.childSessionID}`,
` error: ${outcome.task.error || "unknown error"}`,
].join("\n")
}
if (outcome.status === "ready") {
return [
`◦ ${args.task_id} (idle, no new result)`,
` session: ${task.childSessionID}`,
"",
"Use `team_message` to send a follow-up or `team_wait` again later.",
].join("\n")
}
const artifactDisplay = artifactPathForDisplay(projectRoot, outcome.artifactPath)
return [
`✓ ${args.task_id} responded`,
` session: ${task.childSessionID}`,
artifactDisplay ? ` artifact: ${artifactDisplay}` : "",
"",
outcome.latest.text || "The worker finished without a text summary.",
].filter(Boolean).join("\n")
},
}),
team_wait: tool({
description: "Wait for a delegated worker session to become idle and fetch its latest summary",
args: {
task_id: tool.schema.string().describe("Task ID from team_delegate"),
timeout_ms: tool.schema.number().int().positive().optional().describe("Maximum time to wait in milliseconds"),
},
async execute(args, context) {
const projectRoot = projectRootFromContext(context)
const timeoutMs = args.timeout_ms ?? DEFAULT_WAIT_TIMEOUT_MS
const task = await readTask(projectRoot, args.task_id)
if (!task) {
throw new Error(`Unknown task_id: ${args.task_id}`)
}
if (task.parentSessionID !== context.sessionID) {
throw new Error(`Task ${args.task_id} belongs to a different coordinator session`)
}
context.metadata({
title: `Wait for ${args.task_id}`,
metadata: {
taskID: args.task_id,
childSessionID: task.childSessionID,
},
})
const outcome = await awaitTaskResult(client, context, projectRoot, args.task_id, timeoutMs)
if (outcome.status === "running") {
return [
`▸ ${args.task_id} is running`,
` session: ${task.childSessionID}`,
` status: ${outcome.liveStatus}`,
` waited: ${timeoutMs}ms`,
].join("\n")
}
if (outcome.status === "cancelled") {
return [
`○ ${args.task_id} was cancelled`,
` session: ${task.childSessionID}`,
].join("\n")
}
if (outcome.status === "error") {
return [
`✗ ${args.task_id} is in an error state`,
` session: ${task.childSessionID}`,
` error: ${outcome.task.error || "unknown error"}`,
].join("\n")
}
if (outcome.status === "ready") {
return [
`◦ ${args.task_id} is idle (no new result)`,
` session: ${task.childSessionID}`,
"",
"Use `team_message` to send a follow-up or `team_wait` again later.",
].join("\n")
}
const artifactDisplay = artifactPathForDisplay(projectRoot, outcome.artifactPath)
return [
`✓ ${args.task_id} is idle`,
` session: ${task.childSessionID}`,
artifactDisplay ? ` artifact: ${artifactDisplay}` : "",
"",
outcome.latest.text || "The worker is idle, but no assistant summary was found yet.",
].filter(Boolean).join("\n")
},
}),
team_list: tool({
description: "List delegated worker tasks for the current coordinator session",
args: {
status: tool.schema.enum(["running", "ready", "done", "cancelled", "error"]).optional().describe("Optional status filter"),
},
async execute(args, context) {
const projectRoot = projectRootFromContext(context)
const registry = await loadRegistry(projectRoot)
const scopedTasks = registry.tasks.filter(task => task.parentSessionID === context.sessionID)
const lines = []
const statusCounts = {}
for (const task of scopedTasks) {
let liveStatus = task.status
if (task.status === "running") {
const sessionStatus = await getLiveSessionStatus(client, task.childSessionID, context.directory)
if (sessionStatus.type === "idle") {
liveStatus = task.summary ? "done" : "ready"
} else {
liveStatus = "running"
}
}
statusCounts[liveStatus] = (statusCounts[liveStatus] || 0) + 1
if (args.status && liveStatus !== args.status) {
continue
}
lines.push(formatTaskLine(task, liveStatus))
}
if (lines.length === 0) {
return args.status
? `No tasks matched status \"${args.status}\" for this coordinator session.`
: "No delegated tasks found for this coordinator session."
}
const summaryParts = Object.entries(statusCounts)
.sort(([,a], [,b]) => b - a)
.map(([s, c]) => `${c} ${s}`)
.join(", ")
const shown = args.status ? `${lines.length} of ${scopedTasks.length}` : `${lines.length}`
const summaryLabel = args.status ? "All tasks:" : ""
const summaryLine = summaryLabel ? `${summaryLabel} ${summaryParts}` : summaryParts
return [`Delegated tasks (${shown}):`, ...lines, "", summaryLine].join("\n")
},
}),
team_cancel: tool({
description: "Abort a delegated worker session and mark the task as cancelled",
args: {
task_id: tool.schema.string().describe("Task ID from team_delegate"),
},
async execute(args, context) {
const projectRoot = projectRootFromContext(context)
const task = await readTask(projectRoot, args.task_id)
if (!task) {
throw new Error(`Unknown task_id: ${args.task_id}`)
}
if (task.parentSessionID !== context.sessionID) {
throw new Error(`Task ${args.task_id} belongs to a different coordinator session`)
}
if (task.status === "done" || task.status === "cancelled") {
const icon = task.status === "done" ? "✓" : "○"
return `${icon} ${args.task_id} is already ${task.status}; not cancelling.`
}
context.metadata({
title: `Cancel ${args.task_id}`,
metadata: {
taskID: args.task_id,
childSessionID: task.childSessionID,
},
})
const liveStatus = await getLiveSessionStatus(client, task.childSessionID, context.directory)
if (liveStatus.type === "idle" && task.summary) {
return `✓ ${args.task_id} is already idle with a completed result; not cancelling.`
}
await client.session.abort({
path: { id: task.childSessionID },
query: { directory: context.directory },
})
const cancelResult = await updateTask(projectRoot, args.task_id, currentTask => {
if (currentTask.status === "done" || currentTask.status === "cancelled") {
return { alreadyTerminal: true, task: currentTask }
}
currentTask.status = "cancelled"
currentTask.updatedAt = now()
delete currentTask.error
return { alreadyTerminal: false, task: { ...currentTask } }
})
if (cancelResult.alreadyTerminal) {
const icon = cancelResult.task.status === "done" ? "✓" : "○"
return `${icon} ${args.task_id} is already ${cancelResult.task.status}; no action taken.`
}
return [
`○ Cancelled ${args.task_id}`,
` session: ${task.childSessionID}`,
` title: ${task.title}`,
].join("\n")
},
}),
},
}
}

View File

@@ -0,0 +1,67 @@
---
name: debug
description: Diagnose frozen, slow, or misbehaving sessions by reading debug logs and system state
compatibility: opencode
metadata:
workflow: diagnostics
---
## What I do
I diagnose problems with the current session — freezes, slowness, errors, or unexpected behavior — by reading debug logs and checking system state.
## How to use me
1. **Find the debug log.** Check these locations in order:
- `~/.local/share/opencode/logs/` (Linux)
- `~/Library/Application Support/opencode/logs/` (macOS)
- `~/.config/opencode/logs/` (fallback)
- Environment variable `OPENCODE_LOG_DIR` if set
- Any `.log` files in the project root or `.opencode/` directory
2. **Read the tail of the log** (last 50-100 lines, or last 64KB max). Do not read the entire log file — it can be enormous.
3. **Check system state:**
- Is the process still running? (`ps aux | grep opencode`)
- Is CPU usage abnormally high?
- Is memory usage growing unbounded?
- Are there zombie or defunct child processes?
- Is the git repo in a weird state (merge conflict, detached HEAD, locked index)?
4. **Identify the problem category:**
- **API errors** — rate limits, auth failures, model errors, timeout
- **Tool errors** — command failures, permission denials, file not found
- **Session freeze** — no output for extended period, hung child process
- **Performance** — slow responses, high token usage, context window overflow
- **Configuration** — missing API key, wrong model, broken settings
5. **Report findings with evidence.** Quote relevant log lines. Do not speculate without log evidence.
## Investigation rules
- Reproduce before you reason. Never guess at a cause without first confirming the bug.
- Read logs, don't hallucinate. Quote specific log lines with timestamps.
- If you cannot reproduce, report what you tried and ask for more information.
- If the problem is upstream or environmental, say so explicitly.
## Output format
### Symptoms
What the user reported or what I observed.
### Log Evidence
Relevant log lines (quoted, with timestamps).
### Diagnosis
What is wrong and why.
### Recommended Fix
Specific steps to resolve the issue.
### Escalation
If the problem is a bug in opencode itself, note the version, reproduction steps, and suggest filing an issue at https://github.com/anomalyco/opencode/issues.
## When to use me
Use this when a session is frozen, slow, throwing errors, or behaving unexpectedly. Do not use for general code debugging — this skill is for diagnosing the tool itself, not the user's code.

View File

@@ -0,0 +1,62 @@
---
name: simplify
description: Review changed code for reuse, quality, and efficiency, then fix issues found
compatibility: opencode
metadata:
workflow: code-review
parallel: true
---
## What I do
I review your recent changes from three angles in parallel, then fix the issues I find.
## How to use me
1. Identify the changed files using `git diff` (staged or unstaged).
2. Launch three parallel review agents, each with a different focus:
- **Reuse Review**: Look for duplicated logic, copy-paste patterns, or abstractions that already exist in the codebase. Check if the new code reinvents something that's already available as a utility, helper, or library function.
- **Quality Review**: Look for hacky patterns, missing error handling, unclear naming, magic numbers, overly complex functions, and violations of existing project conventions.
- **Efficiency Review**: Look for unnecessary allocations, N+1 queries, redundant iterations, missing early returns, and opportunities to simplify algorithms.
3. Wait for all three agents to finish.
4. Aggregate findings by severity (bugs > correctness > performance > style).
5. Fix the highest-severity issues directly. For lower-severity items, report them for the user to decide.
## Agent prompt template
For each review agent, use this structure:
```
You are a code reviewer focused on [REUSE|QUALITY|EFFICIENCY].
Review the following changed files. You are a sub-agent — do NOT modify files, only report findings. The parent skill will apply fixes after aggregating all reviews.
Changed files:
<list files from git diff>
For each finding, provide:
- File path and line number
- What is wrong
- Why it matters
- Suggested fix (if applicable)
Order findings by severity: bugs > correctness > performance > style.
If you find no issues, say so explicitly.
```
## Output format
### Findings
Ordered by severity. Each finding includes file, line, issue, and fix.
### Applied Fixes
List of changes made with rationale.
### Remaining
Lower-severity items left for the user to decide.
## When to use me
Use this after making a set of changes and before committing. Do not use for exploratory reading — this skill modifies files.

View File

@@ -0,0 +1,64 @@
---
name: verify
description: Prove a code change works by running real commands and checking output
compatibility: opencode
metadata:
workflow: testing
adversarial: true
---
## What I do
I verify that a code change actually works by running real commands — not by reading code and guessing.
## How to use me
1. Identify the changed files using `git diff` (staged or unstaged).
2. Inspect the changed files to understand what behavior should be validated.
3. Identify the smallest set of commands that can verify that behavior:
- Run relevant tests (`npm test`, `go test`, `pytest`, etc.)
- Run type checks (`tsc --noEmit`, `mypy`, etc.)
- Run linters if they catch correctness issues
- Build the project if build errors would indicate problems
- For CLI tools, run the tool with test inputs
- For APIs, make test requests with `curl`
4. Execute the checks in order of signal: tests first, then typecheck, then lint, then build.
5. For failures, investigate root cause — distinguish product bugs from flaky tests from environment issues.
6. Report results with evidence.
## Verification rules
- Run real commands. Reading code is not verification.
- Check happy paths and at least one meaningful edge case.
- Investigate failures instead of dismissing them.
- Distinguish between product bugs, flaky tests, and environment problems.
- Do not rubber-stamp. If evidence is weak, say so plainly.
## Anti-patterns to avoid
- "The code looks correct" — not verification
- "Tests should pass" — run them and prove it
- Skipping checks because the change "looks simple"
- Accepting partial test runs as full verification
## Output format
### Checks Run
List each command or manual flow executed.
### Results
For each check: PASS or FAIL with the actual output as evidence.
### Confidence
State one of:
- **Verified** — all checks passed, edge cases covered
- **Partially verified** — some checks passed, gaps remain
- **Not verified** — checks failed or could not be run
### Gaps
Anything still unproven. Be specific about what was not checked and why.
## When to use me
Use this after implementing a change to prove it works. Use before committing or opening a PR. Do not use for exploratory code reading.

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

14
pyproject.toml Normal file
View File

@@ -0,0 +1,14 @@
[project]
name = "rp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Yunxiao Xu", email = "xuyunxiao2001@gmail.com" }
]
requires-python = ">=3.12"
dependencies = []
[build-system]
requires = ["uv_build>=0.11.2,<0.12.0"]
build-backend = "uv_build"

2
src/rp/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
def hello() -> str:
return "Hello from rp!"

0
src/rp/py.typed Normal file
View File