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>
On native Windows, Claude Code runs each hook as `bash.exe -c "<command>"`
through the Windows command line. That extra unescape layer collapses the
backslashes in the WSL path conversion of the inline plugin-root resolver,
so `.replace(/\\/g,'/')` reaches node as `.replace(/\/g,'/')` and every hook
fails with `SyntaxError: missing ) after argument list`.
Build the backslash separator with String.fromCharCode(92) and split/join
instead of regex escapes, so the resolver contains no backslash. Conversion
behaviour is unchanged. Regenerate every embedded copy (hooks.json,
codex-hooks.json, commands/*.md) and refresh hooks.metadata.json
fingerprints.
Add a test that the resolver and each hook `node -e` program are
backslash-free, and run the shipped SessionStart command through bash with
backslashes collapsed the way Windows does.
Fixes#3746
Integrates #3697 on current main. Prefer the observer project ID, retain a sole legacy ID until the new observer folder exists, and release the ending session lease across same-root aliases. Replace the unsafe live merge hint with read-only project inspection, and validate alias IDs before path use. #3711 independently identified the computed-ID selection gap; its early switch without observer data and unchanged SessionEnd path were not adopted.
Port #3656 onto current main, preserving both the contributor fix and the unrelated current changelog entry. Includes the follow-up glob normalization from 2c11fd74.
Prettier's config searcher also reads .prettierrc.json5, .prettierrc.ts/.mts/.cts and prettier.config.ts/.mts/.cts. resolve-formatter knew none of them, so a project configured only through prettier.config.ts was never formatted by post-edit-format, quality-gate or the Stop batch. config-protection's rc pattern stopped at .yaml/.toml, so an existing .prettierrc.ts or .prettierrc.json5 could be edited freely.
With diff.relative=true, `git diff --name-only` reports paths relative to the working directory, so joining them to the repository root pointed at files that do not exist, and a session in a subdirectory again got no console.log warning. getGitModifiedFiles now runs `git -c diff.relative=false diff --name-only HEAD`. The -c form also works on Git versions older than --no-relative.