Initial project scaffold with uv
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal 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
2
.opencode/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
state/
|
||||
47
.opencode/agents/review.md
Normal file
47
.opencode/agents/review.md
Normal 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.
|
||||
53
.opencode/agents/subplan.md
Normal file
53
.opencode/agents/subplan.md
Normal 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.
|
||||
69
.opencode/agents/team-lead.md
Normal file
69
.opencode/agents/team-lead.md
Normal 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.
|
||||
48
.opencode/agents/verify.md
Normal file
48
.opencode/agents/verify.md
Normal 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
18
.opencode/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
13
.opencode/commands/team.md
Normal file
13
.opencode/commands/team.md
Normal 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
301
.opencode/lib/team-utils.js
Normal 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
6
.opencode/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.3.13"
|
||||
}
|
||||
}
|
||||
621
.opencode/plugins/team-orchestrator.js
Normal file
621
.opencode/plugins/team-orchestrator.js
Normal 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")
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
67
.opencode/skills/debug/SKILL.md
Normal file
67
.opencode/skills/debug/SKILL.md
Normal 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.
|
||||
|
||||
62
.opencode/skills/simplify/SKILL.md
Normal file
62
.opencode/skills/simplify/SKILL.md
Normal 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.
|
||||
|
||||
|
||||
64
.opencode/skills/verify/SKILL.md
Normal file
64
.opencode/skills/verify/SKILL.md
Normal 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
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
14
pyproject.toml
Normal file
14
pyproject.toml
Normal 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
2
src/rp/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def hello() -> str:
|
||||
return "Hello from rp!"
|
||||
0
src/rp/py.typed
Normal file
0
src/rp/py.typed
Normal file
Reference in New Issue
Block a user