iAI is a monorepo that produces one npm package (for opencode) and one plugin directory (for Claude Code) from a single source of truth.
The governing constraint, from research into both hosts:
Claude Code hooks are external processes (stdin JSON → exit code + stdout JSON). opencode hooks are in-process TypeScript functions that mutate an
outputobject and block bythrow.No hook code can be shared directly. Therefore: a pure core with two thin adapter shims.
But the single biggest lever cuts the other way:
opencode natively reads
.claude/skills/<name>/SKILL.md.
So every skill is written once, lives in skills/, is installed to
.claude/skills/, and both hosts load the identical file. Skills are the bulk of
iAI. Only hooks, agents and commands need per-host generation.
┌──────────────────────────────────────────────────────────────────┐
│ SURFACE Claude Code │ opencode │
├────────────────────┬───────────────────────┼──────────────────────┤
│ Skills │ .claude/skills/**/SKILL.md (SHARED, 1 copy)│
├────────────────────┼───────────────────────┼──────────────────────┤
│ Agents │ .claude/agents/*.md │ .opencode/agents/*.md│
│ Commands │ .claude/commands/ │ .opencode/commands/ │
│ Hooks │ hooks.json + procs │ plugin[] + Hooks{} │
│ │ ▲ │ ▲ │
│ │ adapter-claude │ adapter-opencode │
├────────────────────┴───────────┬───────────┴──────────────────────┤
│ CORE (pure TypeScript, no host imports) │
│ intent · routing · guards · classification · gh · evidence │
├───────────────────────────────────────────────────────────────────┤
│ DOMAIN PACKS dev · trade · health · wealth · know │
│ each exports a DomainBinding {unit, verify, gate, evidence} │
├───────────────────────────────────────────────────────────────────┤
│ SYSTEM OF RECORD GitHub (issues, milestones, PRs) + git repo │
│ SIDECAR Pulse daemon :31337 (optional, read-only UI) │
└───────────────────────────────────────────────────────────────────┘
| Target | Type | Build file | Source dirs |
|---|---|---|---|
| iai-core | library | packages/core/package.json | packages/core/src |
| iai-adapter-opencode | library | packages/adapter-opencode/package.json | packages/adapter-opencode/src |
| iai-adapter-claude | library | packages/adapter-claude/package.json | packages/adapter-claude/src |
| iai-exec | library | packages/exec/package.json | packages/exec/src |
| iai-harness | library | packages/harness/package.json | packages/harness/src |
| iai-installer | binary | packages/installer/package.json | packages/installer/src |
| iai-domain-dev | library | packages/domain-dev/package.json | packages/domain-dev/src |
| iai-domain-trade | library | packages/domain-trade/package.json | packages/domain-trade/src |
| iai-domain-health | library | packages/domain-health/package.json | packages/domain-health/src |
| iai-domain-wealth | library | packages/domain-wealth/package.json | packages/domain-wealth/src |
| iai-domain-know | library | packages/domain-know/package.json | packages/domain-know/src |
| iai-domain-null | library | packages/domain-null/package.json | packages/domain-null/src |
| iai-pulse | binary | packages/pulse/package.json | packages/pulse/src |
| iai-skills | docs | — | skills |
| iai-agents | docs | — | agents |
| iai-references | docs | — | references |
Type is one of library, binary, docs. docs targets have no build file;
they are validated by the frontmatter linter rather than compiled.
No host imports. No process.cwd(). Everything is a pure function from input to a
decision object that an adapter applies.
| Module | Responsibility |
|---|---|
intent/ |
Goals parsing, Design read/write, Current→Ideal delta, goal→milestone mapping |
routing/ |
Category → model chain resolution, reasoning level, fallbacks |
guards/ |
Pure predicates: checkEgress, checkRiskMandate, checkSpend, checkCommitPrefix |
classify/ |
Data classification (PUBLIC / INTERNAL / PRIVATE / SECRET) |
gh/ |
gh command construction + response parsing. Never shells out itself |
evidence/ |
Artifact envelope, sentinel comments, permalink pinning, 60k budget |
binding/ |
DomainBinding interface and registry |
The central type:
export interface DomainBinding {
id: "dev" | "trade" | "health" | "wealth" | "know"
unitOfWork: UnitSpec // what a Task is in this domain
verify: VerifySpec // what "passing" means, and the rungs to get there
gate: GateSpec // the irreversible action, and who may authorise it
evidence: EvidenceSpec // artifact kind, sentinel, path template
labels: { namespace: string; extra: LabelDef[] }
}Exports a named Plugin per the opencode contract:
export const iAI: Plugin = async ({ client, project, directory, worktree, $ }) => ({
"chat.message": async (i, o) => { /* intent gate, ultrawork keyword */ },
"chat.params": async (i, o) => { /* category → model, reasoning level */ },
"tool.execute.before": async (i, o) => { /* guards; throw = block */ },
"tool.execute.after": async (i, o) => { /* evidence capture */ },
"permission.ask": async (i, o) => { /* risk mandate auto-deny */ },
event: async ({ event }) => { /* session.idle → checkpoint */ },
"experimental.chat.system.transform": async (i, o) => { /* Goals injection */ },
})opencode-only capabilities that Claude Code cannot match — and which we therefore
treat as enhancements, never load-bearing: chat.params model rewriting,
tool.definition rewriting, experimental.chat.system.transform.
A set of tiny CLI binaries registered in hooks/hooks.json. Each reads JSON on
stdin, calls the same core function, and writes stdout JSON + an exit code.
| Claude Code event | opencode equivalent | Core function |
|---|---|---|
SessionStart |
event → session.created |
session.start() |
UserPromptSubmit |
chat.message |
intent.gate() |
PreToolUse |
tool.execute.before |
guards.evaluate() |
PostToolUse |
tool.execute.after |
evidence.capture() |
Stop |
event → session.idle |
session.stop() |
PreCompact |
experimental.session.compacting |
context.preserve() |
Exit code 2 blocks in Claude Code; throw blocks in opencode. Both map from the
same Decision { action: "allow" | "warn" | "block", message }.
npx iai install [--host claude|opencode|both]. Writes host-native artifacts:
generates agent markdown in each dialect (model IDs differ:
"opus" vs "amd-anthropic/Claude-Opus-5"), merges hook registrations without
clobbering, and links skills/ into place. Dry-run by default — --apply
required, a lesson taken directly from LifeOS's install tooling.
Each pack is a library exporting one DomainBinding plus any domain tools. Packs
never import each other. Cross-domain work is expressed as a Story dependency, not
a code dependency.
Optional read-only dashboard on :31337, modelled on LifeOS's Pulse. Renders the
GitHub state, the Design tree, and per-domain surfaces. Never writes. If absent,
everything still works — /iai:status is the text-mode equivalent.
Filesystem + GitHub. No database. Inherited from LifeOS's "treats the filesystem as its index instead of a vector store."
| Kind | Location | Format |
|---|---|---|
| Intent | USER/GOALS/GOALS.md |
Markdown, G0/P0/S0 IDs |
| Story design (Design) | docs/design/stories/{issue}.md |
YAML frontmatter + 17 fixed sections |
| Test plan | docs/test-plans/{issue}-plan.md |
Markdown tables, P0/P1/P2 |
| Evidence | docs/evidence/{issue}-{ts}.md |
Markdown + committed raw data |
| Memory | MEMORY/** |
Markdown + JSONL append-only |
| Domain data | USER/{HEALTH,FINANCES,TRADING}/ |
Markdown + YAML with declared schema |
| Work state | GitHub issues | Labels are the source of truth |
Private data never enters the repo. USER/ is a symlink into a private store,
exactly as LifeOS does it. The public repo holds code, skills and templates only.
| Concern | Mechanism |
|---|---|
| Idempotency | Every skill re-runs safely; detect state, address only gaps |
| Verification | Never trust conversation memory; re-read disk and GitHub before acting |
| Egress | classify/ + guards/checkEgress block PRIVATE data to cloud models |
| Secrets | .env only; deny-list on cat/grep/rg against it |
| Rate limits | gh/ backs off; batch operations are resumable |
| Traceability | Every commit prefixed #{issue}:; every artifact has a sentinel comment |
| Context budget | Progressive disclosure — Tier 0 always, Tier 1 on route, Tier 2 on demand |
| Decision | Rationale | Rejected alternative |
|---|---|---|
| GitHub as system of record for all domains | One state machine, one query language, free UI and history | Separate stores per domain |
Skills in .claude/skills/ |
Both hosts read it natively — one copy, zero drift | Duplicate trees per host |
| Pure core + thin adapters | Hook runtimes are fundamentally incompatible | Lowest-common-denominator hooks |
| No config file for project context | forge's lesson: root docs carry the same info and help humans too | iai-config.yaml |
| Domain binding as data, not code branches | A sixth domain is one file, not fourteen edits | if (domain === "trade") |
Trading defaults to research rung |
The irreversible action must be opt-in, explicit and gated | Paper-trading default |
| Health is advisory-only, by construction | No medical authority; avoids the entire liability surface | Diagnostic scoring |
| References hold contracts, skills hold workflows | forge's lesson: 20 copies of a contract drift within a month | Self-contained skills |
| TypeScript on Bun | opencode loads plugins as in-process ES modules exporting async hooks, with no external-process escape hatch; the sub-50 ms guard budget has no out-of-process equivalent | A Python core behind a thin TS shim — splits the hottest and most safety-critical path across two languages, loses tsc drift detection between core and adapters, and forfeits the five inherited healthsync connectors |