Four defects, all from one design mistake: a profile's display name and its
filename were the same string.
- Saving the logged-in account made a SECOND profile of an account another
profile already held. Both then reported themselves active, so there was
nothing to switch between. `POST /profiles` now refuses `EDUPLICATE` and
names the profile that already holds the account. Re-saving into the
account's own profile stays allowed - that is the token-refresh path.
- Worse, an accidental duplicate could not be cleaned up: two profiles of one
account are both active, and the delete guard refused any active profile, so
neither was ever deletable. The guard now blocks only the LAST saved copy of
the live account, which is the actual credential-loss condition it exists
for. The trash button had to follow, or the server would allow what the UI
hid.
- A name could not contain spaces or diacritics, because it had to be
filename-safe. A profile now carries `name` (internal identifier, still the
filename, still what every route takes) and `label` (what the user reads and
edits: 1-60 free-form characters, control characters rejected, stored
trimmed, unique case-insensitively). A label never touches a path, which is
what makes "Tài khoản chính" a legal name.
- Naming happened up front in a "save current account as" field. That field is
gone. The server derives the identifier, the save button lives on the live
account's card and appears only while it is unsaved, and the label is edited
by clicking it (Enter saves, Escape cancels).
Renaming rewrites the label in place and deliberately does NOT move `name`, so
it cannot half-fail and leave a profile unreachable. Profiles written before
labels existed report `label` equal to their `name`, so nothing needs migrating.
Adds `PATCH /api/cc-config/profiles`; `POST` now takes an optional `name`.
Verified: server 1431/1431 (11 new), client 459/459, tsc clean. Also exercised
end to end against a COPY of the real profile store on this machine: the
duplicate deleted, the last copy still refused, a Vietnamese label accepted,
and the stored access/refresh tokens and 0600 mode intact after a rename.
Adds a "Switch profile" button left of Backups on /settings?tool=claude-code.
A *profile* is one Anthropic account - one quota - saved under a name, so the
local `claude` CLI can be pointed at a different account without disturbing
anything else.
A switch swaps exactly two identity artifacts and nothing more: `claudeAiOauth`
inside `<CLAUDE_HOME>/.credentials.json`, and `oauthAccount` + `userID` inside
the `.claude.json` config file(s). Those files are key-merged in place, and only
where they already exist, so `projects`/`numStartups`/everything else survives.
Skills, subagents, commands, memory, plugins, marketplaces, MCP servers and
settings are never read or written here - that is what makes a switch keep the
whole workspace and change only the account. `mcpOAuth` stays put too: those are
the machine's MCP server tokens, not account identity.
Profiles are one JSON file each under `<CLAUDE_HOME>/agenthub-profiles/` (dir
0700, files 0600, atomic temp+rename) and hold live OAuth tokens, so no route
ever returns token material - `GET /profiles` answers identity metadata only.
The dashboard cannot log an account in; only `claude auth login` mints tokens.
Adding a profile is therefore always "log in from a terminal, then snapshot what
the CLI wrote". `activateProfile` re-snapshots the outgoing account first so
tokens Claude Code refreshed since its last save are not lost, and refuses
(`EUNSAVED`) to switch away from an account that was never saved - that would
discard the only copy of its credentials. A switch applies to new sessions only,
since a running session rewrites the config on exit.
Routes: GET/POST/DELETE /api/cc-config/profiles, POST /profiles/activate.
Verified: server 1420/1420 (14 new, incl. token-free responses and 0600/0700
permissions), client 459/459.
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).
A plugin card showed "7 skills, 5 agents" and nothing more — the counts
were the whole story, so finding out WHICH skills a plugin ships meant
opening its install directory by hand. The contribution chips are now
buttons: clicking one expands a list naming each artifact with its
description, one surface open at a time. Commands render with their
slash. Hooks stay a plain chip — a plugin's hooks are matcher/command
pairs declared inline in plugin.json, not named artifacts.
`contributes.items` carries the four surfaces server-side; the existing
counts keep their type and are now derived from those lists, so a chip
reading "3 commands" can never sit beside a list holding two.
That also fixes a real blind spot. Each surface was read only from the
conventional install subdirectory, but plugin.json may declare its own
paths — this repo's own `ccam` lists three files under
plugins/ccam/commands/ and keeps commands/ empty, so it reported zero
commands. Both sources are now read and de-duplicated. A declared path
resolving outside the plugin's install directory is dropped: a manifest
is third-party data and must not turn the dashboard into an arbitrary
file reader.
Also pins the Refresh button's width: both labels share one grid cell so
the button is always as wide as "Refreshing…" / "Đang làm mới…" and no
longer resizes the toolbar when the state flips. Measuring the longest
real string beats a min-width that would be wrong in the next language.
Verified: npm run test:server (1370 tests, +6 for the new discovery),
npm run test:client (457, +3 for the expand behaviour), tsc --noEmit,
prettier and check-headers clean. Docs synced: docs/API.md, openapi.yaml
(new CcConfigPluginItem schema), ARCHITECTURE.md, docs/PLUGINS.md.
Scrolling a lane console repainted history with stray characters glued to
the left edge of blank lines. Reproduced against a real tmux attach: a pane
WIDER than xterm makes every over-long row autowrap its tail onto the next
row's left margin, and tmux — believing that row unchanged — never repaints
over it. One column of difference leaves one stray character per wrapped
line, two columns leave two; matched widths render clean.
TerminalView only reported its size on WS open and on the container's
ResizeObserver, so nothing corrected:
- cols moving with no container resize (webfont load, page zoom, scrollbar)
- tmux's `window-size latest` letting a second attach client (another tab,
`ccam lanes shell`) resize the window out from under this one
Size is now reported from xterm's own onResize, re-asserted when the
terminal regains focus, and never reported as zero — the console collapses
with CSS, and fitting a 0x0 container would resize the real pane to nothing.
client 447/447, tsc clean.
The panel aggregated every lane at once, which answers "does this stage get
proved across the project" but never "does THIS lane prove it". A lane picker
now narrows the same aggregation through ?lane=<id> on
GET /api/lanes/pipeline-stats, counting that lane plus its own archived
features — dropping the features would hide most of a lane's history the moment
it starts its next one.
The response carries a `lanes` array that the filter never narrows: it
populates the picker, so it has to keep offering every other lane even while
the counts show one. For the same reason the picker renders above the empty
branch rather than inside the populated one — narrowing to a lane that has
recorded nothing would otherwise be a one-way trip, with no control left to
select anything else. A lane filter that is not a positive integer is rejected
with 400 EBADLANE rather than silently aggregating everything.
Restores the five analytics sections that turned out to be worth keeping
(patterns, model delegation, error propagation, session complexity, compaction
impact) and reworks what the page opens on.
The lane panel no longer mirrors the Workspace's live lane strip. It now
answers the question the strip cannot: how often each pipeline node actually
passes. GET /api/lanes/pipeline-stats aggregates every stage record of every
lane AND every archived feature — a lane's own `stages` only holds its current
feature, so without the archive the history resets each time a lane starts new
work. Evidence-backed passes are counted apart from bare claims, the same line
the pipeline map draws between `done` and `passed-no-evidence`.
Beside it, GET /api/workflows/skill-usage reports which skills and plugins the
sessions actually reach for. Only PreToolUse rows are counted: every call also
emits a PostToolUse, and counting both would double every number. A plugin's
total includes calls to the MCP tools it ships, so one used purely through MCP
does not read as unused.
Still dropped from issue #6: the agent collaboration network and the subagent
effectiveness scorecards.
Settings becomes a tabbed shell (SettingsHub) with one tab per AI tool:
Dashboard (existing settings), Claude Code (CcConfig), plus Codex and
Cursor placeholders. Active tab lives in the ?tool= query param, and the
old /cc-config route redirects to /settings?tool=claude-code. The
standalone sidebar entry is dropped.
Adding a tool is one entry in TOOL_TABS plus its i18n label.
The Claude Config page was read-only for these four surfaces. It can now:
- register a marketplace from a pasted GitHub `owner/repo`, http(s) URL or
absolute path, and install / enable / disable / update / uninstall plugins,
all delegated to the `claude plugin …` CLI so its registry stays consistent;
- edit hooks and settings.json keys in the user, project and project-local
layers.
Server:
- `server/lib/cc-plugin-cli.js` wraps the CLI with `execFile` and an explicit
argv array — never a shell string. Every argument is validated before the
spawn (shape allowlist per kind, no control characters, no leading `-`, no
`..`), `CLAUDE_CONFIG_DIR` is pinned to the dashboard's CLAUDE_HOME, stdin is
closed so a prompt errors instead of hanging, and mutating calls are
serialized in-process.
- `cc-mutate.patchSettings` merges individual dotted keys into one settings
layer. It re-reads the file unredacted, so keys the UI never displayed —
including secret-valued ones — survive byte-for-byte. A payload carrying the
`<redacted>` placeholder is refused (EREDACTED), as are `__proto__` /
`prototype` / `constructor` segments; a settings file that does not parse is
reported (EBADJSON) rather than clobbered. Backup first, then atomic write.
- Six additive routes on /api/cc-config; a non-zero CLI exit is a 422 carrying
the CLI's own message, a missing binary a 503.
Client: install/add bars, per-card plugin and marketplace actions behind a
two-click confirm, and edit modes for hooks and settings that send only the
keys that changed. New i18n keys in en + vi; the explainer banners that said
these surfaces were read-only now describe what the writes actually do.
Tests: 61 new server tests (CLI argv construction and validation, the settings
key-merge, and the six routes against a fake `claude` on PATH).
Closes issue #5 (Dashboard | Update menu & page):
- Analytics content (heatmap, sparkline, token/cost/workflow/productivity
charts) moves into a new "Analytics" tab on the Dashboard page, alongside
the existing Monitor/Health tabs. The standalone Analytics page, its
/analytics route, and its sidebar menu entry are removed.
- The Kanban Board page, its /kanban route, and its sidebar menu entry are
removed entirely (page content deleted, not just hidden).
- Updated ARCHITECTURE.md, README.md, client/README.md, docs/API.md, and
the affected client tests/snapshots to match.
Unmatched Ask questions now adopt a dedicated "Sagi" lane (POST
/api/lanes/ensure, cwd defaults to $HOME) and spawn a tmux-backed claude
run there instead of just prefilling the Run page composer.
`claude` only offers bypass in its shift+tab permission cycle when started
with `--allow-dangerously-skip-permissions`, which the pane argv never passed.
A dashboard-started run was therefore stuck cycling plan/auto/manual/
accept-edits, while the same session attached from a real terminal could
reach bypass.
The flag makes bypass SELECTABLE, not enabled — `claude --help`: "Enable
bypassing all permission checks as an option, without it being enabled by
default." The starting mode is still whatever `--permission-mode` says
(`acceptEdits` by default) and `--dangerously-skip-permissions`, which would
actually turn it on, is still never passed. Entering bypass remains an
explicit human shift+tab in the pane, or an explicit
`permissionMode: "bypassPermissions"` on POST /api/run.
Three separate reasons the Workspace terminal looked unscrollable:
- xterm falls back, on an alt-screen buffer whose program has not enabled
mouse tracking, to translating each wheel notch into a cursor-key press.
Inside a `claude` pane that reads as arrow up/down, so scrolling walked the
prompt history. A custom wheel handler returning false suppresses only that
emulation branch; real mouse reports come from a separate listener xterm
registers when the program does request wheel events.
- TerminalView never reconnected, so a server restart left the pane frozen on
its last painted frame — still live-looking, but swallowing every keystroke
and mouse report. It now re-attaches 1.5s after an unexpected close and
stops only on the server's `exit` frame; tmux keeps the session, so the
re-attach repaints in full.
- tmux repaints the whole pane, so xterm's own scrollback is always empty and
its viewport scrollbar renders as a groove with no thumb that cannot move.
Hidden, since scrolling there goes through mouse reports, not the DOM.
spawnRun adopted any existing `ccam-lane-<id>` tmux session without looking
at it, so a Resume issued while the session sat at a bare shell prompt (left
by `ccam lanes shell`, or by a `claude` that had already exited) dropped the
whole argv: no `--resume` ran, no initial prompt was typed, and the API still
answered 200. The lane's DB `run_id` is not set in that case, so the ERUNLIVE
guard in the start route never saw it either.
Reuse the pane instead of erroring: when the session exists and
`#{pane_current_command}` is a shell, type the argv into that pane and record
the run. A pane running a program (a live `claude`, an editor, a build) is
still adopted untouched, so attaching shows what is running rather than typing
over it. `sendCommand` POSIX single-quotes every argument and uses
`send-keys -l`, the only place in tmux.js that composes a command line.
- Fix POST /api/run param: change 'prompt' to 'initialPrompt' (Finding 1)
- Fix POST /api/run response shape: show actual fields from publicRun() (Finding 2)
- Delete stale 'Running and releasing lanes' section with old POST /api/lanes/:id/start (Finding 3)
- Delete stale POST /api/lanes/:id/message section — endpoint now returns 400 EUNSUPPORTED (Finding 4)
All changes verified against actual code:
- server/routes/run.js:285 uses body.initialPrompt
- server/lib/pty-run.js:144-156 publicRun() returns {id, laneId, status, cwd, model, ...}
- server/routes/lanes.js:1134 message action returns 400 error
Deletes desktop/ (Electron wrapper), deployments/ (Helm/Kustomize/
Terraform/CI for cloud deploy), and monitoring/ (Prometheus + Grafana
stack) along with DESKTOP.md, DEPLOYMENT.md, docker-compose.full.yml,
their npm scripts, and every dangling reference across README,
ARCHITECTURE, INSTALL, SETUP, docs/, and the repeated per-file
MODULE_GUIDE "Observability" boilerplate comment. The GET /api/metrics
endpoint itself is untouched — it's the dashboard's own route, not
part of the removed monitoring stack.
Creation is the only point the UI could ever set a lane's template, and it
never offered the choice — so every lane added from "+ Add lane" was born
on `default` and rendered an 8-node map for a 16-node workflow, with no
screen able to change it afterwards. That is the defect that made the
ship-feature template unreachable from the browser.
The modal now shows a *Pipeline template* select fed by
`GET /api/lanes/pipelines`, labelled with each template's node count so the
consequence of the choice is visible. A failed fetch degrades to a `default`
option rather than blocking lane creation.
`pipeline` was already accepted by `POST /api/lanes` but silently dropped by
`/ensure` and `/worktree`, which build their own createLane payloads; both
now pass it through, and both map `EBADPIPELINE` to 400 like `EBADCWD`.
A lane's pipeline map only ever moved when a skill remembered to call
`ccam stage`, and the ship-feature template shipped with no detection rules
at all — so a lane driven by Superpowers skills sat at whatever stage it
last declared, and the `gates` node was never declared by anything.
Detection (`detect` rules on each node) now covers the Superpowers skill
invocations and the `ccam`/`gh` commands the ship-feature-lane skill
actually runs. It stays a safety net, not the mechanism: forward-only,
never `done`, never overriding a declaration. Two rules were deliberately
left out — `git diff` on `review` (this repo's own tests record it pinning
a lane at `review` on a real session) and anything on `merged`/`done`.
Stage vocabulary grows to 50 names over the same 16 nodes, following
Shipyard's PHASES shape: sub-states like `migration-collision`,
`e2e-scoped` and `gate-blocked` say WHY a lane sits on a node without the
map growing a node per reason. Every alias has a source — the skill
declares it, `default.json` uses it, or Shipyard's PHASES lists it.
Two silent failures fixed along the way:
- `lane.stages` is keyed by the raw declared string, so a stage declared
under an alias lost its `--evidence` and rendered amber instead of
green. `stageRecords` resolves each key onto its node.
- `ccam stage <typo>` stored fine and then rendered nowhere. It now warns
on stderr while still exiting 0.
`ccam lanes pipeline` closes the gap that made all of this invisible: a
lane could only be assigned a template at creation, and no screen in the
web UI offers the choice, so every lane added from "+ Add lane" was stuck
on `default`'s 8 nodes. An unknown template id is now refused rather than
silently falling back to `default` on read.
Also merges the repo's own `ship-feature` skill into the Superpowers
workflow: it delegates planning/TDD/review/verification instead of
restating them, and declares a stage at each phase.
archiveActiveFeature was overwriting a feature's own title with the
LIVE lane's title on every clear/switch — a --title set on activation
silently disappeared. activateFeature restored stage/status/gate/CI/
stages/notes onto the live lane when switching back to a past feature,
but never links, so they vanished on reactivation. Both are fixed, with
a regression test for each (full activate/archive/reactivate round trip
for links).
Also fixes server/routes/lanes.js: the doc comment explaining GET
/:id/git's rationale had been left sitting above the newly-inserted
/:id/features routes instead of its own route.
docs/API.md's Lane features section described fields and behavior that
don't exist in the real routes (an "active" boolean, a POST response
containing "archivedPrevious", a "409 ESTALE" concurrency response) —
rewritten to match the actual request/response shapes exactly.
docs/API.md's Locks section described a timeoutMs request param, a
408-timeout response, and field names (acquiredAt/acquiredMs) that
don't exist anywhere in the actual routes.js — the server is single-shot
and never blocks; timing out is a CLI-only concept. Also fixes the
section being spliced into the middle of the pre-existing Sessions
heading and its content.
docs/LANES.md said the owner file stores an epoch in milliseconds
(it's seconds), that the CLI polls every ~1s (it's ~2s), that the lane
card shows a "waiting" state (no such server-side concept exists, only
who currently holds), and included a fabricated "manually transfer a
lock's holder identity" procedure that also contradicted the
never-touch-the-lock-directory etiquette rule stated right above it.
Client-side rendering for the per-lane runtime facts (slot, ports,
database, Redis index, service liveness) added in the server-side
A1/A2 work, plus the doc updates (README, CLAUDE.md, docs/API.md,
client/server READMEs) describing the new profile.env keys, hook
environment contract, and REST endpoints.
Internal SmartGift build of a Claude Code monitoring dashboard.
Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.
Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.
Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.