Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ spec-gate hook) and end-to-end CI:
| antigravity *(community)* | **C** | — | — | — | — | ✅ | [2026-07-12](develop/conformance/reports/antigravity-2026-07-12.md) |
| codex *(community)* | **C** | — | — | — | — | ✅ | [2026-07-12](develop/conformance/reports/codex-2026-07-12.md) |
| gemini-cli *(community)* | **C** | — | — | — | — | ✅ | [2026-07-12](develop/conformance/reports/gemini-cli-2026-07-12.md) |
| opencode *(community)* | **C** | | | | — | ✅ | [2026-07-12](develop/conformance/reports/opencode-2026-07-12.md) |
| opencode *(community)* | **A** | 📝 | 📝 | 📝 | — | ✅ | [2026-07-12](develop/conformance/reports/opencode-2026-07-12.md) |

✅ checked · 📝 attested (evidence + expiry) · — not available

Expand Down
60 changes: 60 additions & 0 deletions docs/attestations/opencode-tier-a-2026-07-17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# opencode Tier-A capabilities — attested 2026-07-17

Follow-up to the 2026-07-12 investigation, which left opencode at Tier C with
`pre-tool-veto` proven "up to the last link". All three Tier-A capabilities are
now attested with an instrumented transcript (raw log:
`.sage/docs/development/opencode-tierA-attested-2026-07-17.log`). Every probe
logs each firing, so nothing-happened and it-happened are distinguishable — the
rule the July investigation was built on.

Probed with opencode 1.15.3, plugin API `@opencode-ai/plugin`. The capability
attestations are model-independent (they concern whether the platform's plugin
API fires hooks, not model quality); the probe session ran on the platform's
configured model.

## 1. pre-tool-veto — attested
`.opencode/plugin/sage.js` bridges `tool.execute.before` → `sage-spec-gate.sh`
(paths parsed out of `apply_patch`'s `patchText`, the gap the July probe found).
In a pre-spec cycle, asked to edit source:

tool.execute.before fires → sage-spec-gate.sh exit 2 → throw → EDIT BLOCKED

The edit-being-blocked transcript C15 requires. (Prior log:
`opencode-veto-attested-2026-07-17.log`.)

## 2. post-tool-events — attested
`tool.execute.after` fires after every tool with the result payload:

before tool=read
AFTER tool=read callID=call_00_… outputLen=148

The payload (tool, callID, args, output) is the PostToolUse shape Sage's
recorders need — manifest-sync, degradation-log, verify-tracker.

## 3. subagent-dispatch — attested
A `reviewer` subagent (`.opencode/agent/reviewer.md`, `mode: subagent`) was
dispatched via the built-in `task` tool:

before tool=task
session.next.agent.switched → agent="reviewer" (a NEW session id)
AFTER tool=task outputLen=3770

The reviewer ran in its own session and returned an independent review — Sage's
ADR-10 quality chain (independent reviewer) has a mechanism here.

## The load-bearing bonus: hooks fire INSIDE the subagent
Between task-dispatch and task-return, the reviewer's OWN read fired the hooks:

before tool=read / AFTER tool=read outputLen=148 (child session)

So a dispatched subagent is NOT an enforcement escape hatch — the veto and the
config-gate apply inside it. This is the exact property the claude-code
"hooks-in-subagents" attestation turns on, and it holds on opencode too.

## Derivation
ADR-11: Tier A = pre-tool-veto ∧ post-tool-events ∧ subagent-dispatch. All three
attested. opencode's platform capabilities support Tier A. (Delivery is gated on
Sage shipping the adapter that uses them — the generator work, tracked
separately.)

verified: 2026-07-17 · expires-release: "1.5" · re-probe on opencode major bumps
32 changes: 19 additions & 13 deletions runtime/platforms/community/opencode/STATUS.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
# opencode — community-maintained (experimental)
# opencode — community-maintained, Tier A (2026-07-17)

**Tier:** community. Not a first-class Sage platform.
**Tier:** A — mechanically enforced. `sage init` ships an enforcement adapter
(`.opencode/plugin/sage.js`) that vetoes edits, records the audit trail, and
dispatches independent subagent reviews, all through opencode's plugin API.

- **Generation-tested only.** CI checks that this platform's generator produces
files and that its shell parses. It is not exercised end-to-end.
- **Quality chain unavailable.** The sub-agent reviews (auto-review, auto-QA,
independent Gate 3) and the PreToolUse spec-gate hook are Claude Code only.
On this platform, Rule 3 and Rule 5 are enforced by prose alone, and any
skipped review degrades loudly (announced + logged). See the per-platform
enforcement table in the root README.
- **First-class platforms** — full quality chain and end-to-end CI — are
`claude-code` and `generic`.
- **Enforced, not prose.** The adapter bridges `tool.execute.before` to Sage's
gate scripts and `throw`s on exit 2 — opencode blocks the call. The spec-gate,
tdd-gate, secrets-gate, config-gate and verify-gate all apply, including
**inside dispatched subagents** (proven: the reviewer's own tool calls fire the
hooks). Rules 3 and 5 are mechanical here, not advisory.
- **Proven.** Capabilities attested with an instrumented transcript
(`docs/attestations/opencode-tier-a-2026-07-17.md`); the shipped adapter is
tested deterministically against the real gates
(`setup/adapter-test.mjs`, 5/5), independent of opencode's model backend.
- **The honest edge.** No native skill discovery (system skills are inlined into
`AGENTS.md`), and opencode's model backend was flaky during the probe — which
is why the load-bearing proof is the deterministic adapter test, not a single
live session.

Contributions welcome. If you rely on this platform, help keep its generator
current; the core team does not test it per release.
Maintainer: sage-core. Re-probe on opencode major version bumps (attestations
expire at release 1.5).
113 changes: 77 additions & 36 deletions runtime/platforms/community/opencode/platform.yaml
Original file line number Diff line number Diff line change
@@ -1,58 +1,99 @@
---
name: opencode
description: >
OpenCode — community-maintained. Sage generates an instructions file and commands.
It enforces nothing: no hook surface, so every Sage rule here is prose, and the
v1.2.1 eval measured what prose alone is worth.
OpenCode — Tier A as of 2026-07-17. Sage ships an enforcement adapter
(.opencode/plugin/sage.js) that bridges opencode's plugin hooks to Sage's
gate scripts: tool.execute.before vetoes an edit, tool.execute.after records
the audit trail, and the task tool dispatches an independent reviewer — with
hooks firing inside the subagent, so it is not an escape hatch.
contract-version: 2
maintainer: "unclaimed" # R113: nobody answers when this goes red.
maintainer: "sage-core"

capabilities:
context-injection: true # an instructions file is generated
command-delivery: true # commands are generated
native-skill-discovery: false # the generator emits skills; nothing has verified they are ever triggered
pre-tool-veto: false # no hook surface. Nothing blocks an edit.
post-tool-events: false
subagent-dispatch: false # no evidence Sage has ever dispatched one here
session-events: false
context-injection: true # AGENTS.md at project root
command-delivery: true # .opencode/commands/ → the / menu
native-skill-discovery: false # no discovery; system skills are INLINED into AGENTS.md
pre-tool-veto: attested # tool.execute.before → gate exit 2 → throw → edit blocked
post-tool-events: attested # tool.execute.after fires with the tool result
subagent-dispatch: attested # the task tool spawns an independent reviewer session
session-events: false # the platform's event hook fires session.*, but the adapter ships no session-init that uses it

attestations:
- capability: pre-tool-veto
evidence: docs/attestations/opencode-tier-a-2026-07-17.md
verified: 2026-07-17
expires-release: "1.5"
note: >
The July 2026 investigation proved the chain up to the last link and left
it false. The 2026-07-17 probe closed it: paths parsed out of apply_patch's
patchText, sage-spec-gate.sh returned exit 2 in a pre-spec cycle, the
adapter threw, and opencode BLOCKED the edit. The shipped adapter is tested
deterministically (setup/adapter-test.mjs, 5/5) driving the real gates —
pre-spec veto, config self-disable veto, secrets veto — independent of
opencode's model backend. Undocumented platform behaviour that can regress
silently, so it expires and is re-probed.

- capability: post-tool-events
evidence: docs/attestations/opencode-tier-a-2026-07-17.md
verified: 2026-07-17
expires-release: "1.5"
note: >
tool.execute.after fires after every tool with {tool, callID, args,
output} — the PostToolUse payload sage-verify-tracker, sage-degradation-log
and sage-manifest-sync need. Logged firing on a real session.

- capability: subagent-dispatch
evidence: docs/attestations/opencode-tier-a-2026-07-17.md
verified: 2026-07-17
expires-release: "1.5"
note: >
The built-in task tool dispatched a `reviewer` subagent into its OWN
session, which returned an independent review. The load-bearing half: the
reviewer's own read fired tool.execute.before/after in the child session,
so the veto and config-gate apply inside dispatched subagents — a subagent
is not an enforcement escape hatch. This is the property ADR-10's quality
argument rests on.

supported-os: [linux, macos]

command-prefix: "/"
install-method: manual-copy
install-method: generated
artifacts:
instructions: AGENTS.md
commands-dir: .opencode/commands
plugin: .opencode/plugin/sage.js
hooks-dir: .opencode/sage-hooks
---

# Tier C, and honestly so

Every `false` above means nobody has checked. That is what an unchecked
capability is worth under ADR-11 — not a benefit of the doubt.
# Tier A — mechanically enforced, and proven so

To improve this: run `develop/conformance/run-conformance.sh opencode`, make the
checks pass, attach transcripts for what cannot be checked for free, and claim
`maintainer:`. That is the whole contribution surface.
The three capabilities that decide the tier are attested with an instrumented
transcript (`docs/attestations/opencode-tier-a-2026-07-17.md`) and the shipped
adapter is tested deterministically against the real gate scripts
(`setup/adapter-test.mjs`). Every claim above is either checkable by conformance
or backed by evidence with an expiry — nothing is typed on faith.

# Why `pre-tool-veto: false` — the chain is proven except its last link
## How enforcement reaches opencode

Investigated 2026-07-12. Full transcript of the investigation is kept with
the maintainers; the findings that matter are below.
`sage init` (opencode) writes `.opencode/plugin/sage.js` and copies the gate
scripts to `.opencode/sage-hooks/`. The adapter:

**OpenCode's veto is documented and the mechanism demonstrably works:**
- **vetoes** — `tool.execute.before` maps the opencode tool (`edit`/`write`/
`apply_patch` → `Edit`/`Write`, paths parsed from `patchText`; `bash` →
`Bash`) to the gate payload, runs the PreToolUse gates, and `throw`s on exit 2.
opencode honours the throw by blocking the call.
- **records** — `tool.execute.after` runs the PostToolUse recorders
(verify-tracker, degradation-log, manifest-sync) for their side effects.

1. The plugin LOADS (`.opencode/plugin/` and `.opencode/plugins/` are both scanned).
2. `tool.execute.before` FIRES — an instrumented pass captured every tool.
3. Sage's gate RAN, spawned from inside the hook.
4. And it correctly ALLOWED — because opencode's edit tool is **`apply_patch`,
whose only argument is `patchText`. There is no `filePath`.** The adapter handed
the gate an empty path, and a gate with no file to gate allows. That is the
ADAPTER failing, not the veto.
The gates are the same platform-agnostic scripts Claude Code runs; they read
`.sage/`, not `.claude/`, so a single set enforces on both platforms.

**What was never observed: opencode honouring the `throw` by blocking an edit.**
Without that transcript the capability cannot be `attested` (C15), so it is `false`.
## The honest edge

**To finish:** parse the paths out of `patchText` (`*** Update File: <path>`), run
one probe against a `pre-spec` cycle, and check BOTH that the edit did not land AND
that the hook log shows the gate returning 2. Checking only the file is how you
record a crashed run as a successful veto.
`native-skill-discovery` stays `false` — opencode has no description-triggered
discovery, so the system skills are inlined into `AGENTS.md` (Tier A is about
enforcement, not discovery). And opencode's model backend was flaky during the
probe (frequent startup hangs), which is why the load-bearing proof is the
deterministic `adapter-test.mjs` driving the real hooks, not a single live
session. The live probe supplies the one thing the harness cannot: that opencode
honours the `throw`.
56 changes: 56 additions & 0 deletions runtime/platforms/community/opencode/setup/adapter-test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Deterministic test of the production opencode adapter — drives its hooks
// directly against the real gate scripts. No opencode model backend needed.
import { SagePlugin } from "./.opencode/plugin/sage.js"
import { writeFileSync, mkdirSync } from "fs"
import { join } from "path"

const root = process.cwd()
let pass = 0, fail = 0
const ok = (name, cond) => { cond ? pass++ : fail++; console.log(` ${cond?"✓":"✗"} ${name}`) }

function setManifest(state) {
mkdirSync(join(root, ".sage/work/001-demo"), { recursive: true })
writeFileSync(join(root, ".sage/work/001-demo/manifest.md"),
`---\ncycle_id: "001-demo"\ngate_state: ${state}\nstatus: in-progress\n---\n# demo\n`)
}
function setConfig(enforce) {
writeFileSync(join(root, ".sage/config.yaml"),
`sage-version: "1.3.8"\nhard_enforcement: ${enforce}\n`)
}
async function threw(hooks, tool, args) {
try { await hooks["tool.execute.before"]({ tool }, { args }); return false }
catch { return true }
}

const hooks = await SagePlugin({ directory: root })

// 1. pre-spec source edit → VETO
setConfig("true"); setManifest("pre-spec")
ok("pre-spec source edit is VETOED",
await threw(hooks, "edit", { filePath: join(root, "src/calc.py"),
oldString: "def add", newString: "def add\ndef multiply(a,b): return a*b" }))

// 2. spec-approved → allowed
setManifest("spec-approved")
ok("spec-approved source edit is ALLOWED",
!(await threw(hooks, "edit", { filePath: join(root, "src/calc.py"),
newString: "def multiply(a,b): return a*b" })))

// 3. config self-disable → VETO (the meta-gate, through the adapter)
setManifest("spec-approved")
ok("flipping hard_enforcement off is VETOED",
await threw(hooks, "edit", { filePath: join(root, ".sage/config.yaml"),
oldString: "hard_enforcement: true", newString: "hard_enforcement: false" }))

// 4. hardcoded secret → VETO
setManifest("spec-approved")
ok("a hardcoded live key is VETOED",
await threw(hooks, "write", { filePath: join(root, "src/keys.py"),
content: 'API = "payco_live_4eC39HqLyjWDarjtT"' }))

// 5. a plain read → never gated
ok("a read is never gated",
!(await threw(hooks, "read", { filePath: join(root, "src/calc.py") })))

console.log(`\n adapter: ${pass} pass · ${fail} fail`)
process.exit(fail ? 1 : 0)
39 changes: 39 additions & 0 deletions runtime/platforms/community/opencode/setup/generate-opencode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,43 @@ Classification only.
AGENT_EOF
echo " ✓ sage-classifier.md"

# ═══════════════════════════════════════════════════════════════
# Enforcement adapter — the Tier-A port (attested 2026-07-17)
# opencode's plugin API vetoes (tool.execute.before → throw), records
# (tool.execute.after), and both fire inside dispatched subagents. This ships
# the bridge from those hooks to Sage's platform-agnostic gate scripts.
# ═══════════════════════════════════════════════════════════════
echo ""
echo "🛡 Installing enforcement adapter..."
mkdir -p "$OC_DIR/plugin" "$OC_DIR/sage-hooks"

# The gate scripts the adapter calls. They are platform-agnostic (they read
# .sage/, not .claude/) — copied to a stable path so the adapter never depends
# on where the framework is vendored.
GATE_SRC="$SAGE_DIR/runtime/platforms/claude-code/hooks"
GATE_CORE="$CORE/gates/scripts"
for g in sage-spec-gate.sh sage-tdd-gate.sh sage-bookkeeping-gate.sh \
sage-secrets-gate.sh sage-config-gate.sh sage-verify-gate.sh \
sage-verify-tracker.sh sage-degradation-log.sh sage-manifest-sync.sh; do
if [ -f "$GATE_SRC/$g" ]; then
cp "$GATE_SRC/$g" "$OC_DIR/sage-hooks/$g" && chmod +x "$OC_DIR/sage-hooks/$g"
fi
done
# The gate scripts that delegate to core check-scripts need those too.
for c in sage-verify.sh sage-spec-check.sh sage-hallucination-check.sh; do
[ -f "$GATE_CORE/$c" ] && cp "$GATE_CORE/$c" "$OC_DIR/sage-hooks/$c" && \
chmod +x "$OC_DIR/sage-hooks/$c"
done

# The adapter itself (static — resolves paths at runtime from the project root).
if [ -f "$(dirname "$0")/sage-plugin.js" ]; then
cp "$(dirname "$0")/sage-plugin.js" "$OC_DIR/plugin/sage.js"
echo " ✓ .opencode/plugin/sage.js (veto + audit, incl. inside subagents)"
echo " ✓ .opencode/sage-hooks/ (gate scripts)"
else
echo " ⚠ sage-plugin.js not found — enforcement adapter NOT installed"
fi

# ═══════════════════════════════════════════════════════════════
# Project state — .sage/ initialization
# ═══════════════════════════════════════════════════════════════
Expand All @@ -272,6 +309,8 @@ echo "Files written:"
echo " AGENTS.md (read by Opencode as system context)"
echo " .opencode/commands/ (markdown command definitions)"
echo " .opencode/agents/ (markdown sub-agent definitions)"
echo " .opencode/plugin/sage.js (enforcement adapter — the Tier-A port)"
echo " .opencode/sage-hooks/ (gate scripts the adapter runs)"
echo ""
echo "Next steps:"
echo " - Run \`opencode\` to start a session"
Expand Down
Loading
Loading