ADLC
Integrations

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 structured edit/write/apply_patch to a frozen rail declared by the active ticket; in-session bash is gated too via the @adlc/core shell classifier.
  • Native tools: adlc_gate (the model runs a lifecycle gate, LLM-backed ones keyless through the host model) and adlc_prosecute (the deterministic first-party P5 loop: fan-out → dedupe → verify → loop-until-dry over write-disabled child sessions).
  • Advisory session/compaction hooks: session.created preflight, a session.idle gate-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 @prosecutor meta-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

  1. In-session (enforcing by default). A thrown denial in tool.execute.before aborts 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/core shell classifier; unrecognized structured tools carrying a path fail closed. The only downgrade is the explicit ADLC_ALLOW_ADVISORY_HOOKS=1 escape 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 in ADLC_UNGATED_TOOLS) is allowed when no target is found — those tools carry no path on nearly every call — so its spoof check additionally reads target, targetPath, and target_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, an edits[] entry), an extensionless leaf, a leading-dot name, targetDir, a trailing slash — so {path: 'assets.bundle'} still hits an assets.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.mjs under a src/**/test/*.mjs rail). Where the rail set itself says the target is a directory — assets.bundle under an assets.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.

  2. Commit-time (unbypassable). The real control is scripts/rails-guard-ci.mjs (via docs/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_TICKET or .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, and adlc_gate may 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 — preflight included, 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 rails plus the trust-root rails .adlc/tickets.json and .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

PhaseStatusWired via
P0 TriageYes/adlc-ticket
P1 InterrogateYes/adlc-spec + /adlc-approve-spec (G1) + the adlc skill
P2 DecomposeYes/adlc-decompose
P3 RailYesin-session rails-guard hook (enforcing) + CI diff gate
P4 BuildYesrails-guard hook + shell gating + build-gate context-rot backstop + flail advisory
P5 ProsecuteYes/adlc-verify-build (G4) + the deterministic adlc_prosecute runner (5 lenses + verifier) + @prosecutor gates
P6 IntegratePartialsession.idle gate-manifest audit; the human gate is by design
P7 DistillYes/adlc-distill + /adlc-maintain

Gaps

  1. permission.ask is 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 the tool.execute.before throw; the tolerant handler activates the instant upstream wires the hook.
  2. The native TUI plugin module is deferred. PluginModule types tui?: never at 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.
  3. 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.

On this page