478418f60270f90a1a4dfc085c01b680ecb3e592
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
478418f602 |
fix(macos): scrub parent-session env, survive node upgrades and reboots, store lanes by real path (#22)
What broke (code audit, untested on a Mac): - Without systemd the server is always spawned from inside a Claude Code session, so it inherited CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_PLUGIN_ROOT, ... and tmux handed them to every lane pane (a codex pane even resolved as `claude` in `agenthub`'s harness check). - The ~/.local/bin/agenthub launcher pinned a versioned node path that `brew upgrade`/nvm deletes, leaving the CLI dead until the next bootstrap. - A discovery entry left over from before a reboot could name a recycled pid, so the bootstrap's fast path believed a dashboard was live and stopDashboard could signal an unrelated process. - Lanes were stored under the typed cwd; macOS reports /private/tmp and the on-disk case, so sessions never bound to a /tmp or differently-cased lane. What changed: - server/index.js deletes a DENYLIST of parent-session variables at startup; user config (CLAUDE_CONFIG_DIR, CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_*) stays. - linkCli's launcher falls back to `node` on PATH when the pinned one is gone. - liveServers ignores entries whose startedAt predates this boot (60 s margin). - createLane and POST /api/lanes/ensure store/look up fs.realpathSync.native of an existing cwd; the liveness reaper also tries the realpath. Existing rows are untouched. - session-liveness gains an exec seam so the darwin ps+lsof branch is tested. - plugin-doctor names ~/.zshrc on darwin; INSTALL.md gains a macOS section with the Mac verification checklist, mirrored in both READMEs and PLUGINS.md. No launchd, no macOptionIsMeta, engines stays >=20. |
||
|
|
8d48a038c7 |
fix(ingest,ui): count each assistant message once, each turn once, and each event once
Three defects that all showed up as "the dashboard's numbers are wrong", plus the notification and brand work asked for alongside them. Tokens: Claude Code writes ONE JSONL line per content block of an assistant message, and every one of those lines repeats the same message-level `usage`. Both parsers summed them, so a tool-heavy session's tokens — and its cost — ran 1.5-2x high (one 4h session: $45.82 stored against $27.72 real). `lastUsageMsgId` counts a message once, carried across the transcript cache's incremental chunk boundaries. `replaceTokenUsage`'s high-water baselines mean the fixed parser cannot correct what is already stored, so `setTokenUsage` (exact, baselines cleared) is added for the deliberate re-parse path and `reconcileTokens` now covers every session with a transcript on disk rather than imported ones only. Turns: every `TurnDuration` row was stamped NOW while its dedup guard matched on the transcript's timestamp, so each hook re-inserted the session's whole turn history — 587k rows, 75% of a real events table, one session holding 68,747 copies of the same turns. The row now carries the transcript timestamp, which fixes the guard and the feed's ordering together. Same class of bug in the session metadata: `thinking_blocks` / `turn_count` / `total_turn_duration_ms` are assigned from the full parse instead of `+=`, since `extract()` returns whole-transcript totals on every call. Events: leftover checkout hook entries in `~/.claude/settings.json` double every event, every subagent row and every token figure. `stripLegacyHooks` has always removed them — but only inside `bootstrap()`, which `fastPathOk` skips on exactly the healthy installs where nothing else forces a full pass, so they survived indefinitely. `hasLegacyHooks` (pure fs) now blocks the fast path. A one-time repair at the first start after upgrading (`user_version` 3) deletes the duplicated `TurnDuration` rows and re-parses every transcript with exact writes. Marker 3 and not 2: an install that ran the earlier pass had its corrected rows re-inflated by whatever old build was still ingesting hooks. Router: switching a lane's AI Router while a pane is live only half-applies — Claude Code re-applies `env` to a running session but keeps the model it resolved at startup, so the old router's model name reaches the new endpoint and the next prompt fails with "model not found". The apply route reports `restart_required` / `run_id`, and the picker offers a Restart that respawns the pane from the lane id alone (replaying the old `--model` would re-create the mismatch). Notifications (#29): the two moments Claude is BLOCKED on the user now have their own switches and default to on — `onAwaitingInput` (a permission prompt or a question) and `onTurnComplete` (end of turn). Before, the first was unswitchable and the second hung off a preference that defaults to off, so the one thing a user walks away from went unannounced. Prefs shape/defaults/storage move to `client/src/lib/notifPrefs.ts`; Settings and the hook had private copies. Brand: the sidebar shows the SmartGift wordmark with the AgentHub caption under it, the collapsed rail shows the mark, and the favicon is vendored to match. |
||
|
|
26b0fe53c7 |
feat(skills,pipelines): ask the user at the two points being wrong is cheap, and stop paying for reviews the diff does not need
The `default` 8-node pipeline walked past its own intake: the node declared `agenthub stage intake`, a stage report with no work attached, so "intake" was a label the run passed through without ever asking a question. It now declares `/superpowers:brainstorming` (and detects it — that rule moves off `plan`), and both driving skills make the brainstorm mandatory, asked with AskUserQuestion and waited on rather than answered on the user's behalf. Three stops replace three silent slides: - After the plan is written and adversarially challenged, it goes to the USER for approval. Implementation does not start without it, and work that turns out to need a different approach comes back for a fresh one. - Review picks its reviewers from `git diff --name-only`: Claude Code's own `code-review` always, a domain reviewer only for a layer the diff actually touched, and — new — only once the change is big enough to have seams. Three agents re-reading twenty lines buys one opinion at three times the tokens, on every fix-loop lap. Review is also a loop now: it ends on a clean pass over the current diff, not on one pass having happened. - The gate asks the user to accept before anything is claimed done (and in the lane pipeline, before the PR is published — publishing is outward-facing). A NO-GO re-enters by SIZE: a defect goes back to implement, a wrong approach back to plan. Nothing carries a NO-GO forward with a note about it. Also here, from a real stall seen on a lane: `ship-feature` had no waiting rule at all, and a session at the gate invented one with `ScheduleWakeup` — a /loop-only primitive that paces nothing here and is refused outright when `auto` mode cannot reach its safety classifier. The rule the lane skill already had now exists in both. Cost, measured in what each agent is actually for: the suite run leaves its agent (nothing is judged, so nothing is delegated — an agent there buys a second model's tokens to read output the session can read for free), and `backend-reviewer` drops from opus to sonnet like its frontend twin; both read a scoped diff against a fixed checklist, and the senior gate above them is still opus. Skill bugs found in the same sweep: doc-map told agents not to recreate the removed translations and then, three lines later, to mirror every README change into them; the docs mapping still carried `+VN+CN` and the punctuation left by their removal; Procedure jumped 2 → 6; Verify required `scripts/doc-coverage.sh`, which does not exist in this repo. |
||
|
|
29a5da3e4e |
fix(bootstrap): stop a retired plugin build from downgrading the dashboard
`claude plugin update` unpacks the new version into a new cache directory and says "Restart to apply changes". A Claude Code process that was already running does not: it keeps loading the OLD directory, hooks and all. Every session such a process opened then ran the OLD plugin-bootstrap, which saw a state.json naming a different root, concluded the server had to be replaced, repointed agenthub.service at itself and restarted the dashboard onto the version the user had just upgraded away from. The dashboard reported the old version and offered the update again — once per session, forever, while installed_plugins.json and the cache both said the new version was installed. A /agenthub-update from that session "succeeded" and changed nothing. - `supersededBy()` reads Claude Code's installed_plugins.json. When a SIBLING cache directory is the recorded install, the bootstrap logs one line and returns "superseded" — no deps, no unit, no server. `--force` does not override it: the self-update chain forces from the NEW directory, which is never superseded, so the only --force refused is a deliberate downgrade. It cannot hand over either — the process it would replace is the one loading it — so the log line says to restart Claude Code. - Matched by sibling directory, not by the <plugin>@<marketplace> key: correct without parsing a cache path or trusting a name. A phantom entry (installPath that no longer exists) is ignored rather than wedging the bootstrap shut. Also adds `pruneRetiredPluginRoots()`, since the same updates leave a full copy of the repo — node_modules and all — per release; twelve of them measured 1.7 GB on one machine. It runs after writeState, so a bad prune cannot cost the record of a good bootstrap, and it protects four independent readings of "in use": this build, every installPath Claude Code records, the directory agenthub.service names (`unitRoot`, foreign unit included), and any directory still named in the user's settings.json hooks — that last one is the un-restarted process above, seen from the other side. Plus the 2 newest survivors as a rollback cushion, and nothing at all outside ~/.claude/plugins/cache/. |
||
|
|
c90dd7b147 |
fix(bootstrap): self-heal a systemd unit left on a retired plugin root
The residue an interrupted update leaves behind is invisible: the server
runs, every doctor row reads OK, and the unit quietly still names a cache
directory Claude Code deleted during the update. Nothing surfaces it until
a reboot, when the dashboard does not come back and nothing recorded why.
- fastPathOk now rejects a stale unit, so the next SessionStart repairs it
instead of short-circuiting. `unitIsStale` is pure fs on purpose: that
path runs on every session and may not spawn a process.
- Repairing the unit is not restarting the server. A current, healthy
server keeps running and the corrected unit takes effect at the next
start (`pendingSystemdRestart = systemdOwned && needsServer`), and the
detached-spawn fallback is guarded by needsServer so a stale unit alone
cannot start a second server onto a bound port.
- systemdOwned now also requires the unit to be ACTIVE. Installed-but-
stopped owns nothing: skipping the stop there and asking systemd to start
would bind an occupied port and crash-loop under Restart=always.
- /agenthub-doctor gains a systemd row naming the consequence ("will NOT
come back after a reboot") and the fix. WARN, never FAIL — the dashboard
in front of you works, and having no unit at all is a normal setup.
The fast-path test pins the regression against an otherwise fully healthy
install, live server included; without a live server it would have passed
against the unfixed code either way.
|
||
|
|
de8d1079e0 |
fix(bootstrap): never stop the dashboard from inside its own systemd cgroup
A plugin self-update took the dashboard down and left it down. The chain
ran correctly up to the point where the bootstrap SIGTERMs the server it is
replacing — and that server was systemd-managed, with the bootstrap running
as its descendant, i.e. inside its cgroup. Two failures fired at once:
* the server exits 0, and `Restart=on-failure` reads a clean exit as
"meant to stop", so systemd did not bring it back;
* `KillMode=control-group` reaped the bootstrap itself, mid-run, before it
could repoint the unit at the new plugin root or write state.json.
Evidence from the incident: bootstrap.log ends at "stopped the server
started from the previous plugin version" with no systemd line after it,
.bootstrap.lock survived with a dead pid, state.json still named the
previous version, and the unit's ExecStart still pointed at the old cache
dir. The same truncation is visible on three earlier updates — those only
recovered because a later SessionStart bootstrapped again.
systemd-service.js now splits install into writeUserService (repoints the
unit, starts nothing) and restartUserService (--no-block). The bootstrap
skips the manual stop whenever a unit of ours owns the server, and defers
the restart past writeState and releaseLock, so being killed by its own
request costs nothing. The unit is Restart=always: a graceful SIGTERM from
anything must no longer read as "leave it stopped".
installUserService keeps its old semantics for `agenthub service install`,
which runs from a user's shell and is not in the cgroup.
|
||
|
|
c7dbaf2d9f |
refactor: finish the AgentHub rename and drop the MCP server
Two leftovers from the rebrand, done together because they touch the same docs and manifests. **Data directory.** `agent-dashboard` could not simply be renamed in the source: it names a real directory holding the live SQLite database. Startup now moves it once, via `migrateLegacyDataDir()` in `server/lib/claude-home.js` — a same-filesystem rename, so it is instant even at multiple gigabytes and cannot leave two divergent copies. `getDataDir()` stays PURE and resolves to the legacy path until the move happens; the first version of this put the rename inside the getter, which meant `npm run test:server` relocated the developer's real database as a side effect. The call sits behind `require.main === module` and ahead of the router requires, because those pull in db.js, which opens SQLite at module load. A failed rename is silent and the old directory keeps being used: an unfashionable directory name beats a blank database beside an unreachable one. Same treatment for the port-discovery file (`.agent-dashboard.json` → `.agenthub.json`, old one dropped on write). Not renamed on purpose: the Sagi localStorage keys (renaming silently resets everyone's prefs) and the PWA manifest `id` (changing it orphans an installed app). **MCP server.** 4 tool calls in 46 days across 577,132 events, against 4,556 lines of TypeScript, a 2 MB committed build artifact, and a pre-commit guard that had already blocked commits twice. Deleted: `mcp/`, the maintainer skills and `mcp-reviewer` agent, the build check, `plugins/agenthub/.mcp.json`, `docs/MCP.md`, 12 npm scripts, 5 Makefile targets, and every doc section describing it. Kept, because they are unrelated things sharing the name: the Config Explorer's MCP tab and its API, per-lane Playwright MCP, the agenthub-config plugin's MCP audit, and the historical `mcp__plugin_agenthub_*` events that analytics must keep attributing. Verified: server 1432/1432, client 459/459, tsc clean, prettier clean. Both directions of the migration proven — a sandbox-HOME test run leaves the legacy directory untouched, and a real boot moves it with its contents intact. |
||
|
|
17d56d0e01 |
refactor!: rebrand CCAM to AgentHub
Clean break, no compatibility alias. Renames the product name, the CLI binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` -> `AGENTHUB_`), the Prometheus metric namespace (`ccam_*` -> `agenthub_*`), the tmux session prefix (`ccam-lane-<id>` -> `agenthub-lane-<id>`), the per-repo runtime profile directory (`.ccam/profile` -> `.agenthub/profile`), the lanes state root (`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` -> `/agenthub:*`). Because there is no alias, an existing install has to be updated by hand: rename its `.ccam/profile` directory and any `CCAM_*` variables, and reinstall the plugin under its new name. Two other name families are deliberately left alone here, since they are not just strings: the `Claude-Code-Monitor` repository/marketplace name, and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard` runtime directory, which holds the live database and needs a migration rather than a rename. Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across tracked files, `openapi.yaml` regenerated (95 paths, unchanged count). |
||
|
|
6cb7f4f843 |
feat(plugin): install the pipelines' skill plugins on SessionStart
The ship-feature pipelines drive `/superpowers:*` commands as their per-node drivers, so a fresh install had a dependency it was only told about — the `/ccam-doctor` WARN row and a line in the README. `server/lib/skill-plugins.js` now carries the whole thing, shared by the doctor (reports) and the bootstrap (installs) so the two cannot disagree about what is missing. The bootstrap installs at the very end of its DETACHED worker — the install clones from the network and nothing in the 10 s SessionStart hook may wait on it — retrying once behind a `marketplace add`, logging a failure with the manual command, and never throwing: the dashboard, hooks, CLI and MCP all work without these plugins. The outcome is recorded as `state.skillPluginsSettled`, which `fastPathOk()` now also requires, so the check runs exactly once per install and `/ccam-update` is the retry. `CCAM_AUTO_INSTALL_PLUGINS=0|false|off` opts out. Reporting is derived, installing is allowlisted. Requirements come from the loaded templates, which is right for a report; installing from that same source would not be, because a template is a file anyone can drop into `DASHBOARD_PIPELINES_DIR` — it would turn "edit a JSON file" into "run this vendor's install command on the next session". Auto-install consults `AUTO_INSTALLABLE`, a constant in code (`superpowers` today), and reports every other requirement as skipped. Tests inject a `run` seam so no case reaches a real `claude plugin install`, and cover the refusal explicitly. |
||
|
|
13b2bc4ab6 |
fix(service): forward PATH into the ccam systemd --user unit
systemd --user units start with systemd's own minimal PATH, not the installer's shell PATH, so the dashboard couldn't find claude/codex (e.g. installed under ~/.local/bin) when spawning a lane from a service-managed server. Forward process.env.PATH into the unit's environment from both ccam service install and the plugin bootstrap's auto-install path. |
||
|
|
1cb7ac0a4b |
feat(install): auto-start the dashboard on boot via systemd, no Claude Code needed
Adds ccam service install/uninstall/status for checkout installs and wires the same systemd --user unit into plugin-bootstrap.js, so plugin-installed colleagues get boot persistence too instead of needing a Claude Code session to (re)start the server after every reboot. |
||
|
|
205f40c29c |
fix(plugins): stop stripLegacyHooks from deleting the plugin's own hooks
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.
|
||
|
|
a65ee1512e |
fix(plugins): serverIsLive() falsely reported a server as running
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). |
||
|
|
8a82895c65 |
feat(plugins): make CCAM installable straight from a Claude Code plugin
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>
|