Under OpenCode the skill-health dashboard had no writer and no honest
fallback, so it reported every discovered skill as healthy (#2463).
Root cause: `post:skill:track` is only wired for Claude Code, where
`posttooluse-dispatcher.js` fans the event out to `skill-run-tracker.js`.
The OpenCode plugin re-implements the harness events instead of reusing that
dispatcher, so nothing ever wrote `skill-runs.jsonl` — and the tracker's
matcher only accepted the exact Claude Code tool name `Skill`, while OpenCode
calls the tool `skill`, so even wiring it would have dropped every event.
Commands that locate ECC through the inlined resolver in
`scripts/lib/resolve-ecc-root.js` also fell back to `~/.claude`, which does
not contain ECC, so they could not find their scripts.
Changes:
- `.opencode/plugins/ecc-hooks.ts`: dispatch `post:skill:track` from
`tool.execute.after`. The tracker module is imported lazily and behind
try/catch, matching the existing `changed-files-store` pattern: this file is
OpenCode's startup entry point, so a static import failure would crash the
session before any hook runs (#2530). The module is reused rather than
reimplemented, keeping its identifier validation, length bounds and
"never persist prompt text" guarantees.
- `scripts/hooks/skill-run-tracker.js`: match the tool name case-insensitively
so `Skill` and `skill` are both accepted.
- `scripts/lib/skill-evolution/health.js`: split the summary into
measured/healthy/declining/unmeasured instead of deriving `healthy = total -
declining`, which turned "no telemetry" into "everything is fine" for every
skill.
- `scripts/lib/skill-evolution/dashboard.js`: show the breakdown and warn
explicitly when skills have no recorded runs, since absence of telemetry is
not a health signal.
Under OpenCode the resolver also needs `CLAUDE_PLUGIN_ROOT`, which only the
plugin can supply: `shell.env` exports it when a root is found, without
replacing a root the user set or one an earlier plugin already put in
`output.env`.
Root discovery is per capability and probes required runtime files — never
optional content. Probing for `skills/continuous-learning-v2` (the
`workflow-quality` module) rejected supported modular installs that ship the
plugin and tracker without that optional module, disabling telemetry and the
root export for exactly those users.
Also fixes an incidental bug: the compiled plugin bundle is ESM, where
`__dirname` is undefined, so the ECC version lookup silently fell back to its
default. The module directory is now derived from `import.meta.url`.
Tests: `tests/opencode-plugin-hooks.test.js` covers the telemetry dispatch
(skill tool recorded, other tools ignored, minimal profile respected), a
modular install without the optional skills module, the root export and both
override paths; `tests/hooks/skill-run-tracker.test.js` covers the
case-insensitive matcher; `tests/lib/skill-evolution.test.js` and
`tests/lib/skill-dashboard.test.js` cover the unmeasured bucket, the warning
and the aged-out-coverage wording.
Refs #2463, #2530
* fix(skill-evolution): wire Skill PostToolUse tracker so skill-health shows real runs (#2463)
recordSkillExecution() had no production callers, so
~/.claude/state/skill-runs.jsonl was never written and
`scripts/skills-health.js --dashboard` always reported 0 runs.
Adds scripts/hooks/skill-run-tracker.js and registers it as an async
PostToolUse hook (matcher: Skill) in posttooluse-dispatcher.js, which is now
the single PostToolUse entrypoint on main.
Addresses the privacy and bounds review on #2555's sibling PR:
- No prompt text is persisted. task_description is synthesized as
"Skill invocation: <skill_id>"; tool_input.task_description/description/
prompt are never read.
- Every persisted string is bounded and charset-restricted. A skill id is an
identifier, so free text, newlines, or an over-long value are dropped rather
than truncated and written through.
- The JSONL sink is created 0600 and re-tightened on each append, repairing
files written before this bound existed.
- The sink is capped at MAX_RUN_RECORDS (5000), trimmed oldest-first, so the
append-only file can no longer grow without limit.
Tests cover the privacy guarantee (no prompt text reaches a record), the
identifier bounds, the file mode on POSIX, and the retention cap.
* fix(skill-evolution): re-register the tracker for PostToolUseFailure
The rebase onto current main dropped the hooks.json entry, which silently
resurrected the P1 from the earlier review round: deriveOutcome() still
branches on hook_event_name === 'PostToolUseFailure', but the PostToolUse
dispatcher does not fan that event out, so the branch was unreachable in
production. Hard Skill failures were dropped from telemetry entirely, which
inflates the dashboard success rate — the opposite of what #2463 asks for.
Restores the dedicated PostToolUseFailure entry (matcher Skill, id
post:skill:track, same run-with-flags wrapper and standard,strict gating as
the dispatcher registration). Verified end-to-end: a PostToolUseFailure
payload piped through run-with-flags now records outcome "failure".
Adds a regression test asserting the registration so a future rebase cannot
quietly drop it again.
---------
Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>