c017f6d made /ccam-open call bootstrap() when the live server belongs to a
retired plugin root, which closed a require cycle that had never been closed
before: bootstrap() lazily requires plugin-open back for buildClient, and when
plugin-open is itself the entry point that require resolves to whatever sits on
its module.exports at that instant. The assignment was the last statement in the
file, below the entry block, so it was still empty and /ccam-open died with
"TypeError: buildClient is not a function" before building anything.
Move the assignment above the entry block. The test asserts that ordering
directly — it fails against the previous layout — alongside a check that the
module a re-entrant bootstrap sees really does carry buildClient.
claude plugin install materializes the plugin's inline hooks into
~/.claude/settings.json itself, with \${CLAUDE_PLUGIN_ROOT} resolved to the
actual cache path — confirmed by installing the plugin for real and
inspecting the file. Those entries also contain "hook-handler.js", so
isOurEntry()'s plain substring match could not tell a legitimate
plugin-installed hook from a leftover npm run install-hooks entry: every
SessionStart would have stripped the plugin's own working hooks right back
out. isCheckoutHookEntry() only removes entries whose command does NOT
resolve under ~/.claude/plugins/cache/. plugin-doctor.js's duplicate-hook
count uses the same predicate.
resolveAllDashboardPorts() falls back to [DEFAULT_PORT] when the discovery
file has no live entry — a reasonable guess for the CLI/hook handler, but
wrong for the bootstrap's own liveness check: with no server running at all,
the bootstrap believed one was already up and never called startDashboard(),
confirmed against a real plugin install where the dashboard never started.
plugin-doctor.js's "Server" row had the same bug. Both now read the discovery
file directly and check PID liveness via the new liveServers() (livePids()
reused it instead of duplicating the read).
Adds a root `ccam` plugin (`.claude-plugin/plugin.json`, `"source": "./"`) so
`/plugin marketplace add` + `/plugin install ccam@...` is enough on a machine
with nothing but Claude Code: no clone, no npm run setup, no manual npm start.
- scripts/plugin-bootstrap.js: SessionStart hook. Fast-path exit, Node >=22.5
gate (node:sqlite), mkdir lock with stale reclaim, deps installed into
~/.claude/agent-dashboard/runtime/ (never the plugin cache), legacy
checkout-hook cleanup (backed up), ~/.local/bin/ccam launcher, eager UI
build so client routes like /run work immediately, detached server spawn.
- scripts/plugin-open.js, scripts/plugin-doctor.js: /ccam-open, /ccam-doctor.
- server/index.js: DASHBOARD_CLIENT_DIST override (plugin cache is read-only).
- mcp/build/ is committed (plugin MCP servers start before any bootstrap could
build them) and kept honest by scripts/check-mcp-build.js (content hash,
not mtime), enforced by pre-commit when mcp/src changes.
- plugins/ccam-dashboard/.mcp.json moved under plugins/ccam/ with a working
${CLAUDE_PLUGIN_ROOT} path (the old relative path never resolved from a
marketplace-cached subdir).
- Docs: README, INSTALL, SETUP, ARCHITECTURE, CLAUDE.md, docs/PLUGINS.md,
docs/MCP.md, docs/CLI.md, docs/HOOKS.md.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>