opencode
Native ADLC integration for the opencode terminal agent: an enforce-by-default rails-guard hook, nine lifecycle commands, native adlc_gate/adlc_prosecute tools, and a deterministic P5 prosecution loop (5 lenses + verifier) plus a prosecutor meta-agent.
ADLC in opencode
Brings the lifecycle into the opencode terminal agent as
a native plugin: an in-session rails-guard hook (tool.execute.before),
advisory session hooks, nine lifecycle commands, the adlc phase-routing
skill, two native model-callable tools (adlc_gate, adlc_prosecute), a
deterministic P5 prosecution loop (five lenses + a verifier fanned out over
write-disabled child sessions), and a separate @prosecutor meta-agent (seven
prosecution agents in all). The integration enforces by default: a thrown
denial in tool.execute.before aborts the tool call (documented host behavior,
regression-tested against a real binary).
What you get
- Rails-guard hook on
tool.execute.before. Denies structurededit/write/apply_patchto a frozen rail declared by the active ticket; in-session bash is gated too via the@adlc/coreshell classifier. - Native tools:
adlc_gate(the model runs a lifecycle gate, LLM-backed ones keyless through the host model) andadlc_prosecute(the deterministic first-party P5 loop: fan-out → dedupe → verify → loop-until-dry over write-disabled child sessions). - Advisory session/compaction hooks:
session.createdpreflight, asession.idlegate-manifest audit, per-turn + at-compaction context injection, and slash-command lifecycle/tamper advisories. They warn, never throw. - Commands:
/adlc-init,/adlc-ticket,/adlc-spec,/adlc-approve-spec,/adlc-decompose,/adlc-verify-build,/adlc-prosecute,/adlc-distill,/adlc-maintain. - Subagents: the five P5 prosecution lenses
(
@prosecutor-correctness|security|contract|diff|tests),@prosecutor-verifier, and the@prosecutormeta-agent (deterministic hollow-test / behavior-diff / review-calibration gates).
Install
Two commands (peer dependency: @opencode-ai/plugin ≥ 1.17.13):
npm install -g @adlc/cli # the gate toolkit the plugin shells out to
npx adlc-opencode init # scaffolds .adlc/ + .opencode/, registers the plugin (or npx @adlc/opencode init)Restart opencode so the hooks load. Inside the TUI, /adlc-init re-runs the same
idempotent scaffold. The bootstrap registers a resolvable plugin entry (the
npm name when installed from node_modules, the resolved local path from a source
checkout). Verify locally without an opencode binary:
node scripts/opencode-install-smoke.mjs .Per-repo config rides the ["@adlc/opencode", { … }] plugin-options
tuple (env vars override); see the integration guide for the option table.
Rail enforcement: two layers
-
In-session (enforcing by default). A thrown denial in
tool.execute.beforeaborts the tool call: documented host behavior on@opencode-ai/plugin≥ 1.17.13, regression-tested end-to-end against a real opencode binary (required CI). Bash is gated via the@adlc/coreshell classifier; unrecognized structured tools carrying a path fail closed. The only downgrade is the explicitADLC_ALLOW_ADVISORY_HOOKS=1escape hatch (surface loudly, don't block).Which argument shapes the check can see differs by tool class, because the two classes treat "no path found" oppositely. An unrecognized or mutating tool is denied when no target can be extracted, so breadth costs it nothing: it reads
filePath,path,file,files[],edits[], and apply_patch envelope bodies. An ungated tool (task,skill,todowrite,question, plus anything inADLC_UNGATED_TOOLS) is allowed when no target is found — those tools carry no path on nearly every call — so its spoof check additionally readstarget,targetPath, andtarget_path. Anything neither shape can see (a nested CLI argv, a derived or defaulted write path) is covered by the commit-time gate below, not in-session.Whether a target names a file or a directory decides whether the check also asks "would acting on this destroy a rail underneath it". An existing path is resolved by
stat. A path that does not exist yet is a file only if the key and the name both say so:filePath,file,files[],targetFile, and apply_patch envelopes license the extension heuristic, which then wants a dotted leaf. Everything else resolves to directory — an ambiguous key (target,path,targetPath, anedits[]entry), an extensionless leaf, a leading-dot name,targetDir, a trailing slash — so{path: 'assets.bundle'}still hits anassets.bundle/**rail. One ambiguous spelling wins over a file-specific one for the same path, so an added alias cannot relax the check. The cost is over-denial on an absent file named ambiguously or without an extension; that is the safe direction here, and these tools do not write files in the first place.A claimed file is not taken at face value, because the argument spelling is what a spoofing caller controls: it sheds only the ancestor form an anchored
**creates (src/index.mjsunder asrc/**/test/*.mjsrail). Where the rail set itself says the target is a directory —assets.bundleunder anassets.bundle/**rail — the rails win over the key, so{filePath: 'assets.bundle'}is still denied. Every target is matched in both its raw and its trimmed spelling, so padding a path is not a bypass either. -
Commit-time (unbypassable). The real control is
scripts/rails-guard-ci.mjs(viadocs/ci/rails-guard.yml): it reads the frozen rail set from the trusted base ref and rejects any PR that edits a rail, including the shell-driven writes the in-session hook cannot see. Make it a required check.
Rail contract
Mirrors the sibling integrations; all glob/ticket logic delegates to @adlc/core:
- Active ticket via
ADLC_TICKETor.adlc/current-ticket.json; a conflict fails closed. With the rail set unresolved — a conflicting pointer, a store that will not load, a missing ticket — nothing can be vetted against it: an ungated tool carrying any target is denied, andadlc_gatemay run only a bare non-writing gate (spec-lint,parallax,coldstart,merge-forecast,model-router,flail-detector), enough to diagnose the broken store. Gates that write wait until it is repaired —preflightincluded, since its scratch probes are checkable only against a rail set that exists. - Enforcement is phase-scoped to
ADLC_P4_ENFORCEMENT=1; otherwise no-op. - Rails in force = the single active ticket's
railsplus the trust-root rails.adlc/tickets.jsonand.adlc/current-ticket.json. - Symlink-aware: both the lexical path and the resolved real path are matched.
Context-rot handoff
OpenCode is an enforcing tier for the context-rot handoff. The deny-set from
@adlc/context-handoff is evaluated in tool.execute.before ahead of the
rail check: a session that must hand off should be told to stop, not told which
rail it happened to hit.
Once a session passes the handoff band a deny record is written for it under
.adlc/handoffs/denies/. While any such record is open and unauthorized, the
session loses structured edits and bash wholesale — including attempts to
run adlc handoff write|resume|bypass|repair|unlock from the agent's own shell.
Structured writes to the deny store, resume-auth caches, model-ok stamps,
and lock files are refused whether or not a deny is active. The shell is gated
A shell command naming one of those paths — in any literal spelling: relative,
dot-relative, absolute, symlinked, or a parent directory that holds them — is
refused too, deny-set or not —
there is no CI backstop for them, because .adlc/* is gitignored and a
deletion never reaches a diff. That check reads literal paths only: it cannot
see through a variable, an expansion, or an interpreter one-liner, so it raises
the cost of the obvious attempt rather than closing the class. Read-only tools
keep working.
The band signal is the in-process depth tracker the build-gate backstop already
maintains, keyed on the host's sessionID. A tool call arriving with no usable
session id fails closed once the band fires or a deny store already exists; a
clean repository is never affected.
ADLC_ALLOW_ADVISORY_HOOKS=1 (and advisoryHooks: true) downgrade the rail
guard only. They do not clear the deny-set: rails have an authoritative CI
backstop, and the deny-set has none — its only exits are a signed
adlc handoff resume consumed from a fresh session, or a privileged host repair.
(adlc handoff bypass is deliberately not on that list: its grant is scoped to the
calling invocation and is "not a stored credential", so an automatic PreToolUse
hook has nothing to discover and cannot honour it.)
Clearing a deny needs ADLC_MANIFEST_KEY in the harness's environment:
adlc handoff resume signs a resume-auth cache, and the adapter can only honour
it by verifying that signature. Without the key the gate still fails closed — it
just cannot be re-opened in-session, and says so
(resume_auth_unverifiable:no_manifest_key).
Formal ADLC coverage
| Phase | Status | Wired via |
|---|---|---|
| P0 Triage | Yes | /adlc-ticket |
| P1 Interrogate | Yes | /adlc-spec + /adlc-approve-spec (G1) + the adlc skill |
| P2 Decompose | Yes | /adlc-decompose |
| P3 Rail | Yes | in-session rails-guard hook (enforcing) + CI diff gate |
| P4 Build | Yes | rails-guard hook + shell gating + build-gate context-rot backstop + flail advisory |
| P5 Prosecute | Yes | /adlc-verify-build (G4) + the deterministic adlc_prosecute runner (5 lenses + verifier) + @prosecutor gates |
| P6 Integrate | Partial | session.idle gate-manifest audit; the human gate is by design |
| P7 Distill | Yes | /adlc-distill + /adlc-maintain |
Gaps
permission.askis a dormant lever. It is defined but never dispatched upstream (anomalyco/opencode#7006, re-verified at 1.17.17/1.17.18). The enforcing control is thetool.execute.beforethrow; the tolerant handler activates the instant upstream wires the hook.- The native TUI plugin module is deferred.
PluginModuletypestui?: neverat 1.17.17 (the surface is reserved, not shipped), so the persistent statusline slot / dialogs are a follow-on. The verifiable native touch (the statusline toast) ships now. - P6 acceptance is a human decision by design. The plugin surfaces evidence, it does not automate approval.
Go deeper
Source: plugins/adlc-opencode/
· design rationale: ADR 0004.
The deterministic prosecution loop this integration ships: Prosecution, Not Code Review.
Cursor
Native ADLC integration for Cursor: marketplace plugin with sessionStart context, preToolUse rails, packaged prosecutor agents + MCP wrapper (channel unverified), /adlc-* commands, and CI as the backstop.
pi
Native ADLC extension for the pi coding agent: in-session rail, scope, and suppression enforcement with git-diff revert, plus five phase skills.