Persistent, anti-hallucination Plan & Todo state for OpenCode - modeled on how Antigravity and GitHub Copilot handle planning.
One strict, versioned JSON file per project (.opencode/todo.json) is the single source of truth. It is re-read from disk before every AI reply, injected into the context verbatim before session compaction, and - most importantly - the AI can only change it through deterministic custom tools that validate the schema in code.
Context window = RAM (volatile). Filesystem = disk (persistent). This plugin moves your plan and task state out of the context window and onto disk.
- Strict JSON > free-form Markdown. The AI cannot quietly rewrite your plan: every write is schema-validated by the plugin, not by the model's mood.
- Zero hallucination on reset / compact / new session. State is never reconstructed from memory. A fresh session is built exclusively from what is really on disk.
- Works in every project, no setup. Install once globally; the plugin creates the file, runs a deterministic repo audit, and gets out of the way.
- File stays local. By default the todo file is appended to
.git/info/exclude- no merge conflicts, no repo pollution.
| OpenCode hook | What the plugin does |
|---|---|
| Plugin init | Creates .opencode/todo.json if missing (strict schema). On first run, runs todo_audit and seeds an initial plan from real repo facts. |
experimental.chat.system.transform |
Re-reads the JSON from disk and injects it before every reply, together with operating rules. The AI is told the file is authoritative and must be changed only via the tools. |
experimental.session.compacting |
Injects the real JSON verbatim into the compaction prompt, so the summary is generated from the file - never from the model's memory. |
event (session.*, todo.updated) |
Re-validates / re-creates the file across session boundaries; best-effort sync of OpenCode's native todo tool into the JSON. |
tool.execute.after |
When a test / build / lint / check bash command succeeds, auto-completes the in_progress task and records it in history. |
OpenCode auto-discovers plugin files (not folders) in ~/.config/opencode/plugins/*.ts / *.js. A cloned plugin folder is loaded through a path reference in the config: "plugin": ["C:/path/to/opencode-todo-sync"] (absolute path or file:// URL). The folder must contain a package.json with "main": "./src/index.ts" (already the case here), and the plugin needs @opencode-ai/plugin resolvable from ~/.config/opencode/node_modules.
macOS / Linux (bash):
mkdir -p ~/.config/opencode/plugins ~/.config/opencode/command
git clone --depth 1 https://github.com/otontraore/opencode-todo-sync ~/.config/opencode/plugins/opencode-todo-sync
cp ~/.config/opencode/plugins/opencode-todo-sync/command/OTON.md ~/.config/opencode/command/OTON.md
# Register the folder + give it access to @opencode-ai/plugin
cat >> ~/.config/opencode/opencode.json <<'EOF'
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["$HOME/.config/opencode/plugins/opencode-todo-sync"]
}
EOF
printf '{"name":"opencode-global","private":true,"dependencies":{"@opencode-ai/plugin":"^1.15.10"}}\n' > ~/.config/opencode/package.json
cd ~/.config/opencode && npm installWindows PowerShell:
New-Item -ItemType Directory -Force "$env:USERPROFILE\.config\opencode\plugins","$env:USERPROFILE\.config\opencode\command" | Out-Null
git clone --depth 1 https://github.com/otontraore/opencode-todo-sync "$env:USERPROFILE\.config\opencode\plugins\opencode-todo-sync"
Copy-Item "$env:USERPROFILE\.config\opencode\plugins\opencode-todo-sync\command\OTON.md" "$env:USERPROFILE\.config\opencode\command\OTON.md"
# Register the folder + give it access to @opencode-ai/plugin
$pluginPath = "$env:USERPROFILE\.config\opencode\plugins\opencode-todo-sync"
@{ '$schema' = 'https://opencode.ai/config.json'; plugin = @($pluginPath) } | ConvertTo-Json | Set-Content "$env:USERPROFILE\.config\opencode\opencode.json" -Encoding utf8
'{ "name": "opencode-global", "private": true, "dependencies": { "@opencode-ai/plugin": "^1.15.10" } }' | Set-Content "$env:USERPROFILE\.config\opencode\package.json" -Encoding utf8
Push-Location "$env:USERPROFILE\.config\opencode"; npm install; Pop-LocationThe PowerShell snippet overwrites opencode.json with the plugin entry - merge it into your existing config by hand if you already have one (just add the plugin array). Same for creating package.json if it already exists (add the dependency).
Prerequisites: git, Node.js 18+ (or Bun), OpenCode.
Then restart OpenCode.
Verify with the chat command:
/OTON
You should see Plugin opencode-todo-sync : integre et fonctionnel together with a project summary. /OTON stat adds the full report (last usage, last modified file, remaining todos, coherence verdict). The plugin creates and seeds .opencode/todo.json on first use in each project.
You may rename the /OTON command by editing ~/.config/opencode/command/OTON.md (e.g. TODO.md) or adding your own command in the project's .opencode/command/ folder.
Clone the plugin into the project's .opencode/plugin/opencode-todo-sync/ folder and register it in the project's opencode.json, then create .opencode/package.json for the dependency:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [".opencode/plugin/opencode-todo-sync"]
}{ "name": "project-opencode", "private": true, "dependencies": { "@opencode-ai/plugin": "^1.15.10" } }Also copy command/OTON.md into .opencode/command/OTON.md. OpenCode runs dependency install at startup; npm install in .opencode/ also works.
Download opencode-todo-sync.zip from the Releases page (tagged v*), unzip it into ~/.config/opencode/plugins/opencode-todo-sync/, then follow the config + package.json + install steps above. The zip contains src/ only - the plugin runs from source, no build step required.
Add it to your global config so it applies to every project:
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-todo-sync"]
}Or per-project in opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-todo-sync"]
}With options:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["opencode-todo-sync", { "language": "fr", "autoAudit": true }]
]
}Restart OpenCode after install. Config and plugins are loaded once at startup. Requires Node 18+ / Bun (OpenCode bundles its own).
- Open the project folder in OpenCode. Nothing else to do.
- The plugin creates
.opencode/todo.jsonand - on the very first run - runs a deterministic audit (git ls-files, manifests, CI, Docker, structure) and seeds a proposed plan ofpendingtasks.todo_auditonly ever reports real facts; it never invents anything. - Review/confirm the plan (or run
todo_auditagain), then let the agent work through the tasks. Progress is written to disk every step of the way.
No manual setup. No configuration files in your repo.
| Option | Type | Default | Description |
|---|---|---|---|
filePath |
string |
.opencode/todo.json |
Location of the todo file (relative to the project root or absolute). |
language |
"en" | "fr" |
"en" |
Language of the injected system rules and auto-generated audit tasks. |
autoAudit |
boolean |
true |
Run the deterministic repo audit to seed an initial plan on first run. |
autoComplete |
boolean |
true |
Auto-complete the in_progress task when a test/build/lint command succeeds. |
commandPatterns |
string (regex alternation) |
"test|build|compile|lint|check|verify" |
Pattern matched against bash commands for autoComplete. |
injectRules |
boolean |
true |
Inject the operating rules alongside the state every turn. |
syncNativeTodo |
boolean |
true |
Best-effort sync of OpenCode's native todoread/todowrite into the JSON file. |
ignoreMode |
"gitinfo" | "none" |
"gitinfo" |
gitinfo appends the file to .git/info/exclude (local only). none leaves the file visible to git. |
The agent changes state only through these tools. Each validates input against the strict schema before writing.
| Tool | Purpose |
|---|---|
todo_read |
Read the current plan, tasks (filterable by status / phase_id) and recent history. |
todo_add |
Add one or more schema-validated tasks. |
todo_update |
Update a task: status, priority, text, tags, labels, phase_id, estimates. |
todo_done |
Mark a task completed, promote the next highest-priority pending task to in_progress, set current_focus, log history. |
todo_audit |
Deterministic repo scan → proposed plan. apply: true replaces the plan/tasks with the proposal. |
todo_stat |
Deterministic health report: last usage (mtime + last_updated), last modified file, remaining todos and a coherence / up-to-date verdict (schema, enums, unique ids, phase references, plan-sync) - used by /OTON stat, so the "everything is coherent" verdict is computed by code, never by the model's judgement. |
| Command | What it does |
|---|---|
/OTON |
Short report: calls todo_stat + todo_read, confirms the plugin is wired in, summarizes the project (stack, plan phase, current focus, task statuses). |
/OTON stat |
Adds the full block: last usage (date/time), last modified file, all remaining todos (priority + phase), coherence verdict, up-to-date flag. |
The command lives at command/OTON.md in this repo (and in ~/.config/opencode/command/OTON.md after the Quick start). Customize or rename it freely - add TODO.md variants, add your own arguments, or translate the prompt.
File: .opencode/todo.json. Versioned (schema_version: 1). Enums are fixed, so tooling can validate deterministically.
The TypeScript types (TodoDocument, TodoTask, PlanPhase, …) are exported from opencode-todo-sync/schema so external tooling can validate or extend the file:
import { sanitizeDocument, isTodoDocument } from "opencode-todo-sync/schema"- The file is re-read from disk before every reply. Whatever the model "remembers" is irrelevant - it works from what is on disk.
- Compaction is fed the real file, verbatim, and is explicitly told not to rewrite it. The summary is a summary of facts.
- After a reset or compact, the fresh context is rebuilt only from the file. The plugin re-creates/repairs it on
session.created/session.compactedif anything happened to it. todo_auditonly emits real repo facts (files, manifests, CI, Docker, structure) aspendingproposals. It never infers state from memory.- Corrupted or deleted file → rebuilt from an empty, neutral schema. No hallucinated data is ever restored.
- Every write is validated in code. The AI cannot hand-write JSON; it must go through the
todo_*tools.
.opencode/todo.jsonis local-only by default (gitinfomode) → no merge conflicts; every developer gets their own copy.- Want a shared board? Set
ignoreMode: "none"and commit the file deliberately - every agent on the team then works from the same tracked plan. - Copy a plan across machines by copying the file itself.
npm install # or: bun install
npm run typecheck # strict TypeScript check
npm run build # compile to dist/
npm test # smoke tests (node >= 18, uses tsx --test)Releasing:
- Push a
v*tag (e.g.v0.1.0): the release workflow typechecks, tests, builds, assemblesopencode-todo-sync.zip(src + dist + README + LICENSE) and creates a GitHub Release with the zip and npm tarball attached. - npm publishing uses the publish workflow and the
NPM_TOKENsecret once the package is published atopencode-todo-sync.
Zero runtime dependencies.
MIT © 2026 OTON TRAORE
{ "schema_version": 1, "metadata": { "project": "my-app", "branch": "main", "created_at": "2026-08-20T09:00:00.000Z", "last_updated": "2026-08-20T09:15:00.000Z", "current_focus": "Add user authentication", "stage": "implementing" // planning | implementing | reviewing | done }, "plan": [ { "id": 1, "title": "Architecture & Audit", "status": "in_progress", "created_at": "...", "updated_at": "..." } ], "tasks": [ { "id": 1, "text": "Explore the codebase structure and map the core modules", "status": "in_progress", // pending | in_progress | completed | blocked "priority": "high", // low | medium | high "phase_id": 1, "tags": ["audit"], "labels": [], "created_at": "...", "updated_at": "...", "estimated_min": 30, "actual_min": null } ], "history": [ { "ts": "2026-08-20T09:15:00.000Z", "event": "completed", "detail": "#1 Explore the codebase structure" } ] }