scripts/parse_output.py
#!/usr/bin/env python3
"""Parse OpenCode `--format json` ndjson output and extract useful information.
Usage:
opencode run --format json ... | python parse_output.py
opencode run --format json ... | python parse_output.py --mode text
opencode run --format json ... | python parse_output.py --mode full
opencode run --format json ... | python parse_output.py --mode tools
opencode run --format json ... | python parse_output.py --mode cost
opencode run --format json ... | python parse_output.py --mode session
opencode run --format json ... | python parse_output.py --mode diff
opencode run --format json ... | python parse_output.py --mode summary
Modes:
text - Extract only the model's text response (default)
full - Show text + tool calls + tool results
tools - Show only tool calls and results
cost - Show token usage and cost summary
session - Extract session ID for continuation
diff - Extract file modifications from tool calls
summary - One-line summary (status, tokens, cost)
"""
import json
import sys
import argparse
import re
import time
def parse_events(lines):
"""Parse ndjson lines into structured events."""
events = []
for line in lines:
line = line.strip()
if not line:
continue
try:
events.append(json.loads(line))
except json.JSONDecodeError:
continue
return events
def extract_text(events):
"""Extract concatenated text response."""
parts = []
for ev in events:
if ev.get("type") == "text":
text = ev.get("part", {}).get("text", "")
if text:
parts.append(text)
return "".join(parts) if parts else "(no text response)"
def extract_full(events):
"""Extract text, tool calls, and tool results."""
output = []
for ev in events:
t = ev.get("type", "")
part = ev.get("part", {})
if t == "text":
text = part.get("text", "")
if text:
output.append(text)
elif t == "tool_call":
name = part.get("name", "unknown")
inp = part.get("input", {})
output.append(f"\n--- Tool Call: {name} ---")
output.append(json.dumps(inp, indent=2, ensure_ascii=False))
elif t == "tool_result":
result = part.get("output", "")
output.append(f"--- Tool Result ---")
output.append(str(result)[:2000])
return "\n".join(output)
def extract_tools(events):
"""Extract only tool calls and results."""
output = []
for ev in events:
t = ev.get("type", "")
part = ev.get("part", {})
if t == "tool_call":
name = part.get("name", "unknown")
inp = part.get("input", {})
output.append(f"[CALL] {name}: {json.dumps(inp, ensure_ascii=False)[:500]}")
elif t == "tool_result":
result = str(part.get("output", ""))
output.append(f"[RESULT] {result[:500]}")
return "\n".join(output) if output else "(no tool calls)"
def extract_cost(events):
"""Extract token usage and cost from step_finish events."""
total_input = 0
total_output = 0
total_reasoning = 0
total_cost = 0.0
steps = 0
for ev in events:
if ev.get("type") == "step_finish":
part = ev.get("part", {})
tokens = part.get("tokens", {})
total_input += tokens.get("input", 0)
total_output += tokens.get("output", 0)
total_reasoning += tokens.get("reasoning", 0)
total_cost += part.get("cost", 0)
steps += 1
lines = [
f"Steps: {steps}",
f"Input tokens: {total_input:,}",
f"Output tokens: {total_output:,}",
f"Reasoning tokens: {total_reasoning:,}",
f"Total tokens: {total_input + total_output + total_reasoning:,}",
f"Cost: ${total_cost:.4f}",
]
return "\n".join(lines)
def extract_session(events):
"""Extract session ID from step_start or step_finish events for continuation."""
session_id = None
for ev in events:
if ev.get("type") == "step_start":
sid = ev.get("sessionID") or ev.get("part", {}).get("sessionID")
if sid:
session_id = sid
elif ev.get("type") == "step_finish":
sid = ev.get("sessionID") or ev.get("part", {}).get("sessionID")
if sid:
session_id = sid
return session_id if session_id else "(no session ID found)"
def extract_diff(events):
"""Extract file modifications from tool calls (write, edit, patch operations)."""
modifications = []
write_tools = {"write", "write_file", "Write", "create", "Create"}
edit_tools = {"edit", "edit_file", "Edit", "patch", "Patch", "replace", "Replace"}
bash_tools = {"bash", "Bash", "shell", "Shell"}
for ev in events:
if ev.get("type") != "tool_call":
continue
part = ev.get("part", {})
name = part.get("name", "")
inp = part.get("input", {})
if name in write_tools:
path = inp.get("file_path") or inp.get("path") or inp.get("filePath", "")
content_preview = str(inp.get("content", ""))[:200]
modifications.append(f"[WRITE] {path}")
if content_preview:
modifications.append(f" preview: {content_preview}...")
elif name in edit_tools:
path = inp.get("file_path") or inp.get("path") or inp.get("filePath", "")
old = str(inp.get("old_string") or inp.get("old", ""))[:100]
new = str(inp.get("new_string") or inp.get("new", ""))[:100]
modifications.append(f"[EDIT] {path}")
if old:
modifications.append(f" old: {old}")
if new:
modifications.append(f" new: {new}")
elif name in bash_tools:
cmd = str(inp.get("command") or inp.get("cmd", ""))
# Detect file-modifying bash commands
if any(kw in cmd for kw in [">>", "> ", "mv ", "cp ", "mkdir ", "rm ", "sed ", "tee "]):
modifications.append(f"[BASH] {cmd[:200]}")
if not modifications:
return "(no file modifications detected)"
return "\n".join(modifications)
def extract_summary(events):
"""One-line summary: status | tokens | cost | text length."""
text = extract_text(events)
has_text = text != "(no text response)"
total_tokens = 0
total_cost = 0.0
tool_calls = 0
for ev in events:
if ev.get("type") == "step_finish":
tokens = ev.get("part", {}).get("tokens", {})
total_tokens += tokens.get("input", 0) + tokens.get("output", 0) + tokens.get("reasoning", 0)
total_cost += ev.get("part", {}).get("cost", 0)
elif ev.get("type") == "tool_call":
tool_calls += 1
status = "ok" if has_text else "empty"
text_len = len(text) if has_text else 0
return f"{status} | {total_tokens:,} tokens | ${total_cost:.4f} | {tool_calls} tool calls | {text_len:,} chars"
def main():
parser = argparse.ArgumentParser(description="Parse OpenCode JSON output")
parser.add_argument(
"--mode",
choices=["text", "full", "tools", "cost", "session", "diff", "summary"],
default="text",
help="Output mode (default: text)",
)
args = parser.parse_args()
lines = sys.stdin.readlines()
events = parse_events(lines)
if not events:
print("(no output received from OpenCode)", file=sys.stderr)
sys.exit(1)
extractors = {
"text": extract_text,
"full": extract_full,
"tools": extract_tools,
"cost": extract_cost,
"session": extract_session,
"diff": extract_diff,
"summary": extract_summary,
}
print(extractors[args.mode](events))
if __name__ == "__main__":
main()
scripts/test_skill.py
#!/usr/bin/env python3
"""Autoresearch-style test harness for the opencode skill.
Tests the skill by running real OpenCode invocations against multiple models
and scoring the results. Outputs a TSV results log.
Usage:
python test_skill.py # Run all tests with all models
python test_skill.py --model openai/gpt-5.3-codex # Single model
python test_skill.py --test review # Single test category
python test_skill.py --quick # Fast subset only
python test_skill.py --loop # Autoresearch loop: keep running until interrupted
python test_skill.py --loop --max-iter 5 # Loop with max iterations
python test_skill.py --pipeline # Test implement-then-review pipeline
python test_skill.py --compare # Parallel multi-model comparison
"""
import json
import subprocess
import sys
import os
import time
import argparse
from pathlib import Path
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
PROJECT_DIR = "C:/UnrealEngine/VHS"
SKILL_DIR = Path(__file__).parent.parent
RESULTS_FILE = SKILL_DIR / "results.tsv"
PARSE_SCRIPT = SKILL_DIR / "scripts" / "parse_output.py"
MODELS = [
"openai/gpt-5.3-codex",
"openai/gpt-5.4",
]
# Cross-model pairings for pipeline tests
PIPELINE_PAIRS = [
("openai/gpt-5.4", "openai/gpt-5.3-codex"),
("openai/gpt-5.3-codex", "openai/gpt-5.4"),
]
# Fallback chain for error recovery
FALLBACK_CHAIN = [
"openai/gpt-5.4",
"openai/gpt-5.3-codex",
"openai/gpt-5.4-mini-fast",
"github-copilot/gpt-5.4",
]
TIMEOUT = 300 # seconds per test (5 min)
# ---------------------------------------------------------------------------
# Test Cases
# ---------------------------------------------------------------------------
TEST_CASES = {
"review": {
"description": "Code review of a real project file",
"agent": "plan",
"prompt": (
"[CONTEXT]\n"
"Unreal Engine 5.7 C++ project (VHS) using GAS.\n\n"
"[TASK]\n"
"Review Source/VHS/AbilitySystem/VHSAbilitySystemComponent.h and .cpp for: "
"correctness, potential bugs, performance issues, memory leaks, UE5 best practices.\n\n"
"[SCOPE]\n"
"Review ONLY the AbilitySystemComponent files. Do NOT explore other directories.\n\n"
"[OUTPUT FORMAT]\n"
"For each issue: [SEVERITY] File:Line -- Issue -- Fix. Keep under 300 words."
),
"files": [],
"scoring": {
"mentions_line_numbers": 2,
"mentions_specific_issues": 3,
"suggests_fixes": 3,
"understands_ue5": 2,
"response_not_empty": 1,
"no_hallucinated_files": 1,
},
},
"research": {
"description": "Codebase architecture exploration",
"agent": "plan",
"prompt": (
"[CONTEXT]\n"
"Unreal Engine 5.7 C++ project (VHS - horror game).\n\n"
"[TASK]\n"
"Answer these questions:\n"
"1. What GAS components are implemented?\n"
"2. How is the AbilitySystemComponent initialized?\n"
"3. What gameplay abilities exist?\n\n"
"[SCOPE]\n"
"Search ONLY in Source/VHS/. Do NOT explore Engine or Plugin directories.\n\n"
"[OUTPUT FORMAT]\n"
"Cite file paths for every claim. Keep under 300 words."
),
"files": [],
"scoring": {
"mentions_real_files": 3,
"answers_all_questions": 3,
"cites_specific_code": 2,
"no_hallucinations": 2,
"response_not_empty": 1,
},
},
"debug": {
"description": "Diagnose a hypothetical bug",
"agent": "plan",
"prompt": (
"[CONTEXT]\n"
"UE5.7 project (VHS) using GAS for sprint and stamina.\n\n"
"[BUG REPORT]\n"
"Sprint ability GA_Sprint drains stamina but recovery doesn't start after sprint stops. "
"Recovery is a gameplay effect with MMC.\n\n"
"[TASK]\n"
"Investigate root cause. Check: effect removal timing, MMC dependencies, "
"tag blocking on recovery effect, attribute clamping.\n\n"
"[SCOPE]\n"
"Check ONLY the AbilitySystem/ directory. Do NOT explore other directories.\n\n"
"[OUTPUT FORMAT]\n"
"1. Root cause (most likely)\n2. Evidence (file:line)\n3. Fix. Keep under 200 words."
),
"files": [],
"scoring": {
"identifies_plausible_causes": 3,
"references_gas_patterns": 2,
"mentions_mmc_or_ge": 2,
"suggests_investigation_steps": 2,
"response_not_empty": 1,
"response_is_structured": 1,
},
},
"hard_review": {
"description": "Deep cross-file architecture review (hard)",
"agent": "plan",
"prompt": (
"[CONTEXT]\n"
"Unreal Engine 5.7 C++ project (VHS) using GAS, Enhanced Input, StateTree.\n\n"
"[TASK]\n"
"Architecture review of GAS integration across the codebase:\n"
"1. How does the ASC connect to Character and PlayerState?\n"
"2. Any GAS anti-patterns (ASC ownership, effect stacking, missing replication)?\n"
"3. Is the interaction system properly integrated with GAS?\n\n"
"[SCOPE]\n"
"Read files in Source/VHS/. Do NOT explore Engine or Plugin directories.\n\n"
"[OUTPUT FORMAT]\n"
"For each finding: [SEVERITY] File:Line -- Issue -- Fix. Keep under 500 words."
),
"files": [],
"scoring": {
"mentions_line_numbers": 2,
"mentions_specific_issues": 3,
"suggests_fixes": 2,
"understands_ue5": 2,
"response_not_empty": 1,
"no_hallucinated_files": 1,
"mentions_real_files": 2,
"response_is_structured": 1,
},
},
"hard_debug": {
"description": "Complex multi-system bug diagnosis (hard)",
"agent": "plan",
"prompt": (
"[CONTEXT]\n"
"UE5.7 VHS horror game using GAS. Sprint uses GA_Sprint with stamina attributes. "
"Interaction uses GA_Interact.\n\n"
"[BUG REPORT]\n"
"Player sometimes can't interact with objects after sprinting and running out of stamina.\n\n"
"[TASK]\n"
"Investigate how sprint and interaction systems conflict. Check:\n"
"- Tag blocking between GA_Sprint and GA_Interact\n"
"- Ability activation conditions and stamina thresholds\n"
"- State machine issues and effect cleanup\n\n"
"[SCOPE]\n"
"Investigate Source/VHS/ directory.\n\n"
"[OUTPUT FORMAT]\n"
"1. Root cause\n2. Evidence (file:line)\n3. Fix. Keep under 500 words."
),
"files": [],
"scoring": {
"response_not_empty": 1,
"identifies_plausible_causes": 3,
"references_gas_patterns": 2,
"mentions_mmc_or_ge": 1,
"suggests_investigation_steps": 2,
"response_is_structured": 1,
"mentions_real_files": 2,
},
},
"scoped_implement": {
"description": "Scoped implementation task (read-only validation)",
"agent": "plan",
"prompt": (
"[CONTEXT]\n"
"UE5.7 C++ project (VHS). GAS naming: GA_ (abilities), GC_ (cues), MMC_ (magnitude calcs).\n"
"Headers and .cpp side-by-side, no Public/Private split. #pragma once, CoreMinimal.h first.\n\n"
"[TASK]\n"
"Design (do NOT implement) a new gameplay ability GA_Crouch that:\n"
"- Reduces movement speed by 50% while active\n"
"- Uses a gameplay effect GE_CrouchSlow for the speed modifier\n"
"- Has a gameplay tag State.Crouching applied while active\n"
"- Cannot activate during sprint (blocked by State.Sprinting tag)\n\n"
"[SCOPE]\n"
"Reference Source/VHS/AbilitySystem/Abilities/ for existing patterns.\n\n"
"[OUTPUT FORMAT]\n"
"Provide: 1. Header file content 2. Cpp file content 3. GameplayEffect setup. "
"Follow existing GA_Sprint patterns."
),
"files": [],
"scoring": {
"response_not_empty": 1,
"mentions_real_files": 2,
"references_gas_patterns": 3,
"response_is_structured": 2,
"suggests_fixes": 2, # reusing: checks for concrete code suggestions
"understands_ue5": 3,
},
},
}
# Pipeline test: implementation + cross-model review
PIPELINE_TESTS = {
"pipeline_design_review": {
"description": "Design with model A, review with model B",
"implement_prompt": (
"[CONTEXT]\n"
"UE5.7 C++ project (VHS) using GAS.\n\n"
"[TASK]\n"
"Design a GA_Crouch gameplay ability following the GA_Sprint pattern in "
"Source/VHS/AbilitySystem/Abilities/. Output the .h and .cpp content.\n\n"
"[SCOPE]\n"
"Read ONLY Source/VHS/AbilitySystem/Abilities/ for reference patterns.\n\n"
"[OUTPUT FORMAT]\n"
"Full .h and .cpp file contents ready to save."
),
"review_prompt_template": (
"[CONTEXT]\n"
"UE5.7 C++ project (VHS) using GAS.\n\n"
"[TASK]\n"
"Review this GA_Crouch ability design for: GAS best practices, "
"replication correctness, tag blocking, effect cleanup, memory safety.\n\n"
"--- DESIGN TO REVIEW ---\n{design_output}\n--- END DESIGN ---\n\n"
"[OUTPUT FORMAT]\n"
"For each issue: [SEVERITY] -- Issue -- Fix. Keep under 300 words."
),
"scoring": {
"response_not_empty": 1,
"mentions_specific_issues": 2,
"references_gas_patterns": 3,
"response_is_structured": 2,
"understands_ue5": 2,
},
},
}
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
def run_opencode(model, agent, prompt, files=None, timeout=TIMEOUT):
"""Run opencode and return (text_output, tokens, cost, duration, status)."""
files = files or []
opencode_bin = os.environ.get("OPENCODE_BIN", "opencode")
if sys.platform == "win32":
import shutil
found = shutil.which("opencode")
if found:
opencode_bin = found
cmd = [
opencode_bin, "run",
"--format", "json",
"--model", model,
"--agent", agent,
"--dir", PROJECT_DIR,
"--dangerously-skip-permissions",
]
for f in files:
cmd.extend(["-f", os.path.join(PROJECT_DIR, f)])
cmd.append(prompt)
env = os.environ.copy()
env["OPENCODE_DISABLE_AUTOUPDATE"] = "true"
env["PYTHONIOENCODING"] = "utf-8"
start = time.time()
try:
result = subprocess.run(
cmd,
capture_output=True,
timeout=timeout,
env=env,
shell=(sys.platform == "win32"),
)
duration = time.time() - start
stdout = result.stdout.decode("utf-8", errors="replace")
except subprocess.TimeoutExpired:
return "", 0, 0.0, timeout, "timeout"
except FileNotFoundError:
return "opencode not found in PATH", 0, 0.0, 0, "crash"
except Exception as e:
return str(e), 0, 0.0, time.time() - start, "crash"
if result.returncode != 0 and not stdout.strip():
stderr = result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
return f"exit code {result.returncode}: {stderr[:500]}", 0, 0.0, duration, "crash"
# Parse ndjson
text_parts = []
total_tokens = 0
total_cost = 0.0
tool_calls = 0
for line in stdout.strip().split("\n"):
if not line.strip():
continue
try:
ev = json.loads(line)
except json.JSONDecodeError:
continue
if ev.get("type") == "text":
t = ev.get("part", {}).get("text", "")
if t:
text_parts.append(t)
elif ev.get("type") == "tool_call":
tool_calls += 1
elif ev.get("type") == "step_finish":
tokens = ev.get("part", {}).get("tokens", {})
total_tokens += tokens.get("input", 0) + tokens.get("output", 0) + tokens.get("reasoning", 0)
total_cost += ev.get("part", {}).get("cost", 0)
text = "".join(text_parts)
status = "ok" if text else "empty"
return text, total_tokens, total_cost, duration, status
def run_with_fallback(agent, prompt, files=None, timeout=TIMEOUT, preferred_model=None):
"""Run opencode with automatic model fallback on failure."""
chain = [preferred_model] + FALLBACK_CHAIN if preferred_model else FALLBACK_CHAIN
seen = set()
for model in chain:
if model in seen or model is None:
continue
seen.add(model)
text, tokens, cost, duration, status = run_opencode(model, agent, prompt, files, timeout)
if status == "ok":
return text, tokens, cost, duration, status, model
print(f" [{model}] failed ({status}), trying fallback...")
return "", 0, 0.0, 0, "all_failed", "none"
# ---------------------------------------------------------------------------
# Scorer
# ---------------------------------------------------------------------------
def score_response(text, test_case):
"""Score a response based on test case criteria. Returns (score, max_score, details)."""
scoring = test_case["scoring"]
max_score = sum(scoring.values())
score = 0
details = {}
text_lower = text.lower()
# Generic checks
if "response_not_empty" in scoring and len(text.strip()) > 50:
score += scoring["response_not_empty"]
details["response_not_empty"] = "PASS"
elif "response_not_empty" in scoring:
details["response_not_empty"] = "FAIL"
if "no_hallucinated_files" in scoring:
fake_markers = ["src/main.cpp", "app.js", "index.ts", "main.py"]
if not any(m in text_lower for m in fake_markers):
score += scoring["no_hallucinated_files"]
details["no_hallucinated_files"] = "PASS"
else:
details["no_hallucinated_files"] = "FAIL"
if "no_hallucinations" in scoring:
fake_markers = ["src/main.cpp", "app.js", "index.ts", "main.py", "package.json"]
if not any(m in text_lower for m in fake_markers):
score += scoring["no_hallucinations"]
details["no_hallucinations"] = "PASS"
else:
details["no_hallucinations"] = "FAIL"
# Review-specific
if "mentions_line_numbers" in scoring:
import re
if re.search(r"line\s*\d+|:\d+|L\d+", text):
score += scoring["mentions_line_numbers"]
details["mentions_line_numbers"] = "PASS"
else:
details["mentions_line_numbers"] = "FAIL"
if "mentions_specific_issues" in scoring:
issue_words = ["bug", "issue", "problem", "error", "missing", "incorrect",
"should", "could", "potential", "risk", "vulnerability", "warning",
"concern", "note", "redundant", "unused", "unnecessary", "inefficient",
"unsafe", "deprecated", "anti-pattern", "smell"]
hits = sum(1 for w in issue_words if w in text_lower)
if hits >= 3:
score += scoring["mentions_specific_issues"]
details["mentions_specific_issues"] = f"PASS ({hits} markers)"
else:
details["mentions_specific_issues"] = f"FAIL ({hits} markers)"
if "suggests_fixes" in scoring:
fix_words = ["fix", "change", "replace", "add", "remove", "instead", "suggest",
"recommend", "consider", "should be", "use"]
hits = sum(1 for w in fix_words if w in text_lower)
if hits >= 3:
score += scoring["suggests_fixes"]
details["suggests_fixes"] = f"PASS ({hits} markers)"
else:
details["suggests_fixes"] = f"FAIL ({hits} markers)"
if "understands_ue5" in scoring:
ue_words = ["uproperty", "ufunction", "uclass", "gas", "gameplay", "ability",
"unreal", "ue5", "actor", "component", "blueprint", "ustruct",
"generated.h", "coreminal", "replicated"]
hits = sum(1 for w in ue_words if w in text_lower)
if hits >= 2:
score += scoring["understands_ue5"]
details["understands_ue5"] = f"PASS ({hits} markers)"
else:
details["understands_ue5"] = f"FAIL ({hits} markers)"
# Research-specific
if "mentions_real_files" in scoring:
real_paths = ["abilitysystem", "vhsabilitysystemcomponent", "horrorcharacter",
"ga_sprint", "ga_interact", "vhsattributeset", "vhsgameplaytags",
"source/vhs", "ga_crouch", "horrorplayercontroller"]
hits = sum(1 for p in real_paths if p in text_lower)
if hits >= 2:
score += scoring["mentions_real_files"]
details["mentions_real_files"] = f"PASS ({hits} paths)"
else:
details["mentions_real_files"] = f"FAIL ({hits} paths)"
if "answers_all_questions" in scoring:
markers = ["ability system component", "abilities", "gas"]
if any("asc" in text_lower or "abilitysystemcomponent" in text_lower for _ in [1]):
markers_hit = 1
else:
markers_hit = 0
markers_hit += sum(1 for m in markers if m in text_lower)
if markers_hit >= 2:
score += scoring["answers_all_questions"]
details["answers_all_questions"] = f"PASS ({markers_hit})"
else:
details["answers_all_questions"] = f"FAIL ({markers_hit})"
if "cites_specific_code" in scoring:
import re
code_refs = len(re.findall(r'`[A-Z]\w+`|`\w+\.\w+`|```', text))
if code_refs >= 2:
score += scoring["cites_specific_code"]
details["cites_specific_code"] = f"PASS ({code_refs} refs)"
else:
details["cites_specific_code"] = f"FAIL ({code_refs} refs)"
# Debug-specific
if "identifies_plausible_causes" in scoring:
cause_words = ["cause", "because", "likely", "possibly", "root cause",
"the issue", "the problem", "reason", "due to", "stems from",
"triggered by", "results in", "leads to", "prevents", "blocks",
"conflict", "race condition", "timing", "never"]
hits = sum(1 for w in cause_words if w in text_lower)
if hits >= 2:
score += scoring["identifies_plausible_causes"]
details["identifies_plausible_causes"] = f"PASS ({hits})"
else:
details["identifies_plausible_causes"] = f"FAIL ({hits})"
if "references_gas_patterns" in scoring:
gas_words = ["gameplay effect", "gameplay ability", "attribute", "modifier",
"ga_", "ge_", "mmc_", "gas", "gc_", "gameplay tag",
"ability system", "effect stack"]
hits = sum(1 for w in gas_words if w in text_lower)
if hits >= 2:
score += scoring["references_gas_patterns"]
details["references_gas_patterns"] = f"PASS ({hits})"
else:
details["references_gas_patterns"] = f"FAIL ({hits})"
if "mentions_mmc_or_ge" in scoring:
if "mmc" in text_lower or "modifier magnitude" in text_lower or "gameplay effect" in text_lower or "ge_" in text_lower or "default effect" in text_lower or "effect stack" in text_lower:
score += scoring["mentions_mmc_or_ge"]
details["mentions_mmc_or_ge"] = "PASS"
else:
details["mentions_mmc_or_ge"] = "FAIL"
if "suggests_investigation_steps" in scoring:
step_words = ["check", "verify", "look at", "inspect", "debug", "set breakpoint",
"log", "print", "investigate", "examine", "step"]
hits = sum(1 for w in step_words if w in text_lower)
if hits >= 2:
score += scoring["suggests_investigation_steps"]
details["suggests_investigation_steps"] = f"PASS ({hits})"
else:
details["suggests_investigation_steps"] = f"FAIL ({hits})"
if "response_is_structured" in scoring:
structure_markers = ["1.", "2.", "##", "**", "- "]
hits = sum(1 for m in structure_markers if m in text)
if hits >= 2:
score += scoring["response_is_structured"]
details["response_is_structured"] = f"PASS ({hits})"
else:
details["response_is_structured"] = f"FAIL ({hits})"
return score, max_score, details
# ---------------------------------------------------------------------------
# Runners
# ---------------------------------------------------------------------------
def run_standard_tests(models, tests, tsv):
"""Run standard single-model tests."""
total_score = 0
total_max = 0
results = []
for test_name, test_case in tests.items():
for model in models:
print(f"\n--- {test_name} | {model} ---")
print(f" Running... ", end="", flush=True)
text, tokens, cost, duration, status = run_opencode(
model=model,
agent=test_case["agent"],
prompt=test_case["prompt"],
files=test_case.get("files", []),
)
if status == "ok":
score, max_score, details = score_response(text, test_case)
pct = round(score / max_score * 100) if max_score > 0 else 0
else:
score, max_score, pct = 0, sum(test_case["scoring"].values()), 0
details = {status: "FAIL"}
total_score += score
total_max += max_score
print(f"{score}/{max_score} ({pct}%) | {tokens:,} tok | ${cost:.4f} | {duration:.0f}s | {status}")
for k, v in details.items():
marker = "+" if "PASS" in str(v) else "-"
print(f" {marker} {k}: {v}")
ts = time.strftime("%Y-%m-%d %H:%M:%S")
detail_str = "; ".join(f"{k}={v}" for k, v in details.items())
tsv.write(f"{ts}\t{model}\tstandard/{test_name}\t{score}\t{max_score}\t{pct}\t{tokens}\t{cost:.4f}\t{duration:.0f}\t{status}\t{detail_str}\n")
tsv.flush()
results.append({
"model": model, "test": test_name, "score": score,
"max_score": max_score, "pct": pct, "status": status,
})
return total_score, total_max, results
def run_pipeline_tests(tsv):
"""Run implement-then-review pipeline tests with cross-model validation."""
print(f"\n{'='*70}")
print("PIPELINE TESTS (implement with model A, review with model B)")
print(f"{'='*70}")
total_score = 0
total_max = 0
results = []
for test_name, test_case in PIPELINE_TESTS.items():
for impl_model, review_model in PIPELINE_PAIRS:
print(f"\n--- {test_name} | impl={impl_model} → review={review_model} ---")
# Step 1: Implementation/Design
print(f" [1/2] Implementing with {impl_model}... ", end="", flush=True)
impl_text, impl_tok, impl_cost, impl_dur, impl_status = run_opencode(
model=impl_model,
agent="plan",
prompt=test_case["implement_prompt"],
)
if impl_status != "ok":
print(f"FAIL ({impl_status})")
score, max_score = 0, sum(test_case["scoring"].values())
details = {"implement": f"FAIL ({impl_status})"}
total_max += max_score
ts = time.strftime("%Y-%m-%d %H:%M:%S")
detail_str = "; ".join(f"{k}={v}" for k, v in details.items())
tsv.write(f"{ts}\t{impl_model}+{review_model}\tpipeline/{test_name}\t0\t{max_score}\t0\t{impl_tok}\t{impl_cost:.4f}\t{impl_dur:.0f}\timpl_{impl_status}\t{detail_str}\n")
tsv.flush()
continue
print(f"OK ({impl_tok:,} tok, {impl_dur:.0f}s)")
# Step 2: Cross-model review
review_prompt = test_case["review_prompt_template"].format(
design_output=impl_text[:3000] # Cap to avoid context overflow
)
print(f" [2/2] Reviewing with {review_model}... ", end="", flush=True)
review_text, review_tok, review_cost, review_dur, review_status = run_opencode(
model=review_model,
agent="plan",
prompt=review_prompt,
)
combined_text = review_text if review_status == "ok" else ""
total_tokens = impl_tok + review_tok
total_cost = impl_cost + review_cost
total_duration = impl_dur + review_dur
if review_status == "ok":
score, max_score, details = score_response(combined_text, test_case)
pct = round(score / max_score * 100) if max_score > 0 else 0
details["pipeline_complete"] = "PASS"
else:
score, max_score, pct = 0, sum(test_case["scoring"].values()), 0
details = {"review": f"FAIL ({review_status})"}
total_score += score
total_max += max_score
print(f"{score}/{max_score} ({pct}%) | {total_tokens:,} tok | ${total_cost:.4f} | {total_duration:.0f}s")
for k, v in details.items():
marker = "+" if "PASS" in str(v) else "-"
print(f" {marker} {k}: {v}")
ts = time.strftime("%Y-%m-%d %H:%M:%S")
detail_str = "; ".join(f"{k}={v}" for k, v in details.items())
tsv.write(f"{ts}\t{impl_model}+{review_model}\tpipeline/{test_name}\t{score}\t{max_score}\t{pct}\t{total_tokens}\t{total_cost:.4f}\t{total_duration:.0f}\t{'ok' if review_status == 'ok' else review_status}\t{detail_str}\n")
tsv.flush()
results.append({
"models": f"{impl_model}+{review_model}", "test": test_name,
"score": score, "max_score": max_score, "pct": pct,
})
return total_score, total_max, results
def run_fallback_test(tsv):
"""Test automatic model fallback chain."""
print(f"\n{'='*70}")
print("FALLBACK TEST (automatic model recovery)")
print(f"{'='*70}")
prompt = (
"[CONTEXT]\nUE5.7 C++ project.\n\n"
"[TASK]\nList the files in Source/VHS/AbilitySystem/. Keep under 100 words.\n\n"
"[SCOPE]\nONLY Source/VHS/AbilitySystem/."
)
print(f"\n--- fallback_chain ---")
text, tokens, cost, duration, status, used_model = run_with_fallback(
agent="plan",
prompt=prompt,
preferred_model="openai/gpt-5.4",
)
print(f" Result: {status} via {used_model} | {tokens:,} tok | {duration:.0f}s")
ts = time.strftime("%Y-%m-%d %H:%M:%S")
tsv.write(f"{ts}\t{used_model}\tfallback/chain_test\t{'1' if status == 'ok' else '0'}\t1\t{'100' if status == 'ok' else '0'}\t{tokens}\t{cost:.4f}\t{duration:.0f}\t{status}\tused_model={used_model}\n")
tsv.flush()
return (1 if status == "ok" else 0), 1
def run_loop(models, tests, tsv, max_iterations):
"""Autoresearch-style infinite improvement loop.
Runs tests repeatedly, tracking best scores per model/test combination.
Keeps running until interrupted or max_iterations reached.
"""
print(f"\n{'='*70}")
print(f"AUTORESEARCH LOOP MODE (max {max_iterations} iterations, Ctrl+C to stop)")
print(f"{'='*70}")
best_scores = {} # (model, test) -> best pct
iteration = 0
try:
while iteration < max_iterations:
iteration += 1
print(f"\n{'='*70}")
print(f"ITERATION {iteration}/{max_iterations}")
print(f"{'='*70}")
iter_score = 0
iter_max = 0
for test_name, test_case in tests.items():
for model in models:
key = (model, test_name)
print(f"\n--- [{iteration}] {test_name} | {model} ---")
print(f" Running... ", end="", flush=True)
text, tokens, cost, duration, status = run_opencode(
model=model,
agent=test_case["agent"],
prompt=test_case["prompt"],
files=test_case.get("files", []),
)
if status == "ok":
score, max_score, details = score_response(text, test_case)
pct = round(score / max_score * 100) if max_score > 0 else 0
else:
score, max_score, pct = 0, sum(test_case["scoring"].values()), 0
details = {status: "FAIL"}
iter_score += score
iter_max += max_score
# Track best score (autoresearch ratchet pattern)
prev_best = best_scores.get(key, 0)
improved = pct > prev_best
if improved:
best_scores[key] = pct
marker = "^" if improved else "=" if pct == prev_best else "v"
print(f"{score}/{max_score} ({pct}%) [{marker} best={best_scores.get(key, pct)}%] | {tokens:,} tok | {duration:.0f}s")
ts = time.strftime("%Y-%m-%d %H:%M:%S")
detail_str = "; ".join(f"{k}={v}" for k, v in details.items())
tsv.write(f"{ts}\t{model}\tloop[{iteration}]/{test_name}\t{score}\t{max_score}\t{pct}\t{tokens}\t{cost:.4f}\t{duration:.0f}\t{status}\t{detail_str}\n")
tsv.flush()
iter_pct = round(iter_score / iter_max * 100) if iter_max > 0 else 0
print(f"\n Iteration {iteration} total: {iter_score}/{iter_max} ({iter_pct}%)")
# Early stop if all tests are at 100%
if all(v == 100 for v in best_scores.values()) and len(best_scores) == len(tests) * len(models):
print(f"\n All tests at 100% -- stopping early.")
break
except KeyboardInterrupt:
print(f"\n\n Loop interrupted by user after {iteration} iterations.")
print(f"\n{'='*70}")
print(f"LOOP SUMMARY (best scores across {iteration} iterations):")
for (model, test), best_pct in sorted(best_scores.items()):
print(f" {model} / {test}: {best_pct}%")
print(f"{'='*70}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="Test the opencode skill")
parser.add_argument("--model", type=str, help="Test only this model")
parser.add_argument("--test", type=str, help="Run only this test category")
parser.add_argument("--quick", action="store_true", help="Fast subset (research only, fast models)")
parser.add_argument("--pipeline", action="store_true", help="Run pipeline tests (cross-model implement+review)")
parser.add_argument("--compare", action="store_true", help="Run all models on all tests for comparison")
parser.add_argument("--fallback", action="store_true", help="Test model fallback chain")
parser.add_argument("--loop", action="store_true", help="Autoresearch loop mode: run repeatedly")
parser.add_argument("--max-iter", type=int, default=10, help="Max iterations for loop mode (default: 10)")
parser.add_argument("--all", action="store_true", help="Run everything: standard + pipeline + fallback")
args = parser.parse_args()
models = [args.model] if args.model else MODELS
tests = {args.test: TEST_CASES[args.test]} if args.test else TEST_CASES
if args.quick:
models = ["openai/gpt-5.4-mini-fast"]
tests = {"research": TEST_CASES["research"]}
if args.compare:
models = ["openai/gpt-5.4", "openai/gpt-5.3-codex", "github-copilot/gpt-5.4"]
# Header
print(f"{'='*70}")
print(f"OpenCode Skill Test Harness (autoresearch-style)")
print(f"Models: {', '.join(models)}")
print(f"Tests: {', '.join(tests.keys())}")
if args.pipeline:
print(f"Pipeline pairs: {', '.join(f'{a}+{b}' for a, b in PIPELINE_PAIRS)}")
if args.loop:
print(f"Loop mode: max {args.max_iter} iterations")
print(f"{'='*70}")
# TSV log
write_header = not RESULTS_FILE.exists()
tsv = open(RESULTS_FILE, "a", encoding="utf-8")
if write_header:
tsv.write("timestamp\tmodel\ttest\tscore\tmax_score\tpercent\ttokens\tcost\tduration\tstatus\tdetails\n")
total_score = 0
total_max = 0
# Loop mode
if args.loop:
run_loop(models, tests, tsv, args.max_iter)
tsv.close()
return 100 # Loop mode doesn't have a single score
# Standard tests
if not args.pipeline or args.all:
std_score, std_max, _ = run_standard_tests(models, tests, tsv)
total_score += std_score
total_max += std_max
# Pipeline tests
if args.pipeline or args.all:
pipe_score, pipe_max, _ = run_pipeline_tests(tsv)
total_score += pipe_score
total_max += pipe_max
# Fallback test
if args.fallback or args.all:
fb_score, fb_max = run_fallback_test(tsv)
total_score += fb_score
total_max += fb_max
tsv.close()
# Summary
total_pct = round(total_score / total_max * 100) if total_max > 0 else 0
print(f"\n{'='*70}")
print(f"TOTAL: {total_score}/{total_max} ({total_pct}%)")
print(f"Results appended to: {RESULTS_FILE}")
print(f"{'='*70}")
return total_pct
if __name__ == "__main__":
sys.exit(0 if main() >= 50 else 1)
SKILL.md
---
name: opencode
description: >
Invoke OpenCode CLI as a sub-agent to leverage alternative AI models (GPT-5.x, Codex,
local models) for code implementation, review, debugging, research, and second opinions.
Use when the user asks to "use opencode", "get a second opinion from GPT/Codex",
"implement with opencode", "review with opencode", "let opencode handle this",
"use a different model", or wants cross-model validation. Also triggers when the user
explicitly names an OpenCode model (e.g., "use gpt-5.4", "use codex")
or wants to delegate a task to a non-Claude AI system. Supports all providers configured
in the user's OpenCode installation (OpenAI, GitHub Copilot, local models via
Ollama/LM Studio, and OpenCode's own free-tier models).
---
# OpenCode Sub-Agent Protocol
This is an executable protocol for delegating work to OpenCode agents. Follow it precisely.
## Prerequisites
- `opencode` CLI installed and in PATH (verify: `opencode --version`)
- At least one provider authenticated (`opencode providers list`)
- Available models listed via `opencode models`
## Core Command
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json --model <provider/model> --agent <build|plan> --dir "<working-dir>" --dangerously-skip-permissions [options] "<prompt>" 2>/dev/null
```
Key flags:
- `--format json` -- machine-parseable ndjson output (always use this)
- `--model provider/model` -- e.g., `openai/gpt-5.4`, `openai/gpt-5.3-codex`
- `--variant <level>` -- reasoning effort: `minimal`, `medium`, `high`, `xhigh` (provider-specific)
- `--dir <path>` -- working directory for the task
- `--dangerously-skip-permissions` -- auto-approve tool calls (required for headless)
- `-f/--file <path>` -- attach file(s) to the prompt
- `--agent <name>` -- `build` for implementation (filesystem+bash), `plan` for analysis (read-only)
- `-c/--continue` -- continue last session
- `-s/--session <id>` -- continue specific session
## Output Parsing
The `--format json` flag emits ndjson (one JSON object per line):
| type | Contains |
|------|----------|
| `text` | `.part.text` -- model's text response |
| `tool_call` | `.part.name`, `.part.input` -- tool invocations |
| `tool_result` | `.part.output` -- tool execution results |
| `step_start` | Session/message IDs |
| `step_finish` | `.part.tokens` (usage), `.part.cost`, `.part.reason` |
Parse with the helper script (always redirect stderr):
```bash
opencode run --format json ... 2>/dev/null | python .claude/skills/opencode/scripts/parse_output.py [--mode text|full|tools|cost|session|diff|summary]
```
Parse modes:
- `text` -- model's text response (default)
- `full` -- text + tool calls + tool results
- `tools` -- tool calls and results only
- `cost` -- token usage and cost breakdown
- `session` -- extract session ID for continuation
- `diff` -- extract file modifications from tool calls
- `summary` -- one-line summary (status, tokens, cost, duration)
---
## Task Protocols
Determine the task type from the user's request, then follow the matching protocol.
### Protocol 1: Implement
Use when the user wants OpenCode to write or modify code.
**Steps:**
1. **Scope** -- identify exactly which files to create/modify. List them explicitly.
2. **Context** -- gather files the agent needs to see. Attach with `-f`.
3. **Constraints** -- build the constraint block (see Prompt Engineering below).
4. **Execute** -- run with `--agent build`:
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.4 --agent build \
--dir "C:/UnrealEngine/VHS" --dangerously-skip-permissions \
-f <file1> -f <file2> \
"<prompt>" 2>/dev/null | python .claude/skills/opencode/scripts/parse_output.py --mode full
```
5. **Validate** -- read every modified file. Check correctness, conventions, compilation.
6. **Report** -- summarize what changed, flag anything suspicious.
**Default model:** `openai/gpt-5.4` | **Timeout:** `timeout: 300000`
### Protocol 2: Review
Use when the user wants a second opinion on code quality, bugs, or correctness.
**Steps:**
1. **Target** -- identify files to review.
2. **Execute** -- run with `--agent plan` (read-only):
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.4 --variant high --agent plan \
--dir "C:/UnrealEngine/VHS" --dangerously-skip-permissions \
-f <file1> "<review prompt>" 2>/dev/null | python .claude/skills/opencode/scripts/parse_output.py
```
3. **Present** -- relay findings to user with file:line references.
**Default model:** `openai/gpt-5.4` + `--variant high` | **Timeout:** `timeout: 300000`
### Protocol 3: Debug
Use when the user wants OpenCode to investigate a bug or error.
**Steps:**
1. **Evidence** -- gather error messages, stack traces, relevant code.
2. **GAS context** -- for GAS bugs, instruct the agent to check: tag blocking/requirements, effect application/removal ordering, MMC dependencies, ASC initialization timing.
3. **Execute** -- run with `--agent plan`:
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.3-codex --variant high --agent plan \
--dir "C:/UnrealEngine/VHS" --dangerously-skip-permissions \
"<diagnostic prompt>" 2>/dev/null | python .claude/skills/opencode/scripts/parse_output.py
```
4. **Analyze** -- cross-reference findings with actual code before presenting.
**Default model:** `openai/gpt-5.3-codex` + `--variant high` | **Timeout:** `timeout: 300000`
### Protocol 4: Research
Use when the user wants OpenCode to explore and analyze the codebase.
**Steps:**
1. **Question** -- formulate a clear, bounded research question.
2. **Execute** -- run with `--agent plan`:
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.4-mini-fast --agent plan \
--dir "C:/UnrealEngine/VHS" --dangerously-skip-permissions \
"<research question>" 2>/dev/null | python .claude/skills/opencode/scripts/parse_output.py
```
3. **Verify** -- spot-check claims by reading referenced files yourself.
**Default model:** `openai/gpt-5.4-mini-fast` | **Timeout:** `timeout: 300000`
---
## Orchestration Patterns
These patterns compose the basic protocols above into more powerful workflows.
Use them when the task benefits from multi-step validation or parallel execution.
### Pattern A: Implement-Then-Review Pipeline
**When:** Implementation tasks where quality matters. This is the default for non-trivial implementation.
**Protocol:**
1. Run **Protocol 1 (Implement)** with `--agent build`
2. Read and verify the changes yourself (quick sanity check)
3. Run **Protocol 2 (Review)** on the changed files, using a DIFFERENT model:
- If implementation used `openai/gpt-5.4`, review with `openai/gpt-5.3-codex`
- Cross-model review catches model-specific blind spots
4. **Quality gate:** If review finds critical issues, fix them (either yourself or re-run implement with fix instructions)
5. Report both the implementation and review results to user
### Pattern B: Parallel Multi-Model Review
**When:** The user wants thorough validation, or says "get multiple opinions".
**Protocol:**
1. Construct the review prompt once
2. Spawn parallel Bash commands (use `run_in_background` or `&`):
```bash
# Run in parallel -- each to a temp file
PROMPT="<review prompt>"
DIR="C:/UnrealEngine/VHS"
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.4 --variant high --agent plan \
--dir "$DIR" --dangerously-skip-permissions "$PROMPT" \
2>/dev/null > /tmp/review_gpt54.txt &
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.3-codex --variant high --agent plan \
--dir "$DIR" --dangerously-skip-permissions "$PROMPT" \
2>/dev/null > /tmp/review_codex.txt &
wait
```
3. Parse each result separately:
```bash
cat /tmp/review_gpt54.txt | python .claude/skills/opencode/scripts/parse_output.py
cat /tmp/review_codex.txt | python .claude/skills/opencode/scripts/parse_output.py
```
4. **Synthesize** -- identify issues raised by multiple models (high confidence) vs. single model (lower confidence). Present a unified report.
### Pattern C: Autonomous Improvement Loop
**When:** The user wants iterative code improvement, or says "keep improving this", "optimize", "polish".
Inspired by autoresearch's iterative experiment loop. The agent makes changes, validates them, keeps improvements, and reverts failures.
**Protocol:**
```
LOOP (max N iterations, default 3):
1. Identify the current quality baseline (read code, note issues)
2. Construct an improvement prompt targeting the worst issue
3. Run Protocol 1 (Implement) with --agent build
4. Validate: read changed files, check for regressions
5. Run Protocol 2 (Review) on changed files with a different model
6. QUALITY GATE:
- If review passes (no critical issues) → keep changes, report improvement
- If review fails (critical issues found) → revert changes (git checkout the files), report why
7. If no more meaningful improvements found → STOP
8. Continue to next iteration
```
**Important constraints:**
- Always set a maximum iteration count (default 3, user can specify more)
- Each iteration must target a specific, measurable improvement
- Revert on failure -- never accumulate broken changes
- Stop early if improvements become marginal
- Report each iteration's outcome so the user can follow progress
### Pattern D: Background Agent
**When:** Claude Code wants to continue other work while OpenCode runs. Use for long-running tasks that don't block the conversation.
**Protocol:**
1. Run the OpenCode command using Bash with `run_in_background: true`:
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.4 --agent plan \
--dir "C:/UnrealEngine/VHS" --dangerously-skip-permissions \
"<prompt>" 2>/dev/null > /tmp/opencode_result.txt
```
2. Continue with other work while waiting for notification
3. When notified, parse the result:
```bash
cat /tmp/opencode_result.txt | python .claude/skills/opencode/scripts/parse_output.py
```
4. Present findings to user
### Pattern E: Session Continuation (Multi-Step Workflow)
**When:** Complex tasks that benefit from building context across multiple exchanges.
**Protocol:**
1. Run the first task and capture the session ID:
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--model openai/gpt-5.4 --agent plan \
--dir "C:/UnrealEngine/VHS" --dangerously-skip-permissions \
"<first task>" 2>/dev/null | tee /tmp/oc_step1.txt | python .claude/skills/opencode/scripts/parse_output.py --mode session
```
2. Use the session ID for follow-up tasks:
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--session "<session-id>" --dangerously-skip-permissions \
"<follow-up task>" 2>/dev/null | python .claude/skills/opencode/scripts/parse_output.py
```
Or use `--continue` for the last session:
```bash
OPENCODE_DISABLE_AUTOUPDATE=true opencode run --format json \
--continue --dangerously-skip-permissions \
"<follow-up task>" 2>/dev/null | python .claude/skills/opencode/scripts/parse_output.py
```
### Pattern F: Git-Checkpoint Implementation
**When:** Risky implementations where rollback safety is important.
Inspired by autoresearch's git-as-state-machine pattern.
**Protocol:**
1. **Checkpoint** -- note the current git state:
```bash
git stash # or commit current work
```
2. Run **Protocol 1 (Implement)** with `--agent build`
3. **Validate** -- read all changed files, run build/tests if applicable
4. **Gate:**
- If changes are correct → keep them, report success
- If changes are wrong → revert:
```bash
git checkout -- <modified-files>
```
- Report what went wrong and why the revert happened
5. **Never leave broken changes** -- if validation fails, always revert before reporting
---
## Prompt Engineering
OpenCode has NO access to this conversation's context. Every prompt must be **fully self-contained**.
Do NOT use OpenCode to invoke Claude/Anthropic models or Google/Gemini models -- these providers are
not available in OpenCode. Only use OpenAI, GitHub Copilot, local, and OpenCode's own models.
### Prompt Structure (Required)
Every prompt sent to OpenCode must follow this structure:
```
[CONTEXT BLOCK]
<project type, conventions, key constraints>
[TASK]
<imperative description of what to do>
[SCOPE]
<explicit boundaries: which files/dirs to touch, which to ignore>
[CONSTRAINTS]
<naming conventions, patterns to follow, things to avoid>
[OUTPUT FORMAT]
<what form the response should take>
```
### UE5/VHS Context Block (Always Include for Project Tasks)
```
This is an Unreal Engine 5.7 C++ project (VHS - first-person horror game).
Source: Source/VHS/ | Module: VHS | No Public/Private split (side-by-side .h/.cpp)
Naming conventions:
- UE prefixes: A (actors), U (UObjects), F (structs), E (enums), I (interfaces)
- GAS: GA_ (abilities), GC_ (cues), MMC_ (magnitude calcs), GE_ (effects)
- Booleans: b prefix (bIsRecovering)
- #pragma once, CoreMinimal.h first, .generated.h last
Key systems: GAS (AbilitySystem/), Enhanced Input, StateTree (AI)
Dependencies: GameplayAbilities, GameplayTags, GameplayTasks, EnhancedInput, StateTreeModule
```
### Scope Constraints (Critical for Avoiding Timeouts)
Always set explicit scope boundaries. Broad prompts cause the agent to explore the entire filesystem and timeout.
**Good:**
```
Search ONLY in Source/VHS/AbilitySystem/. Do NOT explore other directories.
Modify ONLY HorrorCharacter.h and HorrorCharacter.cpp. Do NOT touch other files.
```
**Bad:**
```
Look through the project and find issues. (too broad -- will timeout)
```
### Implementation Prompt Template
```
[CONTEXT]
This is an Unreal Engine 5.7 C++ project (VHS). <conventions block>
[TASK]
Implement <feature> in <file(s)>.
<detailed requirements>
[SCOPE]
- Create/modify ONLY: <explicit file list>
- Reference (read-only): <files to read for context>
- Do NOT touch: <exclusions>
[CONSTRAINTS]
- Follow existing patterns in <reference file>
- Use UPROPERTY(EditDefaultsOnly, Category = "<Category>") for configurable values
- All new UObject classes need UCLASS(ClassGroup=(VHS)) macro
- Header: #pragma once, CoreMinimal.h first, .generated.h last
[OUTPUT]
Write the implementation directly. No explanations needed.
```
### Review Prompt Template
```
[CONTEXT]
This is an Unreal Engine 5.7 C++ project using GAS. <conventions block>
[TASK]
Review the following files for: correctness, bugs, performance, security, UE5 best practices.
Be specific: cite file paths and line numbers. Suggest concrete fixes.
[SCOPE]
Review ONLY: <file list>
Do NOT explore other directories.
[OUTPUT FORMAT]
For each issue found:
- **[SEVERITY]** (critical/warning/info)
- **File:Line** -- specific location
- **Issue** -- what's wrong
- **Fix** -- concrete solution
```
### Debug Prompt Template
```
[CONTEXT]
UE5.7 project (VHS) using GAS. <conventions block>
[BUG REPORT]
<symptom description>
<error messages / stack traces>
<reproduction steps if known>
[TASK]
Investigate root cause. Check:
- <specific things to check based on the bug>
- For GAS bugs: tag blocking, effect ordering, MMC deps, ASC init timing
[SCOPE]
Search ONLY in: <directory>
[OUTPUT FORMAT]
1. Root cause (most likely)
2. Evidence (file:line references)
3. Fix (concrete code changes)
```
---
## Model Selection
Pick the right model for the task. See `references/models.md` for the full guide.
**Quick defaults:**
| Task | Fast | Default | Deep |
|------|------|---------|------|
| Implementation | `openai/gpt-5.4-mini-fast` | `openai/gpt-5.4` | `openai/gpt-5.3-codex` |
| Review | `openai/gpt-5.4-mini` | `openai/gpt-5.4` + `--variant high` | `openai/gpt-5.3-codex` + `--variant high` |
| Debug | `openai/gpt-5.4-mini-fast` | `openai/gpt-5.3-codex` + `--variant high` | `openai/gpt-5.4` + `--variant xhigh` |
| Research | `openai/gpt-5.4-mini-fast` | `openai/gpt-5.4-fast` | `openai/gpt-5.4` |
Use `opencode models` to discover all available models. Only OpenAI, GitHub Copilot, OpenCode's own
free-tier models, and local models (Ollama/LM Studio) are supported.
**Cross-model review pairings** (for Pipeline Pattern A):
- Implement with `openai/gpt-5.4` → Review with `openai/gpt-5.3-codex`
- Implement with `openai/gpt-5.3-codex` → Review with `openai/gpt-5.4`
- Always use a different model for review than implementation
**Cost-sensitive alternatives:**
- `github-copilot/gpt-5.4` -- free with Copilot subscription
- `github-copilot/gpt-5.3-codex` -- free with Copilot subscription
- `opencode/big-pickle` -- OpenCode's own free tier
- Local: `ollama/qwen3-coder:latest` -- zero API cost
---
## Error Handling & Recovery
### Error Detection
| Symptom | Cause | Recovery |
|---------|-------|----------|
| `opencode` not found | Not installed | Tell user: `npm i -g opencode` |
| No providers configured | No auth | Tell user: `opencode providers login` |
| Model not available | Wrong model ID | Run `opencode models`, suggest alternatives |
| Timeout (>3 min) | Scope too broad | Narrow scope constraints, use faster model |
| JSON parse failure | Corrupt output | Retry with `--format default`, read raw text |
| Exit code non-zero | Runtime error | Check stderr (up to 500 chars), retry once |
| Empty text response | Agent did only tool calls | Use `--mode full` to see tool call results |
### Automatic Model Fallback
If a model fails or times out, fall back in this order:
1. `openai/gpt-5.4` (primary)
2. `openai/gpt-5.3-codex` (code-optimized)
3. `openai/gpt-5.4-mini-fast` (fastest, always works)
4. `github-copilot/gpt-5.4` (free tier)
5. `opencode/big-pickle` (OpenCode free tier)
### Crash Recovery
If OpenCode crashes mid-implementation (`--agent build`):
1. Check which files were modified: `git diff --name-only`
2. Read each modified file to assess completeness
3. If changes are partial or broken → `git checkout -- <files>`
4. Retry with a narrower scope or different model
5. Report the crash and recovery to the user
---
## Decision Framework
Use this to decide WHEN and HOW to invoke OpenCode:
```
User request received
│
├─ "use opencode" / names a model → Use OpenCode (user explicit)
│
├─ "second opinion" / "cross-validate" → Pattern B (Parallel Multi-Model)
│
├─ "implement with X" → Protocol 1, optionally Pattern A (Implement-Then-Review)
│
├─ "review with X" → Protocol 2
│
├─ "keep improving" / "optimize" / "polish" → Pattern C (Autonomous Loop)
│
├─ Complex implementation → Pattern A (Implement-Then-Review) by default
│
├─ Risky changes → Pattern F (Git-Checkpoint)
│
└─ Long-running task, user wants to continue chatting → Pattern D (Background)
```
## Advanced Patterns
See `references/advanced.md` for:
- Server mode for persistent sessions
- Environment variables for automation
- Export/import sessions
- Stats and cost tracking