Collect string-valued manifest.hooks as a file reference so missing files, directories, and paths outside the cache fail validation. Add regression coverage for valid and invalid hook references while preserving non-string and absent-hook behavior.
Closes#3786
The compiled OpenCode tools import `tool` from `@opencode-ai/plugin/tool`
at module top level, and the hooks plugin imports those tools statically.
The package was only a devDependency, so production installs (OpenCode's
npm plugin cache, `npm install --omit=dev`) could not resolve it and the
whole plugin failed to load.
Move it to `dependencies` and add a build-opencode regression test that
fails when the built bundle imports a package not declared as a runtime
dependency.
Fixes#3543
(cherry picked from commit 106024070e)
openBrowser() captures the launcher's async error internally, so a missing
xdg-open/open left headless users with no hint why no browser appeared.
Wrap the injected spawn in the entrypoint to report the failure on stderr
with the dashboard URL; the shared platform-launch helper is unchanged.
The auto-open in the dashboard-web.js entrypoint spawned xdg-open/open
without an 'error' listener. When the launcher is missing (headless
Linux, containers, many WSL setups) spawn reports ENOENT asynchronously,
the try/catch cannot see it, and the unhandled 'error' event kills the
process right after the banner is printed.
Use the shared openBrowser() helper from scripts/lib/platform-launch.js,
which already handles the child's error event and unrefs the launcher,
as plan-canvas.js and control-pane.js do.
Fixes#3600
AGENTS.md asks for new objects rather than mutation. Leave NODE_PATH out
with a rest destructure, as pre-bash-tmux-reminder.test.js does, instead
of deleting it from the env after building it.
A NODE_PATH that exposes js-yaml let the regression pass the test, and an
inherited ECC_SESSION_START_MAX_CHARS=0 made it fail falsely. Drop
NODE_PATH from the child environment and pin the cap to 8000, as
session-start-alias-unavailable.test.js does.
The merge of #3470 moved the Flutter check in scripts/lib/project-detect.js
onto js-yaml with a top-level require. session-start.js loads that module
on every SessionStart, and plugin installs have no node_modules (#2822),
so the hook now exits 1 with "Cannot find module 'js-yaml'" in every
project, not only Dart ones.
Require js-yaml inside hasFlutterSdkDependency instead. When it is
missing, the existing catch returns false: Dart is still detected from
pubspec.yaml and only the Flutter refinement is skipped. Installs that
have their dependencies behave as before.
Add a case to the no-node_modules plugin install test that runs
session-start.js against a pubspec.yaml project. It fails on main.
normalizeIssueNumber parsed with Number.parseInt, which stops at the
first non-digit ("42oops" -> 42, "42.9" -> 42, "4e2" -> 4) and loses
precision past 2^53 (9007199254740993 became 9007199254740992). The CLI
shares this helper for --issue, --limit, and the positional issue number,
so malformed values silently targeted the wrong issue before any integer
guard downstream could notice.
Validate the entire trimmed decimal token and require
Number.isSafeInteger so malformed or out-of-range values throw
"Invalid issue number" during argument parsing, before the state store
is opened or GitHub is contacted. Whitespace, leading plus, and leading
zero tolerance is preserved. Regression coverage added at both the
library level (tests/lib/github-coordination-gh-api.test.js) and the CLI
argument parser (tests/scripts/github-coordination.test.js).
Fixes#3514
Issue #3492: scope migration aborted with generic verification errors in
two real states. When the destination scope is installed but inventory
reports enabled:false because the user explicitly set
enabledPlugins["ecc@ecc"]: false, setup now throws a targeted
DESTINATION_EXPLICITLY_DISABLED error naming the setting and file and
offering the native claude plugin enable recovery, leaving the user's
choice untouched. The generic not-enabled error also gains the enable
command in its recovery list.
uninstallSource now accepts a destination-only state when the source
scope is gone from the fresh inventory even if the uninstall command
errored, and SOURCE_UNINSTALL_FAILED recovery no longer suggests
uninstalling a source scope the inventory no longer lists; it prepends
claude plugin list --json so the stuck state can be inspected.
Tests: node tests/lib/claude-scope-migration.test.js — five new cases
cover explicitly disabled resume and single-scope destinations across
user/project/local settings files, the generic fallback keeping the
enable path, an uninstall error with the source already absent, and
non-circular recovery. 16 -> 21 passing; red against unpatched scripts
is 15 passed / 6 failed. Full-suite failure set is byte-identical to
unpatched HEAD in a zero-dependency environment.
Also sync the pinned SOURCE_UNINSTALL_FAILED recovery in
tests/scripts/setup.test.js ('migration JSON failures retain phase,
scopes, and exact recovery') with the new claude plugin list --json
diagnostic: 29 passed / 1 failed on the patched tree before the sync,
30 passed / 0 failed after, identical to clean 2d515e4f.
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.
Actual installed skills-health --help fails with require undefined in ESM Claude project and home fixtures. RED evidence: command-runtime-evidence/red-final.log; bounded repair remains pending.
Scoped adaptation from PR #3088. Keep the canonical atomic writer and dispatcher; exclude the broader provider/tool changes.
Co-authored-by: Geronimo <varungore1@gmail.com>