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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
@@ -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>}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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}` : ""}`,
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user