Files
AgentHub/CLAUDE.md
T
nntrivi2001 1aee080fca fix(cc-config): read the live account's quota from the CLI, and ask for every account at once
The quota check failed with `invalid_grant` for the account the user was
logged into - the one profile they would try first. An OAuth refresh token is
single use, so every refresh Claude Code performs on its own account retires
the copy sitting in that account's saved profile: the stored credentials of
the LIVE account are the one set guaranteed to be rejected, however recently
the profile was saved. Confirmed on a real install - the saved profile held a
refresh token three rotations behind `.credentials.json`.

`cc-profiles.liveCredentialsFor` now matches a profile's accountUuid against
the live credentials and, on a hit, the usage call uses what the CLI is
holding. That token is never refreshed and that file is never written:
rotating it would log the user out of their own terminal. An expired live
token answers ELIVEEXPIRED - using Claude Code once makes the CLI refresh it -
and a genuinely spent refresh token is now reported as ESTALE with what to do
about it, instead of echoing Anthropic's raw 400.

Opening the Profiles modal also asks for every saved account at once, rather
than one button per row: comparing accounts is the only reason the panel
exists. The guard against rewriting credentials over and over is no longer
"one row at a time" but the rules above - the live account costs a plain read,
and any other profile refreshes only once its token has actually expired,
which then leaves it valid for hours of further opens. Each row resolves on
its own, so a superseded account still lets its neighbours report.
2026-09-17 23:17:06 +07:00

28 KiB

Claude Code Working Guide

Last synced: v1.6.10 (2026-09-17) + per-profile Claude quota (server/lib/cc-usage.js, POST /api/cc-config/profiles/usage, the Check quota button in the Profiles modal) — a POST because a saved profile's token is normally expired, so answering refreshes it and the refresh ROTATES the credentials written back to disk, which is also why it never runs on a poll; the LIVE account is the exception and is read from .credentials.json via cc-profiles.liveCredentialsFor, never refreshed, because the CLI has already retired the refresh token its profile copy holds + the settings patch that changes nothing now writing nothing (patchSettings in server/lib/cc-mutate.js, plus .claude/.cc-config-backups/ in a lane's git exclude) + the Add-lane modal closing itself on a clean setup and offering a real Close when a step failed + a lane's router pick no longer landing on whichever lane the user switched to mid-apply (LaneRouterRow), nor reverting on the next lane switch — POST /api/lanes/:id/router/apply now broadcastLanes like every other mutating lane route, so the client's cached row stops disagreeing with what was written + a run being opened by a DECLARATION only — a backwards DETECTION now continues the run in flight (appendStageEvent in server/lib/lanes.js), because detection bounces within one feature and runs is what every pass rate on the Workflows page is divided by + SETTLE_MS in server/lib/cc-reload.js — the two reload commands are typed a second apart, and reloadOpenSessions is now async, because Claude Code swallows the Enter after a slash command and back-to-back sends merged into one composer line + v1.6.0 (2026-09-14) + the lane strip's drag/Alt-arrow reordering and inline rename (client/src/lib/laneOrderStorage.ts, LaneStripCard's index/onReorder props, the Workspace detail header's rename field, and normalizeTitle/bindable in server/lib/lanes.js) + the uncommitted-files hover panel (LaneGitFilesPanel in LaneCard, and files from GET /api/lanes/:id/git now being {path,status,added,deleted} records off one extra git diff --numstat HEAD) + AI Router profiles (server/lib/ai-routers.js, server/lib/router-apply.js, the Settings tab /settings?tool=ai-router, its Test button / POST /api/ai-routers/test, the per-lane router select — labelled Model, LaneRouterRow exported from LaneCard and rendered in the Workspace lane-detail header row — and lanes.router_profile) + the bootstrap's superseded-build guard and plugin-cache pruner (supersededBy/pruneRetiredPluginRoots in scripts/plugin-bootstrap.js, unitRoot in scripts/systemd-service.js) + v1.5.6 (2026-09-11) + the systemd handover rule below (scripts/systemd-service.js's writeUserService/restartUserService split and plugin-bootstrap.js's deferred pendingSystemdRestart) + eb63569 (2026-09-10) + release-based update detection (server/lib/update-check.js reads the forge releases API — a checkout via its origin remote, a plugin-cache install via the marketplace manifest on disk, since an archive-sourced cache dir has no .git; a plugin_managed install self-updates via the detached plugin-update chain) + the install-wide Update all batch and session reload (server/lib/cc-update-all.js, server/lib/cc-reload.js). A change that makes any statement in this file, AGENTS.md, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker.

Project mission

  • Maintain a reliable local-first dashboard for Claude Code session monitoring.
  • Preserve real-time behavior (hooks -> API -> SQLite -> WebSocket -> UI).

Repo map

  • server/: Express API, hook ingestion, SQLite access, websocket broadcast (includes release-based update detection — lib/update-check.js reads the repo's own releases API and routes/updates.js exposes status/check/apply, refusing to self-update a ~/.claude/plugins/cache/ install, plus lib/workflow-ingest.js which ingests on-disk Workflow-tool run journals — fleets that emit no hooks — and lib/codex-ingest.js, the same idea for codex lanes: it tails Codex's own rollout journals because Codex emits no hooks at all).
  • client/: React + Vite UI.
  • scripts/: hook installer/handler, import, seed, cleanup utilities. (Update detection lives server-side in server/lib/update-check.js, keyed on the forge's newest release rather than a commit count. POST /api/updates/apply has two modes and picks by install kind, never from the request: a fast-forwardable checkout merges the release tag --ff-only, rebuilds, and self-restarts via server/lib/self-restart.js + the detached scripts/restart-helper.js; a plugin-cache install is handed to server/lib/plugin-update.js + the detached scripts/plugin-update-helper.js, which runs claude plugin marketplace update → claude plugin update -y → plugin-bootstrap.js --force from the NEW cache dir. Both write the HTTP response before anything stops the process, and every plugin-chain outcome is recorded to plugin-update.json in the runtime dir — outside the cache, so it survives the swap and the next server reports it as last_plugin_update. Neither mode can run /reload-plugins: it is REPL-scoped to the session that invokes it, so an open Claude Code session still reloads by hand for new skills/commands. Elsewhere users run the printed manual command, surfaced in the UI and by agenthub update-check. scripts/cc-update-all-helper.js is the same detached pattern for the config panel's Update all button — marketplaces → every plugin → AgentHub LAST → one reload — journalling each step to cc-update-all.json in the runtime dir.)
  • skills/: this repo's skills. Because the repo root is the agenthub plugin root, this is also the plugin's skills directory — every skill here ships to anyone who installs the plugin, addressed as /agenthub:<name> (bare /<name> works as an alias). Put a skill here only if it should reach plugin users; agenthub skills install remains a fallback that copies ship-feature-lane into ~/.claude/skills/ for plain checkouts. - agents/: the same story for subagents — plugin root, so all four (qc-local, senior-gate-reviewer, backend-reviewer, frontend-reviewer) ship as agenthub:<name>. This is also the source agenthub lanes agents install copies the two pipeline agents from into a lane's .claude/agents/, which is what keeps the bare subagent_type: qc-local working there. One file per agent, never a second copy.
  • .claude-plugin/: the marketplace plus the root agenthub plugin manifest ("source": "./" — the whole repo is the plugin). Its hooks are inline in plugin.json; its commands live in plugins/agenthub/commands/, which is NOT a subdirectory plugin. scripts/plugin-bootstrap.js runs from SessionStart and owns the writable runtime under ~/.claude/agenthub/runtime/ — it never writes into the plugin cache, which Claude Code replaces on every update. See docs/PLUGINS.md.
  • Reporting a required plugin is derived; installing one is allowlisted. server/lib/skill-plugins.js reads the /plugin:skill strings out of the loaded pipeline templates — right for the /agenthub-doctor row, wrong for an install: a template is a file anyone can drop into DASHBOARD_PIPELINES_DIR, so installing whatever name it contains would make "edit a JSON file" mean "run this vendor's install command next session". The bootstrap installs only what AUTO_INSTALLABLE (a constant in that file, superpowers today) lists, in its DETACHED worker, never in the 10 s hook, recorded once as state.skillPluginsSettled (also gating fastPathOk), never throwing, and off under AGENTHUB_AUTO_INSTALL_PLUGINS=0|false|off. Do not derive an install target from a template.

Lanes

A lane is a durable unit of parallel agent work — one working directory, many Claude Code sessions over time. Lanes are keyed by cwd, not session_id, so they survive session restarts and allow the dashboard to show a pipeline that persists across runs.

Key points:

  • Sessions are bound to lanes by longest path-boundary prefix match on their cwd (set in hook data from claude CLI).
  • A lane's stage is declared by the driving Claude session (via agenthub stage <stage> [--evidence "..."]), not inferred by the dashboard.
  • A CLI command resolves its lane by (cwd, tool), never cwd alone. resolveLaneArg (bin/agenthub.js) answers in order: --lane/leading id → AGENTHUB_LANE_ID (every tmux pane the dashboard starts carries it, via tmux new-session -e) → --tool, else the harness marker (CLAUDECODE ⇒ claude; Codex has NO reliable env marker, so it passes --tool codex) → the owning lane. Two candidates at one directory and no signal is an ERROR that names both, never a pick — writing a codex session's stage onto the claude lane's card is a lie about which agent did the work.
  • The console never writes a lane's stage. Declared stages come from agenthub stage (a skill calling the CLI command), and inferred stages come from tool-event detection only. One exception, codex lanes only: codex-ingest.js advances stage from its own written detection (setStage(..., { keepDetection: true })) — a codex session has no hook and rarely calls agenthub stage, so nothing else would ever move its map. It passes NO evidence, so the node renders passed-no-evidence (amber) and never done, and keepDetection preserves the detection columns so the card still shows the command that moved it. Do not extend this to claude lanes or to the console. The UI's run console and the Workspace page never call POST /api/lanes/:id/stage — that boundary exists because the console observes tool calls but cannot see their outcomes. A lane's stage is the session's truth about progress; the console's observation of tool execution would not be proof of completion.
  • Going back is a declaration, and it takes the nodes ahead with it. A declaration landing on a node BEFORE the lane's current one (a fix-loop re-entry, or a new request on a lane that already reached done) clears the detection columns like every real transition AND drops the stages records of every node ahead of it — those belong to the previous pass, and left in place they render done with the old work's evidence beside a current ring further back, contradicting progress (computed from the declared index alone). Records under a key matching no node are left alone: unplaceable, so unprovably ahead. agenthub feature activate <slug> is the other half of this — a feature switch archives the outgoing pipeline and clears detection too, because a detection belongs to the feature that produced it.
  • A detection expires, an evidence rule does not. recordDetection skips its forward-only comparison once detected_at is older than DETECTION_TTL_MS (default 5 min), so a lane can move backwards between work sessions. That window changes only WHICH detection is current — it never relaxes declared-wins (an agent's own claim has no expiry) and never lets an inferred node render done. Declared-wins has exactly ONE exception: a declaration on the pipeline's LAST node (done) stops vetoing, because the pipeline is over there and a later detection is new work — without it a lane left at done rejects every detection for the rest of its life.
  • Working-copy facts live at GET /api/lanes/:id/git, never inside GET /api/lanes. That endpoint shells out to git five times; the lane list is polled and re-broadcast on every hook. A cwd that is not a readable repo returns {available:false} with HTTP 200 — a normal state, not a fault. Cards fetch it themselves every 30s and fail silently.
  • The Workspace console collapses with CSS, never by unmounting — and it has exactly ONE render site. Unmounting RunConsole disposes the run subscription and drops a live run's rendered history. The same rule binds one level up: ConsoleArea lives inside the always-rendered lane-detail section in client/src/pages/Workspace.tsx, never inside a {currentLane && …} branch. A second render site makes React unmount the whole console every time currentLane flips — lanes arriving on mount, the selected lane removed, a refetch re-picking the selection — which kills the terminal and lets LaneConsolePane's lane-switch effect wipe a half-typed prompt. Header and info block are the conditional parts; the console is not.
  • AgentHub does not orchestrate: no chaining, no queue, no retry logic, no gate evaluation. The session in control makes all decisions; the dashboard records the claimed stage and shows evidence. The runtime layer (agenthub lanes up|down|hook) does not change this — it offers primitives a session calls; nothing in the dashboard sequences them.
  • The runtime never writes stage, status or notes — only slot and ports. status=running means an AGENT is working, not that a server is listening; merging the two would corrupt classifyLiveness. Boot failures live in LANES_ROOT/.state/lane<slot>/last-error.json and surface via GET /api/lanes/:id/runtime. Same boundary as the console-never-writes-stage rule above.
  • A lane's stack is up or down as a computed fact, never a stored one. runtimeFacts re-derives it from pid files and port probes on every read. A process dies to OOM, a stray kill, a reboot — caching a truth AgentHub does not control buys ghost state. Slot allocation is the opposite (fully controlled, must be race-free), so that one does live in the DB, under withLaneLock plus a partial unique index.
  • A profile's profile.env is parsed, never sourced. Hooks are executed deliberately; config is only read. Sourcing arbitrary shell from a user's repository into the dashboard process would be a code-execution path. Hook names always come from the fixed allowlist in server/lib/lane-profile.js, never from a request.
  • Pipeline templates are JSON files (server/data/pipelines/) with node definitions; custom templates override built-ins when DASHBOARD_PIPELINES_DIR is set.
  • Nodes render in five states: failed (rejected), current (now), done (with evidence), passed-no-evidence (claimed or skipped, amber), pending (not reached).
  • Liveness: a silent watcher (stage matching /watch|poll/) is dead after LANE_DEAD_SEC seconds (default 300); a silent idle lane is at rest, not dead.
  • Never rm -rf a lane. Destructive lane operations (reset, remove, purge) go through server/lib/worktree.js/server/lib/lanes.js, never a raw filesystem delete — that's what keeps the three-check destroy guard and the DB bookkeeping in lockstep.
  • Never build a git command as a shell string. Lane git operations use execFile("git", [...args]) with an explicit argument array (see server/lib/worktree.js's git() helper), never a concatenated/interpolated string passed to a shell.
  • A lane's AI Router profile is a lane fact too, and it never travels alone. lanes.router_profile (nullable; null means the same as native) is written ONLY by setRouterProfile, called beside the disk write in POST /api/lanes/:id/router/apply — never through PATCHABLE. The column and the lane's own config must move together, or the row claims a router the lane's settings.local.json has never heard of. What is written comes from the lane row, never the request: a claude lane gets eight env keys merged into <cwd>/.claude/settings.local.json (.local because the value carries an API key; every other key in that file is preserved and a non-JSON file is refused, not rewritten), a codex lane gets $CODEX_HOME/<name>.config.toml — the layer codex -p <name> loads — because Codex has NO project-level config, and its base config.toml is never touched. Key material never leaves the server: profiles live at <CLAUDE_HOME>/agenthub-routers/ (0700/0600), GET /api/ai-routers reports only hasKey, and there is deliberately no route that returns one profile in full. POST /api/ai-routers/test sends a stored key ONLY to the endpoint it was stored against — a blank apiKey falls back to the profile named by name, but only while the submitted baseUrl still equals the saved one, else EBADKEY. Relax that and the route becomes a way to make the server POST a saved key to any URL a browser names. Re-pointing a lane is allowed and is not a PATCH: the lane card's AI Router select calls POST /api/lanes/:id/router/apply (column + disk together, same rule as above), never PATCH /api/lanes/:id. Codex's key reaches the pane through tmux's -e argv and is never typed into it — tmux.sendCommand takes over an idle pane with respawn-pane for exactly this reason; a KEY='sk-…' codex … line typed with send-keys would be rendered to every attached browser, kept in the scrollback and appended to the user's shell history. Do not reintroduce a typed command line there. And attaching COPIES a profile, so the store stops being the single source of truth the moment a lane exists: PATCH/DELETE /api/ai-routers/:name must push through to the lanes naming it (reapplyToLanes/detachFromLanes), or a rotated key silently never reaches the lane still using the revoked one. See server/lib/ai-routers.js, server/lib/router-apply.js.
  • A lane's tool is a lane fact, immutable, and never read from a request. lanes.tool (claude | codex, default claude) is set by createLane only — it is deliberately absent from PATCHABLE, so no PATCH can flip the CLI under a live pane. Every spawn path (POST /api/lanes/:id/start, POST /api/run) takes the tool from the lane row and ignores whatever the body says; the run panel's Codex-only fields (sandbox, approval, resumeLast) are validated against the fixed allowlists in server/lib/pty-run.js before they reach an argv, and the bypass flags (--dangerously-bypass-approvals-and-sandbox, --dangerously-bypass-hook-trust) are never emitted. GET /api/run/binary?tool= resolves the probed binary name from that same allowlist — the query string reaches which.
  • A lane's card position is a viewer preference; its name is a lane fact. The Workspace strip's drag/Alt-arrow order lives in localStorage (agenthub.workspace.laneOrder, client/src/lib/laneOrderStorage.ts) and writes NOTHING server-side — GET /api/lanes and agenthub lanes stay in id order, so no sort_order column, no reorder route, no websocket message. Renaming is the opposite: it is PATCH /api/lanes/:id with {title}, the one lane column a user now types into directly, so updateLane normalizes it (normalizeTitle: trim, 200-char cap, null ⇒ "", because clearing is legitimate — every render is lane.title || lane.cwd). The rule is the column-wide one, not a special case for the field the UI exposes: bindable rejects any non-scalar patch value with EBADFIELD (links excepted — it is stored as JSON), and the route answers 400 for that AND for the SQLITE_CONSTRAINT family, rather than handing better-sqlite3 a datatype throw and reporting 500 on plain bad input.
  • Adopted lanes are not destroyable. A lane with kind === "adopted" may never have its worktree reset or removed; "removing" one only drops the dashboard's own record of it. This is enforced in code (assertDestroyable's first check) — do not add a path that bypasses it.
  • A cwd can back at most one lane per tool, not one lane overall. Uniqueness is the composite idx_lanes_cwd_tool (cwd, tool), not cwd alone — Repo/Adopt mode (POST /api/lanes/ensure) lets the same directory carry a claude lane and a codex lane side by side, shown as two separate cards. resolveLaneByCwd(cwd, tool) takes an optional tool filter for exactly this; hook ingestion (touchLaneFromHook, resolveLaneForWork) always pins it to "claude" because Codex has no hook mechanism in this codebase, so a hook event must never touch a codex lane sharing the path. Worktree mode is unaffected — it already allocates a unique directory per lane.

See docs/LANES.md for full guide: stage reporting, custom templates, lane actions, and the five node states.

Non-negotiable engineering rules

  • When a agenthub.service unit of ours owns the server, nothing else may stop it. The bootstrap that performs a self-update is spawned by the running server, so it lives in that server's systemd cgroup: SIGTERM-ing the server from there exits 0 (which Restart=on-failure read as "meant to stop") AND trips KillMode=control-group, reaping the bootstrap before it can repoint the unit or write state.json — a dashboard dead after every update until some later session bootstrapped again. So scripts/systemd-service.js splits the API: writeUserService repoints the unit and starts NOTHING, restartUserService uses --no-block, and plugin-bootstrap.js defers that restart past writeState and releaseLock (pendingSystemdRestart). The unit is Restart=always. Do not add a path that stops the dashboard from inside its own cgroup, and do not move the restart earlier. agenthub service install may keep using installUserService — it runs from a user's shell, outside the cgroup. systemdOwned additionally requires the unit to be ACTIVE (an installed-but-stopped unit owns nothing; treating it as owner skips the stop and then binds an occupied port). A unit naming a RETIRED plugin root is invisible until a reboot, so fastPathOk rejects it via unitIsStale (pure fs — that path may never spawn a process) and the bootstrap rewrites it; repairing the unit is NOT restarting the server (pendingSystemdRestart = systemdOwned && needsServer), and the detached-spawn fallback is guarded by needsServer so a stale unit alone never starts a second server onto a bound port.
  • A bootstrap that is no longer the installed build does NOTHING, and --force does not override that. supersededBy (scripts/plugin-bootstrap.js) reads Claude Code's installed_plugins.json: if a SIBLING cache directory is the recorded install, this build logs one line and returns superseded — no deps, no unit, no server. A Claude Code process that was not restarted after an update keeps loading the OLD cache dir, and without this its every session repointed agenthub.service at that old dir and restarted the dashboard onto the version the user just upgraded away from, once per session, forever. It may not hand over to the new build either — the process it would replace is the one loading it. Do not add a path that lets a retired root write the unit, stop the server, or "just" refresh deps.
  • Deleting retired cache copies is the ONE thing here that touches the plugin cache, and four protections gate it. pruneRetiredPluginRoots never deletes: this build; any installPath in installed_plugins.json; the dir agenthub.service names (unitRoot, foreign unit included); any dir named in the user's settings.json hooks (a un-restarted Claude Code is still executing from there — deleting it breaks that session's hooks). Plus the 2 newest survivors as a rollback cushion, and nothing at all outside ~/.claude/plugins/cache/. It runs AFTER writeState, so a bad prune cannot also cost the record of a good bootstrap. Adding a fifth thing that pins a cache dir means adding a fifth protection here.
  • A session reload is typed into a live pane, never spawned. /reload-plugins and /reload-skills are scoped BY CLAUDE CODE to the session that runs them, so nothing can reload a session from the outside. server/lib/cc-reload.js therefore types them into the dashboard's existing tmux lane panes and spawns NO claude process — a one-shot claude -p "/reload-plugins" reloads only its own short-lived session, exits, and leaves a junk session in the dashboard's own history. Do not add a path that starts a session in order to reload one.
  • Preserve existing behavior unless explicitly asked to change it.
  • Prefer minimal, reversible diffs.
  • Never silently weaken safety controls around destructive actions.
  • Keep docs updated when behavior, commands, file locations, or workflows change — apply the update-project-docs skill automatically at the end of every change-set that alters behavior, config, interfaces, events, schema, CLI commands, or features (do not wait to be asked).
  • Every applicable source file you create or update (.js/.ts/.tsx/.cjs/.mjs/.py/.sh/.css) must start with the copyright/authorship header — file overview + the exact line @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>. Formats and audit script: skills/file-headers/ (verify with bash skills/file-headers/scripts/check-headers.sh). This binds every coding agent (Claude Code, Codex, or others).

Commands you should know

  • Setup: npm run setup
  • Dev: npm run dev
  • Prod build/start: npm run build then npm start
  • Server tests: npm run test:server
  • Client tests: npm run test:client
  • CLI (after setup): agenthub <command> — terminal access to the full dashboard surface (bin/agenthub.js; agenthub help lists commands)

Testing and verification policy

  • Backend changes: run npm run test:server before finishing.
  • Frontend changes: run npm run test:client when relevant. This includes per-screen render snapshots (client/src/pages/__tests__/screens.snapshot.test.tsx). If a UI change is intentional, review the snapshot diff and regenerate baselines with cd client && npx vitest run -u; never blindly update snapshots to make tests pass.
  • If you cannot run a verification step, state exactly what was not run and why.

Change guidelines by area

  • API routes: preserve response shapes unless change is requested and documented.
  • Database: avoid schema changes without migration-safe logic.
  • Hooks: keep fail-safe and non-blocking behavior.
  • WebSocket: keep message types stable and backward-compatible.
  • Documentation: include exact commands and paths; keep markdown examples runnable.

Agent behavior

  • Explore first, then implement.
  • For larger tasks, propose/check a short plan before broad edits.
  • Use file-specific rules in .claude/rules/ when working in scoped areas.
  • Use project skills from skills/ for repeatable workflows (they ship with the agenthub plugin: /agenthub:<name>, or the bare /<name> alias).
  • Use agents/ subagents for focused review or investigation passes (they ship with the plugin: agenthub:backend-reviewer, etc.).
  • Declare lane stage even in plain chat, not just inside skills. agenthub stage (see docs/LANES.md § Reporting a stage) is a reporting command, not a skill-only ritual — any Claude session working inside an adopted lane's cwd should call it on real stage transitions (starting to plan, starting to implement, running tests, opening the PR, etc.), whether or not a skill is driving. Tool-event detection (server/lib/stage-detect.js) only ever paints the amber "detected" badge, never the blue current ring — a lane worked entirely through plain chat with no agenthub stage calls will show a stale current stage no matter how much real work happens. Skip it only when agenthub stage reports no lane owns the cwd (not adopted).
  • Prefer agenthub:ship-feature(-lane) over invoking superpowers:brainstorming/writing-plans/executing-plans/subagent-driven-development/test-driven-development directly for implementation work inside a lane's cwd. Those Superpowers skills know nothing about AgentHub lanes and never call agenthub stage themselves — going straight to them (skipping the AgentHub wrapper) is exactly how a lane's current stage goes stale mid-implementation while agenthub stage is never called for real transitions. agenthub:ship-feature/ship-feature-lane drive the same underlying Superpowers skills phase-by-phase and declare agenthub stage at every boundary — pick one of them first for any multi-phase feature/bugfix inside a lane; reach for a bare Superpowers skill directly only for work with no lane cwd to report against (e.g. a scratch repo, or explicitly told to skip the AgentHub wrapper).