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).
This commit is contained in:
2026-09-07 12:09:15 +07:00
parent ae9b3dbcd5
commit 17d56d0e01
345 changed files with 2705 additions and 2727 deletions
+2 -2
View File
@@ -26,7 +26,7 @@ const crypto = require("crypto");
* bundles but low enough to stop most zip-bomb attacks from filling disk.
*/
const MAX_EXTRACT_BYTES = parseInt(
process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES || String(4 * 1024 * 1024 * 1024),
process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES || String(4 * 1024 * 1024 * 1024),
10
);
@@ -63,7 +63,7 @@ function safeJoin(root, entryName) {
* Create a unique temp directory for extraction under the OS tmpdir.
* Caller is responsible for cleanup via `rmTempDir`.
*/
function mkTempDir(prefix = "ccam-import-") {
function mkTempDir(prefix = "agenthub-import-") {
const dir = path.join(os.tmpdir(), prefix + crypto.randomBytes(6).toString("hex"));
fs.mkdirSync(dir, { recursive: true });
return dir;
+2 -2
View File
@@ -264,8 +264,8 @@ const readOutputStyles = readSimpleMdSurface("output-styles");
* Absolute `.md` files a plugin contributes for one surface: the conventional
* `<root>/<subdir>` directory PLUS whatever `plugin.json` points at for that
* key. Both matter — a manifest entry may be a single file or a directory, and
* a plugin that declares its commands explicitly (this repo's own `ccam` lists
* three files under `./plugins/ccam/commands/`) keeps NOTHING in `commands/`,
* a plugin that declares its commands explicitly (this repo's own `agenthub` lists
* three files under `./plugins/agenthub/commands/`) keeps NOTHING in `commands/`,
* so reading only the conventional directory reported zero for it.
*
* A manifest is data from a third-party plugin, so a declared path that escapes
+1 -1
View File
@@ -2,7 +2,7 @@
* @file Turns a GitHub Actions run for one ref into a single derived fact —
* {state, checks} — for ship-feature-lane's Stage 13 CI-wait. Deliberately a
* snapshot, not a poll loop: the driving skill owns the wait/retry decision
* (docs/LANES.md "Orchestration: what CCAM does NOT do"), this module only
* (docs/LANES.md "Orchestration: what AgentHub does NOT do"), this module only
* reports what gh already knows.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
+1 -1
View File
@@ -313,7 +313,7 @@ function ingestCodexOnce({ broadcast } = {}) {
if (detection) {
try {
changed = lanesLib.recordDetection(lane.id, detection).written;
// Auto-advance: a codex session has no hook and rarely calls `ccam
// Auto-advance: a codex session has no hook and rarely calls `agenthub
// stage` on its own, so without this its card sits at `idle` forever
// while the amber badge is the only thing that ever moves. Gated on
// recordDetection having WRITTEN, so it inherits that function's
+2 -2
View File
@@ -4,7 +4,7 @@
* from its rollout journal — that keeps the pipeline map moving, but an
* inferred stage carries no evidence, so its node renders
* `passed-no-evidence` (amber) rather than `done`. Only the session's own
* `ccam stage <node> --evidence "..."` call can supply that, and a codex
* `agenthub stage <node> --evidence "..."` call can supply that, and a codex
* session has no hook or wrapper that does it for it.
*
* This module never writes `stage`, `stages` or `detected_stage` itself. It
@@ -65,7 +65,7 @@ function sweepCodexNudges() {
tmux.sendText(
runId,
`[ccam] Stage "${lane.stage}" was inferred and has no evidence — run: ccam stage ${lane.stage} --tool codex --evidence "..."`
`[agenthub] Stage "${lane.stage}" was inferred and has no evidence — run: agenthub stage ${lane.stage} --tool codex --evidence "..."`
);
nudgedFor.set(lane.id, lane.stage);
nudged += 1;
+1 -1
View File
@@ -33,7 +33,7 @@
"use strict";
const EXPORT_FORMAT = "ccam-export";
const EXPORT_FORMAT = "agenthub-export";
const EXPORT_VERSION = 1;
// Tables serialized into the bundle. Order matters for restore (parents before
+9 -9
View File
@@ -3,8 +3,8 @@
* the ship-feature-lane pipeline's agent templates
* (qc-local, senior-gate-reviewer) into a lane's own .claude/agents/, so a
* driving session can launch them by the BARE subagent_type name. Source is
* the repo's own `agents/` — the same files the ccam plugin ships, where
* they resolve as `ccam:qc-local`; installing them into the lane is what
* the repo's own `agents/` — the same files the agenthub plugin ships, where
* they resolve as `agenthub:qc-local`; installing them into the lane is what
* makes the unprefixed name work there too. Plain file I/O except
* for one git call — resolving where a worktree lane's shared info/exclude
* actually lives, the same git-dir/git-common-dir distinction E2's
@@ -25,29 +25,29 @@ const EXCLUDE_LINE = ".claude/agents/";
// loads from the working directory. Marker-delimited so a reinstall replaces
// exactly this block and never the user's own prose, and appended rather than
// written over an existing file.
const STAGE_BLOCK_START = "<!-- ccam:stage:start -->";
const STAGE_BLOCK_END = "<!-- ccam:stage:end -->";
const STAGE_BLOCK_START = "<!-- agenthub:stage:start -->";
const STAGE_BLOCK_END = "<!-- agenthub:stage:end -->";
const STAGE_BLOCK = `${STAGE_BLOCK_START}
## Declare your lane stage as you work
This directory is watched by CCAM. Its pipeline map advances on its own by
This directory is watched by AgentHub. Its pipeline map advances on its own by
*inferring* your stage from your tool calls, but an inference carries no
evidence — the node stays amber, never green. Only your own declaration does:
\`\`\`bash
ccam stage <stage> --tool codex --evidence "<what is actually done>"
agenthub stage <stage> --tool codex --evidence "<what is actually done>"
\`\`\`
- Call it at every real transition — starting to plan, starting to implement,
running tests, opening the PR — not once at the end.
- \`--tool codex\` matters: one directory can back both a \`claude\` lane and a
\`codex\` lane, and without a tool the command fails rather than guess. Inside
a pane the dashboard started you need nothing — it carries \`CCAM_LANE_ID\`.
- \`ccam stage\` warns on stderr and names the valid nodes if a stage matches
a pane the dashboard started you need nothing — it carries \`AGENTHUB_LANE_ID\`.
- \`agenthub stage\` warns on stderr and names the valid nodes if a stage matches
none of this lane's pipeline template.
- If it reports that no lane owns this directory, skip the stage calls and
carry on. Stage reporting is reporting, never control flow.
- A one-line \`[ccam]\` reminder may be typed straight into this pane asking for
- A one-line \`[agenthub]\` reminder may be typed straight into this pane asking for
evidence on the current stage. That is the dashboard, not the user — treat it
as the same instruction as this section and make the real call.
${STAGE_BLOCK_END}`;
+5 -5
View File
@@ -1,5 +1,5 @@
/**
* @file Node.js project detection and `.ccam/profile/` scaffolding (A3).
* @file Node.js project detection and `.agenthub/profile/` scaffolding (A3).
* `detectNode` reads a repository's filesystem (package.json layout,
* docker-compose.yml) without executing anything; `scaffoldProfile` turns
* those facts into a working profile; `checkProfile` validates one. Only a
@@ -218,7 +218,7 @@ function bootLine(name, service) {
}
/**
* Scaffold `.ccam/profile/` under `repoPath` from `detectNode`'s facts.
* Scaffold `.agenthub/profile/` under `repoPath` from `detectNode`'s facts.
* Refuses to overwrite an existing `profile.env` unless `options.force`.
*
* @param {string} repoPath - Absolute repository path.
@@ -227,7 +227,7 @@ function bootLine(name, service) {
* @returns {{written: string[], todos: string[]}}
*/
function scaffoldProfile(repoPath, facts, options = {}) {
const profileDir = path.join(repoPath, ".ccam", "profile");
const profileDir = path.join(repoPath, ".agenthub", "profile");
const envPath = path.join(profileDir, "profile.env");
if (fs.existsSync(envPath) && !options.force) {
throw Object.assign(new Error(`profile already exists at ${envPath}`), {
@@ -328,7 +328,7 @@ function containsTodo(text) {
/**
* Validate a scaffolded (or hand-written) profile. Read-only: never mutates
* anything, never spawns a hook. `dir` may be a repo root (profile at
* `.ccam/profile/`) or a `.ccam/profile/` directory itself.
* `.agenthub/profile/`) or a `.agenthub/profile/` directory itself.
*
* @param {string} dir
* @returns {Promise<{ok: boolean, errors: string[], warnings: string[]}>}
@@ -337,7 +337,7 @@ async function checkProfile(dir) {
const errors = [];
const warnings = [];
const candidates = [path.join(dir, ".ccam", "profile"), dir];
const candidates = [path.join(dir, ".agenthub", "profile"), dir];
const profileDir = candidates.find((p) => fs.existsSync(path.join(p, "profile.env")));
if (!profileDir) {
return { ok: false, errors: [`no profile.env found under ${dir}`], warnings: [] };
+1 -1
View File
@@ -64,7 +64,7 @@ function rewriteEnvText(text, want) {
* A no-op when the profile declares no `ENV_FILES` — the feature is off by
* default, and an adopted repo that never opts in gets no `.env` writes at
* all. Refuses on an adopted lane (`assertManaged`): that file is the user's
* real working config, not a template CCAM may overwrite.
* real working config, not a template AgentHub may overwrite.
*
* @param {object} lane - Lane row; `slot` must already be allocated.
* @param {object} profile - Resolved profile.
+2 -2
View File
@@ -4,10 +4,10 @@
* got reparented to pid 1 — a live session's MCP keeps its real parent and
* is left alone) and cap hook logs that have grown past 10MB back to their
* last 2MB. Port of the two pieces of Shipyard's `lane-gc.sh` that match
* CCAM's actual architecture — see docs/superpowers/specs/2026-08-05-lane-gc-design.md
* AgentHub's actual architecture — see docs/superpowers/specs/2026-08-05-lane-gc-design.md
* for why the other three (stale-worktree auto-removal, state archiving,
* scratch-debris sweep) are out of scope. Machine-wide, not lane-scoped —
* no route, same local-only shape as `ccam skills install`.
* no route, same local-only shape as `agenthub skills install`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
+5 -5
View File
@@ -1,7 +1,7 @@
/**
* @file The stack seam. A profile is a repository's own description of how to
* build, boot and check its stack: `<repo>/.ccam/profile/` holding a `profile.env`
* of declarations plus a `hooks/` directory of shell scripts. CCAM stays
* build, boot and check its stack: `<repo>/.agenthub/profile/` holding a `profile.env`
* of declarations plus a `hooks/` directory of shell scripts. AgentHub stays
* stack-agnostic and calls those hooks with a stable environment contract, which
* is the same contract Shipyard's `run_hook` exports so its profiles port over
* unchanged.
@@ -21,10 +21,10 @@ const { spawn } = require("node:child_process");
const { slotDirs, dataFacts } = require("./lane-slots");
/** Directory, relative to a repository root, holding its profile. */
const PROFILE_SUBDIR = path.join(".ccam", "profile");
const PROFILE_SUBDIR = path.join(".agenthub", "profile");
/**
* Hook names CCAM will run, ever.
* Hook names AgentHub will run, ever.
*
* A fixed allowlist rather than "whatever is in hooks/": `:name` arrives from an
* HTTP route, and a name taken from a request is a path taken from a request.
@@ -197,7 +197,7 @@ function profileSearchPaths(lane) {
/**
* Whether a named integration is turned on for this lane — reads
* .ccam/profile/integrations.env (same two-location search as profile.env:
* .agenthub/profile/integrations.env (same two-location search as profile.env:
* the lane's own working copy first, the source repo second) and checks
* <NAME>_ENABLED=1. A missing file or missing key is off, never an error —
* same off-by-default shape every other optional declaration follows.
+9 -7
View File
@@ -4,23 +4,23 @@
* data-isolation lifecycle (A2): `provisionLane`/`resetLaneData`/`removeLaneData`
* seed `.env`, create/drop the lane's own database, and run `bootstrap`/
* `migrate`/`seed` at the points Shipyard's `lane-bootstrap.sh`/`lane-up.sh`/
* `lane-reset.sh`/`lane-remove.sh` do — CCAM's version of "ports came from A1,
* `lane-reset.sh`/`lane-remove.sh` do — AgentHub's version of "ports came from A1,
* everything else a lane needs to run its own stack comes from here."
*
* Two properties shape everything here.
*
* Services are FULLY DETACHED (see `harness_spawn` in lane-profile.js), so a
* lane's stack outlives both the hook that started it and the dashboard itself —
* restarting or updating CCAM must never kill work in progress.
* restarting or updating AgentHub must never kill work in progress.
*
* Liveness is COMPUTED, never stored. Whether a stack is up is not a fact CCAM
* Liveness is COMPUTED, never stored. Whether a stack is up is not a fact AgentHub
* controls: a process dies to OOM, to a stray `kill`, to a reboot. A cached
* "running" flag would be wrong from that moment until something noticed, so
* `runtimeFacts` re-derives it from pid files and port probes on every read. The
* payoff is that adopting a stack after a dashboard restart needs no code at all.
*
* This module writes `slot` and `ports` and nothing else on the lane row. It never
* writes `stage`, `status` or `notes`: in CCAM those describe the AGENT's work, not
* writes `stage`, `status` or `notes`: in AgentHub those describe the AGENT's work, not
* the stack's state, and conflating "a server is listening" with "an agent is
* running" would corrupt lane liveness. Boot failures live in `last-error.json`
* and surface through `runtimeFacts`.
@@ -58,7 +58,9 @@ function requireProfile(lane) {
const profile = resolveProfile(lane);
if (!profile) {
throw Object.assign(
new Error(`lane has no .ccam/profile — looked in: ${profileSearchPaths(lane).join(", ")}`),
new Error(
`lane has no .agenthub/profile — looked in: ${profileSearchPaths(lane).join(", ")}`
),
{ code: "ENOPROFILE", searched: profileSearchPaths(lane) }
);
}
@@ -299,8 +301,8 @@ async function resetLaneData(lane, profile, options = {}) {
* Drop a managed lane's database and its `_test` sibling before the rest of
* removal tears down the worktree and state directory.
*
* A no-op for an adopted lane — its data was never CCAM's to create, so it is
* never CCAM's to destroy either, the same invariant `assertDestroyable`
* A no-op for an adopted lane — its data was never AgentHub's to create, so it is
* never AgentHub's to destroy either, the same invariant `assertDestroyable`
* enforces for the worktree itself. Best-effort per database: mirrors
* Shipyard's `dropdb --if-exists ... || true` — a failed drop is logged, not
* thrown, because it must never block removing the lane's dashboard record.
+5 -5
View File
@@ -1,6 +1,6 @@
/**
* @file A lane's own database: create it once at provisioning and boot, drop
* it on remove. CCAM stays stack-agnostic here — `createdb` vs `mysqladmin
* it on remove. AgentHub stays stack-agnostic here — `createdb` vs `mysqladmin
* create` vs `touch foo.db` genuinely differ, so the actual command lives in
* the profile's `db-create.sh`/`db-drop.sh` hooks (already in the A1
* allowlist); this module only decides WHEN to call them and guards WHICH
@@ -16,11 +16,11 @@ const { dbName, slotDirs } = require("./lane-slots");
const { runHook } = require("./lane-profile");
/**
* Where CCAM records that a slot's database has already been created.
* Where AgentHub records that a slot's database has already been created.
*
* A hook can't be trusted to know this on its own without being stack-aware
* (a plain `createdb` errors on a database that already exists; `touch`
* wouldn't), so CCAM tracks it itself with one flag file per database name,
* wouldn't), so AgentHub tracks it itself with one flag file per database name,
* beside the rest of a slot's runtime bookkeeping. This is also how `upLane`
* tells "freshly created" from "already existed" to decide whether to seed.
*/
@@ -63,10 +63,10 @@ async function ensureDatabase(lane, profile, options = {}) {
* Drop a database via the profile's `db-drop` hook.
*
* Two checks run before anything is spawned, and neither trusts the caller:
* `assertManaged` (an adopted lane's data is the user's own, never CCAM's to
* `assertManaged` (an adopted lane's data is the user's own, never AgentHub's to
* destroy) and a name check against what THIS lane's slot actually derives —
* the lane's own database or its `_test` sibling, and nothing else. Only a
* name CCAM itself computed can ever reach the hook.
* name AgentHub itself computed can ever reach the hook.
*
* @param {object} lane - Lane row; `slot` must already be allocated.
* @param {object} profile - Resolved profile.
+4 -4
View File
@@ -1,13 +1,13 @@
/**
* @file Slot and port allocation for lane runtimes. A slot is the small integer
* every per-lane runtime fact derives from — the numbering Shipyard gets for free
* from its fixed `lane1..lane9` directories, and CCAM, whose lanes are keyed by
* from its fixed `lane1..lane9` directories, and AgentHub, whose lanes are keyed by
* `cwd`, has to allocate. Ports come from `PORT_BASE_<name> + slot`, stepping
* aside when something outside CCAM already holds the number. Database name,
* aside when something outside AgentHub already holds the number. Database name,
* Redis logical index, and upload directory are the same idea one layer up
* (`dataFacts`) — every fact a lane's slot number determines, in one place.
*
* Allocation is the one runtime fact CCAM fully controls, which is why it lives
* Allocation is the one runtime fact AgentHub fully controls, which is why it lives
* in the database (transactional, uniquely indexed) while liveness does not.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -24,7 +24,7 @@ const { LANES_ROOT } = require("./worktree");
* Nine by default, which is Shipyard's ceiling and worth keeping as a default:
* a single decimal digit keeps `base + slot` readable (`:8003` is lane 3), and
* Redis ships 16 logical databases, so A2's per-lane index stays in range. It is
* configurable because CCAM, unlike Shipyard, has no structural reason to stop at
* configurable because AgentHub, unlike Shipyard, has no structural reason to stop at
* nine — a machine that can run twenty stacks may raise it, at the cost of ports
* that no longer read as a slot number.
*
+3 -3
View File
@@ -190,10 +190,10 @@ async function unmergedFiles(cwd) {
* already established. */
async function setupMergeDriver(cwd, generatedPaths) {
if (!generatedPaths.length) return;
await git(cwd, ["config", "merge.ccam-generated.driver", "true"]);
await git(cwd, ["config", "merge.agenthub-generated.driver", "true"]);
await git(cwd, [
"config",
"merge.ccam-generated.name",
"merge.agenthub-generated.name",
"keep ours; regenerated post-merge by the profile regen hook",
]);
@@ -204,7 +204,7 @@ async function setupMergeDriver(cwd, generatedPaths) {
const lines = new Set(existing.split("\n").filter(Boolean));
let changed = false;
for (const gp of generatedPaths) {
const line = `${gp} merge=ccam-generated`;
const line = `${gp} merge=agenthub-generated`;
if (!lines.has(line)) {
lines.add(line);
changed = true;
+3 -3
View File
@@ -523,7 +523,7 @@ function classifyLiveness({ status, stage, ageSec }, deadSec = DEAD_SEC) {
* only path to `done`.
*
* The `current` node is never flagged, even when it has no `stages` entry under
* its own id: declaring by ALIAS (`ccam stage coding` → the `implement` node)
* its own id: declaring by ALIAS (`agenthub stage coding` → the `implement` node)
* keys `stages` by the raw declared string, so the node the agent says it is on
* would otherwise render as an inference instead of the blue `current` ring.
* Past nodes go through `stageRecords`, which resolves those alias keys — a
@@ -550,8 +550,8 @@ function withDetected(states, pipeline, lane) {
* The inferred stage a lane should be RENDERED at, or null when the lane has a
* declaration of its own to render.
*
* A lane whose sessions never call `ccam stage` — any repo that isn't wired to
* CCAM — has no declaration at all: `stage` sits on the DB sentinel `idle` and
* A lane whose sessions never call `agenthub stage` — any repo that isn't wired to
* AgentHub — has no declaration at all: `stage` sits on the DB sentinel `idle` and
* `stages` stays `{}` forever, so its pipeline map drew 100% pending while
* detection was quietly tracking every node the agent reached. Such a lane is
* rendered from its detection instead.
+1 -1
View File
@@ -57,7 +57,7 @@ function readOwner(name) {
/**
* Try to acquire a named lock once. Never blocks, never retries — the caller
* (the CLI) owns the polling loop; CCAM's server-side primitives are always
* (the CLI) owns the polling loop; AgentHub's server-side primitives are always
* single-shot.
*
* A stale holder (older than `effectiveMaxHoldSec()`) is broken automatically:
+1 -1
View File
@@ -41,7 +41,7 @@ function loadAll() {
icon: n.icon || "",
gate: !!n.gate,
// The one command that drives THIS node — a slash command
// (`/superpowers:writing-plans`) or a shell one (`ccam lanes hook
// (`/superpowers:writing-plans`) or a shell one (`agenthub lanes hook
// ci-gate`), mirroring whatever the node's own `detect` rules watch
// for. `doc.skill` above names the driver for the whole pipeline;
// this narrows it to the step, so a reader of the pipeline map can
+2 -2
View File
@@ -5,7 +5,7 @@
* `lane_features` already tracks. Ported from Shipyard's
* `dashboard/server/services/proof.js`; the "current feature" special-casing
* that file needed (it has no DB, so it parsed a branch regex) is dropped —
* CCAM's `lane_features` row already IS that source of truth.
* AgentHub's `lane_features` row already IS that source of truth.
*
* Path containment is the entire security surface here: every path this
* module touches is resolved, realpath'd, and checked to be inside the
@@ -249,7 +249,7 @@ function deleteProof(laneId, { slug, group, images } = {}) {
* already points at the right place.
*
* Never auto-invoked by this codebase; exposed only as the explicit
* `ccam lanes proof-link` primitive.
* `agenthub lanes proof-link` primitive.
*
* @param {object} lane - A hydrated lane row (must have `cwd`).
* @returns {{linked: boolean}} `linked: false` when nothing changed.
+2 -2
View File
@@ -12,10 +12,10 @@
const tmux = require("./tmux");
const RUN_ID_RE = /^ccam-lane-\d+$/;
const RUN_ID_RE = /^agenthub-lane-\d+$/;
/**
* Reject anything that isn't exactly `ccam-lane-<digits>` before it can ever
* Reject anything that isn't exactly `agenthub-lane-<digits>` before it can ever
* reach a tmux/PTY command — the trust boundary for this WS path, since a
* validated runId is the only thing standing between an authenticated WS
* client and naming an arbitrary session on the host.
+10 -10
View File
@@ -13,9 +13,9 @@
* exists but its pane sits at a shell prompt, the argv is typed into that
* pane instead of being dropped on the floor by a silent adopt.
*
* Every session is named `ccam-lane-<laneId>` so a real terminal can attach
* to the exact same session (`tmux attach -t ccam-lane-<id>`, or
* `ccam lanes shell`) — that's the whole point: the dashboard both creates
* Every session is named `agenthub-lane-<laneId>` so a real terminal can attach
* to the exact same session (`tmux attach -t agenthub-lane-<id>`, or
* `agenthub lanes shell`) — that's the whole point: the dashboard both creates
* the session (one-click Start/Resume) and is just one of possibly several
* attached clients tmux already keeps in sync.
*
@@ -33,7 +33,7 @@ try {
/* db-less environment, skip persistence */
}
const RUN_ID_RE = /^ccam-lane-(\d+)$/;
const RUN_ID_RE = /^agenthub-lane-(\d+)$/;
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
const ALLOWED_PERMISSION_MODES = new Set(["acceptEdits", "default", "plan", "bypassPermissions"]);
// Pane commands that mean "idle shell prompt, safe to type a command into".
@@ -45,7 +45,7 @@ const CODEX_APPROVALS = new Set(["on-request", "never"]);
const CODEX_EFFORTS = new Set(["low", "medium", "high", "xhigh"]);
function runIdForLane(laneId) {
return `ccam-lane-${laneId}`;
return `agenthub-lane-${laneId}`;
}
function laneIdFromRunId(id) {
@@ -216,18 +216,18 @@ function spawnRun(args) {
});
};
// Every pane the dashboard starts carries its lane id. A `ccam stage` call
// Every pane the dashboard starts carries its lane id. A `agenthub stage` call
// from inside it then resolves the lane exactly, instead of matching on cwd —
// which is ambiguous now that one directory can back both a `claude` lane and
// a `codex` lane (see bin/ccam.js's resolveLaneArg).
const env = { CCAM_LANE_ID: String(laneId) };
// a `codex` lane (see bin/agenthub.js's resolveLaneArg).
const env = { AGENTHUB_LANE_ID: String(laneId) };
if (!tmux.hasSession(id)) {
tmux.newSession({ name: id, cwd, argv, env });
record();
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
// The session exists but its pane is sitting at a bare shell prompt — a
// `ccam lanes shell`, or a `claude` that already exited. Adopting it
// `agenthub lanes shell`, or a `claude` that already exited. Adopting it
// silently here would swallow the whole request: a Resume would spawn no
// `--resume` and an initial prompt would never be typed, while the API
// still answered 200. Run the argv in the pane the user already sees
@@ -278,7 +278,7 @@ function getRun(id) {
/** Computed fresh from tmux state every call — see file header. */
function listRuns() {
return tmux.listSessions("ccam-lane-").map(publicRun);
return tmux.listSessions("agenthub-lane-").map(publicRun);
}
module.exports = {
Binary file not shown.
+5 -5
View File
@@ -1,9 +1,9 @@
/**
* @file Machine-level lane secrets: the database and Redis connection settings
* shared by every lane on this host. Deliberately NOT part of a repository's
* `.ccam/profile/` — a profile is committed and read by anyone who clones the
* `.agenthub/profile/` — a profile is committed and read by anyone who clones the
* repo, and a database password does not belong there. Lives instead at
* `~/.ccam/secrets.env`, parsed with the same literal `KEY=VALUE` reader
* `~/.agenthub/secrets.env`, parsed with the same literal `KEY=VALUE` reader
* `lane-profile.js` uses for `profile.env` (config is parsed, never sourced).
*
* Never returned by any route: `GET /runtime` may report which keys are
@@ -18,7 +18,7 @@ const path = require("node:path");
const { parseEnvFile } = require("./lane-profile");
const SECRETS_PATH =
process.env.CCAM_SECRETS_PATH || path.join(os.homedir(), ".ccam", "secrets.env");
process.env.AGENTHUB_SECRETS_PATH || path.join(os.homedir(), ".agenthub", "secrets.env");
/** Every declaration a lane's database/Redis facts can rely on when the file
* is absent or unreadable — a local default stack, not a guess. */
@@ -35,12 +35,12 @@ let warnedMissing = false;
let warnedPerms = false;
/**
* Read `~/.ccam/secrets.env`, merged over DEFAULTS.
* Read `~/.agenthub/secrets.env`, merged over DEFAULTS.
*
* Never throws: a missing file warns once and falls back to DEFAULTS (a lane
* with no secrets file still gets a usable local Postgres/Redis target), and a
* file readable by group or world is refused outright rather than trusted —
* loading it would make CCAM the thing that taught a shared machine's other
* loading it would make AgentHub the thing that taught a shared machine's other
* users the database password.
*
* @returns {Record<string,string>}
+5 -5
View File
@@ -1,6 +1,6 @@
/**
* @file skill-plugins.js
* @description The plugins CCAM's pipelines depend on: which ones a template
* @description The plugins AgentHub's pipelines depend on: which ones a template
* names, which of those are actually resolvable, and the narrow set the
* SessionStart bootstrap is allowed to install on the user's behalf.
*
@@ -18,7 +18,7 @@
* It is NOT right for an install. A template is a file anyone can drop into
* that directory, so installing whatever plugin name it happens to contain
* would turn "edit a JSON file" into "run this vendor's plugin install command
* on the next session" — a supply-chain path CCAM must not open. Auto-install
* on the next session" — a supply-chain path AgentHub must not open. Auto-install
* therefore consults {@link AUTO_INSTALLABLE}, a fixed map in this file, and
* ignores every requirement outside it.
*
@@ -53,7 +53,7 @@ const SLASH_COMMAND_RE = /^\/([A-Za-z0-9_-]+):([A-Za-z0-9_-]+)/;
* Every `/plugin:skill` the loaded pipeline templates name — as a pipeline's
* own `skill` or as one node's `command` — grouped by the plugin that has to
* provide it. Derived rather than hardcoded so editing a template cannot leave
* the check behind. `ccam` is dropped: it is the plugin asking.
* the check behind. `agenthub` is dropped: it is the plugin asking.
*
* @returns {Map<string, Set<string>>} plugin name → the skill names it must ship.
*/
@@ -70,7 +70,7 @@ function requiredSkillPlugins() {
const match = SLASH_COMMAND_RE.exec(command || "");
if (!match) continue; // a shell command, or a bare /command
const [, plugin, skill] = match;
if (plugin === "ccam") continue;
if (plugin === "agenthub") continue;
if (!requirements.has(plugin)) requirements.set(plugin, new Set());
requirements.get(plugin).add(skill);
}
@@ -172,7 +172,7 @@ function autoInstallSkillPlugins(log, { timeoutMs = 180_000, run = runClaudePlug
for (const entry of missing) {
if (!entry.autoInstall) {
// Reported by /ccam-doctor, never installed: not on the allowlist.
// Reported by /agenthub-doctor, never installed: not on the allowlist.
summary.skipped.push(entry.name);
continue;
}
+4 -4
View File
@@ -2,11 +2,11 @@
* @file Installs skills/ship-feature-lane/ into ~/.claude/skills/, so
* /ship-feature-lane is discoverable from a session running inside any
* lane's own working directory. This is the FALLBACK path for a plain
* checkout: the skill also ships with the `ccam` plugin (the repo root IS
* checkout: the skill also ships with the `agenthub` plugin (the repo root IS
* the plugin root, so `skills/` is the plugin's skills dir), where it is
* already global as `/ccam:ship-feature-lane`. Installing it here just
* already global as `/agenthub:ship-feature-lane`. Installing it here just
* restores the bare `/ship-feature-lane` name for people who do not run the
* plugin. Shared by bin/ccam.js's `ccam skills install` CLI and the
* plugin. Shared by bin/agenthub.js's `agenthub skills install` CLI and the
* POST /api/skills/install route, so there is exactly one copy of this
* logic. Pure filesystem action.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -18,7 +18,7 @@ const path = require("node:path");
/**
* @param {{repoRoot: string}} options - `repoRoot` is this checkout's own
* root (bin/ccam.js already computes this as `REPO_ROOT`; the server
* root (bin/agenthub.js already computes this as `REPO_ROOT`; the server
* computes its own equivalent — see the route for how).
* @returns {{installed: true, path: string}}
*/
+4 -4
View File
@@ -1,9 +1,9 @@
/**
* @file tmux.js
* @description Thin wrapper around the `tmux` CLI for the terminal-run
* feature. Every dashboard-managed session is named `ccam-lane-<id>` (see
* feature. Every dashboard-managed session is named `agenthub-lane-<id>` (see
* `pty-run.js`) so a real terminal can attach to the exact same session with
* `tmux attach -t ccam-lane-<id>` (or `ccam lanes shell`). Never builds a
* `tmux attach -t agenthub-lane-<id>` (or `agenthub lanes shell`). Never builds a
* shell string — every call is `execFileSync("tmux", [...argv])` with an
* explicit argument array (matches this repo's rule for git in worktree.js).
* The one place a command line is composed is `sendCommand`, which types into
@@ -53,9 +53,9 @@ function envFlags(env) {
* if tmux itself fails to start (caller decides how to surface that).
*
* `env` becomes tmux `-e NAME=value` pairs, which tmux puts in the pane's own
* environment (tmux >= 3.2). The dashboard uses it for CCAM_LANE_ID so a `ccam`
* environment (tmux >= 3.2). The dashboard uses it for AGENTHUB_LANE_ID so a `agenthub`
* call from inside the pane knows exactly which lane it belongs to instead of
* guessing from cwd — see resolveLaneArg in bin/ccam.js.
* guessing from cwd — see resolveLaneArg in bin/agenthub.js.
*/
function newSession({ name, cwd, argv, env }) {
execImpl(["new-session", "-d", "-s", name, "-c", cwd, ...envFlags(env), "--", ...argv]);
+5 -5
View File
@@ -85,7 +85,7 @@ function formatDiscord(alert) {
description: truncate(alert.message, 4000),
color: ACCENT_INT,
fields,
footer: { text: "Claude Code Agent Monitor" },
footer: { text: "AgentHub" },
timestamp: alert.triggered_at,
},
],
@@ -152,7 +152,7 @@ function formatMattermost(alert) {
color: ACCENT_HEX,
text: truncate(alert.message, 3000),
fields: attachmentFields(alert),
footer: "Claude Code Agent Monitor",
footer: "AgentHub",
},
],
};
@@ -196,7 +196,7 @@ function formatPagerDuty(alert, config) {
return {
routing_key: config.routing_key,
event_action: "trigger",
dedup_key: `ccam:${alert.rule_id || "test"}:${alert.session_id || ""}`,
dedup_key: `agenthub:${alert.rule_id || "test"}:${alert.session_id || ""}`,
payload: {
summary: truncate(`${alert.rule_name}: ${alert.message}`, 1024),
source: alert.session_id || "claude-code-agent-monitor",
@@ -219,7 +219,7 @@ function formatPagerDuty(alert, config) {
function formatOpsgenie(alert) {
return {
message: truncate(`${alert.rule_name}: ${alert.message}`, 130),
alias: `ccam:${alert.rule_id || "test"}:${alert.session_id || ""}`,
alias: `agenthub:${alert.rule_id || "test"}:${alert.session_id || ""}`,
description: truncate(alert.message, 15000),
source: "claude-code-agent-monitor",
tags: ["claude-code", alert.rule_type].filter(Boolean),
@@ -238,7 +238,7 @@ function formatOpsgenie(alert) {
function formatSplunkOnCall(alert, config) {
return {
message_type: config.severity || "WARNING",
entity_id: `ccam:${alert.rule_id || "test"}:${alert.session_id || ""}`,
entity_id: `agenthub:${alert.rule_id || "test"}:${alert.session_id || ""}`,
entity_display_name: truncate(alert.rule_name, 256),
state_message: truncate(
`${alert.message}\n\ntype: ${alert.rule_type}${alert.session_id ? `\nsession: ${alert.session_id}` : ""}`,
+1 -1
View File
@@ -287,7 +287,7 @@ function sendTest(target) {
rule_type: "test",
session_id: null,
agent_id: null,
message: `Test notification from Claude Code Agent Monitor to "${target.name}". If you can read this, delivery works.`,
message: `Test notification from AgentHub to "${target.name}". If you can read this, delivery works.`,
details: { test: true, target: target.name, type: target.type },
triggered_at: new Date().toISOString(),
};
+1 -1
View File
@@ -14,7 +14,7 @@ const { promisify } = require("node:util");
const execFileAsync = promisify(execFile);
const LANES_ROOT = process.env.LANES_ROOT || path.join(os.homedir(), ".claude", "ccam-lanes");
const LANES_ROOT = process.env.LANES_ROOT || path.join(os.homedir(), ".claude", "agenthub-lanes");
const PROTECTED_BRANCHES = new Set(["main", "master"]);
/**