The inert stub written when hook consent is not granted was
`export default async () => ({})`. OpenCode v2 validates every plugin
module before running it and requires the default export to be a definition
object with an `id` and an `effect` or `setup` function, so a declined or
runtime-less home install printed the same startup error as the V1 hook
plugin:
PluginModule.LoadError: Plugin must export a default definition with an id
and an effect or setup function.
(cause: SchemaError(Expected object at ["default"]))
Write `export default { id: "ecc-hooks", setup() {} }` instead. It keeps the
inert behaviour (no imports, no module initialization of the replaced
plugin) and satisfies the loader.
Tests that pinned the generated bytes now compare against
getDisabledOpenCodePluginContent() rather than a duplicated literal, and
tests/opencode-plugin-loader-contract.test.js asserts the loader contract on
the generated payload. That test fails on the previous stub
("default export must be an object, got function").
Refs #3537. The same assertion should cover .opencode/plugins/*.ts once a V2
entrypoint lands (#3536, #3224); those plugins are scanned independently in
home installs, so both the barrel and the implementation are loaded.
Address CodeRabbit re-review on #3771: rewriteRuleLinks now tracks fenced
code blocks (``` and ~~~) line by line and only rewrites links outside
them, so Markdown examples shown inside fences stay verbatim. One shipped
rule contains such an example, so this changes installed output today.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address CodeRabbit review on #3771:
- Treat immediately empty (`---\n---`) and whitespace-only frontmatter
blocks as empty metadata instead of throwing or leaking the delimiters
into the installed body.
- Reject an empty `paths: []` list before serialization so an installed
instruction never receives an empty `applyTo:` glob.
- Rewrite relative Markdown links that resolve inside the installed rule
set to their renamed `.instructions.md` targets; links escaping rules/,
external URLs, absolute paths, anchors, and README.md stay unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The copilot project target installed only skills and agents, dropping the
entire rules/ tree even though Copilot CLI and VS Code read path-specific
instructions from .github/instructions/**/*.instructions.md.
Plan each rules/**/*.md file into .github/instructions/ecc/ as an
.instructions.md file and add a copilot-rule-frontmatter transform that maps
the Claude-style `paths:` list onto Copilot's `applyTo:` glob list. Rules
without `paths:` become applyTo: "**", matching their always-on semantics in
Claude Code. rules/README.md documents the rule set and is not installed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reuse the JSON object checks before filtering MCP merge payloads so ordinary
JSON merges retain their existing behavior when ECC_DISABLED_MCPS is set.
Add 10 regression cases covering payload shapes, preview and install state,
file contents, and plan immutability. Nine cases reproduce the original
failure. All 224 cases across 10 installer/lifecycle suites pass.
Validation: changed-file ESLint (no errors), git diff --check, and apply.js
coverage of 91.05% lines, 83.70% branches, and 100% functions.
Address review findings on #3590:
- Unterminated block comments now throw instead of silently passing:
{'plugin':[]} /* no longer parses, so the install keeps its
source-context error for genuinely malformed configs (CodeRabbit,
Greptile).
- Line-comment scanners stop at CR as well as LF, so CR-only line
endings do not consume the rest of the file (CodeRabbit).
- activationIsInactive restores the 'plugin must be an array' shape
check that the strict parser surfaced, so a non-array plugin no
longer counts as inactive (Greptile).
Regression tests: unterminated comment with source context, CR-only
endings, and non-array plugin across apply/repair/doctor modes.
The read-only plugin-membership check in activationIsInactive and the
validation inside disableOpenCodeHookPluginRegistration used strict
JSON.parse, so any user whose ~/.config/opencode/opencode.json contains
JSONC comments (which OpenCode itself accepts) could not install the
OpenCode catalog. The plan never writes the user-owned config; this
parse is purely a membership read (issue #3546).
Add a string-aware JSONC parser (line/block comments, the trailing
commas they hide, URL strings untouched) and route both call sites
through it. Truly malformed JSON keeps its source-context error.
Tests: unit coverage for comment/trailing-comma/URL handling, plus an
apply-level regression test that installs over an inactive JSONC user
config byte-for-byte, across default and declined consent modes.
When strict YAML cannot load a rule (for example unquoted globs), the
line-based fallback treated trigger: '' as native and left the rule
without an activation trigger. Read the trigger value by hand, drop a
trailing comment and surrounding quotes, and only treat a nonblank value
as native.
Refs #3548
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Treat a rule as native only when its parsed `trigger` is a non-empty
string, so empty, comment-only, and blank-string triggers are adapted.
The line check is kept only as the fallback for native rules whose
unquoted globs cannot be parsed as YAML.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty `trigger:` line is not a usable activation trigger, so adapt
the rule instead of passing it through unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rule that already declares a trigger may carry unquoted
`globs: **/*.ts`, which js-yaml rejects as an alias. Detect an existing
trigger before parsing so native rules, and the adapter's own output,
pass through unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain YAML scalar cannot start with `*`, so `globs: **/*.py` is not
valid YAML for a strict parser. Emit the joined globs as a quoted string
and have the adapter tests parse the emitted frontmatter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Antigravity only attaches a workspace rule when its frontmatter declares
a trigger. The antigravity target copied rules verbatim: common rules
have no frontmatter and language rules carry Claude's `paths:` list, so
every ECC rule landed in .agents/rules/ and was never loaded.
Add an antigravity-rule-frontmatter content transform and apply it to
rule pack files on both the manifest and legacy install paths. Rules
without frontmatter become `trigger: always_on`; `paths:` becomes
`trigger: glob` with matching `globs:`. A rule that already declares a
trigger is left alone, and rules/README.md stays inert. Canonical rule
sources are unchanged.
Fixes#3548
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse the existing scripts boundary asset and guard for commands-core without hooks. Preflight selected helper scopes and preserve user module metadata, unowned/edited JS interpretation, and boundary ownership through uninstall. Actual installed ESM help/JSON, receipt, reinstall/uninstall and 34 focused cases pass. OpenCode boundary 8/8, adapters 51/51, manifests 43/43; focused coverage exceeds 80%. Two root-owned legacy ESM core-profile fixtures require correction; no unsupported states weakened.
Reproduced issue #3485 on installed Codex CLI 0.162.0. Keep user legacy choices for explicit migration and verify disposable install, repair, reinstall and removal.
Preserve main OpenCode lease and activation gates and native Claude namespace receipts. Detect same-size in-place edits using bigint modification and change times during hashing and before deletion; verify both races and actual public CLI reinstall lifecycles.