Files
AgentHub/docs/superpowers/plans/2026-08-05-integration-toggle.md
T
nntrivi2001 17d56d0e01 refactor!: rebrand CCAM to AgentHub
Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
2026-09-07 12:09:15 +07:00

317 lines
13 KiB
Markdown

# F3a — Integration Toggle Reader Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `agenthub lanes integration <name>` reads a lane's `.agenthub/profile/integrations.env` and reports whether `<NAME>_ENABLED=1` is set, exit 0/1 — so a driving skill can gate a stage on a real check instead of a hardcoded assumption.
**Architecture:** One function in the existing `lane-profile.js` (reusing its already-exported `parseEnvFile`), one lightweight route, one CLI subcommand. No new files beyond a test.
**Tech Stack:** Plain `node:fs`/`node:path`, the existing `parseEnvFile` helper. No git calls, no new dependency.
## Global Constraints
- Every applicable source file (`.js`) MUST start with the project's authorship header — verify with `bash .claude/skills/file-headers/scripts/check-headers.sh`.
- Missing `integrations.env` or missing `<NAME>_ENABLED` key → `false` (off), never an error — same off-by-default shape every other optional declaration in this codebase already follows.
- Do not touch `GET /api/lanes/:id/runtime` — this is a separate, lightweight endpoint (`GET /:id/integrations/:name`), not an addition to the polled/broadcast runtime payload.
- Never use `git add -A`. Stage exactly the files each task names.
- Run `npm run test:server` (full suite) plus `bash .claude/skills/file-headers/scripts/check-headers.sh` before every commit.
---
### Task 1: `isIntegrationEnabled` in `lane-profile.js`
**Files:**
- Modify: `server/lib/lane-profile.js`
- Test: `server/__tests__/lane-profile.test.js`
**Interfaces:**
- Produces: `isIntegrationEnabled(lane, name) => boolean`. `name` is the lowercase form (`"tracker"`, `"dev_qc"`, `"ci_wait"`); the function uppercases it and appends `_ENABLED` to build the env key it looks for (`TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED`).
- [ ] **Step 1: Write the failing tests**
Add to `server/__tests__/lane-profile.test.js` (it already has `makeLane`/`writeProfile` helpers — reuse them):
```js
describe("isIntegrationEnabled", () => {
it("returns false when integrations.env doesn't exist at all", () => {
const lane = makeLane();
writeProfile(lane.cwd, "PORTS=api\n");
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "tracker"), false);
});
it("returns true when <NAME>_ENABLED=1 is set", () => {
const lane = makeLane();
writeProfile(lane.cwd, "PORTS=api\n");
fs.writeFileSync(
path.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
"TRACKER_ENABLED=1\nTRACKER_PROJECT=demo\n"
);
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "tracker"), true);
});
it("returns false when the flag is 0 or absent from an existing file", () => {
const lane = makeLane();
writeProfile(lane.cwd, "PORTS=api\n");
fs.writeFileSync(
path.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
"DEV_QC_ENABLED=0\n"
);
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "dev_qc"), false);
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "ci_wait"), false);
});
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `node --test server/__tests__/lane-profile.test.js`
Expected: FAIL — `profileLib.isIntegrationEnabled is not a function`.
- [ ] **Step 3: Implement**
In `server/lib/lane-profile.js`, add near `resolveProfile` (after it, since it reuses the same two-location search pattern):
```js
/**
* Whether a named integration is turned on for this lane — reads
* .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.
*
* @param {object} lane - Lane row (`cwd`, `source_repo`).
* @param {string} name - Lowercase integration name, e.g. "tracker", "dev_qc", "ci_wait".
* @returns {boolean}
*/
function isIntegrationEnabled(lane, name) {
const candidates = [lane.cwd, lane.source_repo].filter(Boolean);
const key = `${name.toUpperCase()}_ENABLED`;
for (const root of candidates) {
const filePath = path.join(root, PROFILE_SUBDIR, "integrations.env");
if (!fs.existsSync(filePath)) continue;
let declared;
try {
declared = parseEnvFile(fs.readFileSync(filePath, "utf8"));
} catch {
continue;
}
return declared[key] === "1";
}
return false;
}
```
Update `module.exports` (`server/lib/lane-profile.js:422-433`):
```js
module.exports = {
HOOKS,
DEFAULTS,
PROFILE_SUBDIR,
parseEnvFile,
splitList,
parseQcBootEnv,
resolveProfile,
profileSearchPaths,
isIntegrationEnabled,
hookEnv,
runHook,
};
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `node --test server/__tests__/lane-profile.test.js`
Expected: PASS — all 3 new tests, plus every existing test in the file still green.
- [ ] **Step 5: Header check + full suite**
```bash
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
```
- [ ] **Step 6: Commit**
```bash
git add server/lib/lane-profile.js server/__tests__/lane-profile.test.js
git commit -m "feat(lanes): add isIntegrationEnabled toggle check (F3a)"
```
---
### Task 2: Route + CLI
**Files:**
- Modify: `server/routes/lanes.js`
- Modify: `bin/agenthub.js`
**Interfaces:**
- Consumes: `isIntegrationEnabled(lane, name) => boolean` from Task 1 (`require("../lib/lane-profile")` — already imported in `server/routes/lanes.js` for `HOOKS`/`runHook`/`resolveProfile`, just add `isIntegrationEnabled` to that existing destructure).
- Produces: `GET /api/lanes/:id/integrations/:name` → `{enabled: boolean}`. `agenthub lanes integration <name> [<id>]` → prints `on`/`off`, `process.exitCode` 0 when enabled, 1 when disabled.
- [ ] **Step 1: Add the route**
In `server/routes/lanes.js`, extend the existing import (`server/routes/lanes.js:35`):
```js
const { HOOKS, runHook, resolveProfile, isIntegrationEnabled } = require("../lib/lane-profile");
```
Add the route directly after the `/:id/mcp/sync` route (added in F1, search `router.post("/:id/mcp/sync"` to find it):
```js
/**
* Whether a named integration (tracker, dev_qc, ci_wait, ...) is turned on
* for this lane — reads .agenthub/profile/integrations.env. Read-only, no
* sameOriginGuard needed (same reasoning GET /:id/git already documents:
* that guard exists for destructive actions).
*/
router.get("/:id/integrations/:name", (req, res) => {
const lane = lanesLib.getLane(req.params.id);
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
res.json({ enabled: isIntegrationEnabled(lane, req.params.name) });
});
```
- [ ] **Step 2: Add the CLI subcommand**
In `bin/agenthub.js`, add near `cmdLanesMcpSync`:
```js
/** `agenthub lanes integration <name> [<id>]` — check whether a named
* integration (tracker, dev_qc, ci_wait) is turned on for this lane, per
* its .agenthub/profile/integrations.env. Exit 0 = on, 1 = off. */
async function cmdLanesIntegration(args) {
const name = args.find((arg) => !arg.startsWith("--"));
if (!name) {
console.error("usage: agenthub lanes integration <name> [<id>]");
process.exitCode = 1;
return;
}
const resolved = await resolveLaneArg(args.filter((a) => a !== name));
if (!resolved) return;
const { enabled } = await get(`/api/lanes/${resolved.laneId}/integrations/${encodeURIComponent(name)}`);
console.log(enabled ? "on" : "off");
process.exitCode = enabled ? 0 : 1;
}
```
Wire the dispatcher — in the `lanes` case, right after the `mcp sync` check:
```js
if (rest[0] === "mcp" && rest[1] === "sync") {
return cmdLanesMcpSync(rest.slice(2));
}
```
Add directly below it:
```js
if (rest[0] === "integration") {
return cmdLanesIntegration(rest.slice(1));
}
```
Add the help-catalog entry right after `lanes mcp sync`:
```js
[
"lanes integration",
"<name> [<id>]",
"Check whether a named integration (tracker, dev_qc, ci_wait) is on for this lane — exit 0/1",
],
```
- [ ] **Step 3: Manual smoke check**
```bash
npm run dev &
sleep 3
curl -s http://localhost:4820/api/lanes/1/integrations/tracker
echo
node bin/agenthub.js lanes integration tracker 1
echo "exit: $?"
```
Expected: `{"enabled":false}` (or `true` if lane 1's profile happens to declare it) and matching CLI output/exit code. Stop the dev server afterward.
- [ ] **Step 4: Run the full suite + header check**
```bash
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
```
- [ ] **Step 5: Commit**
```bash
git add server/routes/lanes.js bin/agenthub.js
git commit -m "feat(lanes): add agenthub lanes integration CLI + route (F3a)"
```
---
### Task 3: Wire `SKILL.md`'s hardcoded-off checks to the real primitive
**Files:**
- Modify: `.claude/skills/ship-feature-lane/SKILL.md`
- Modify: `docs/LANES.md`
- Modify: `docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`
**Interfaces:** none — documentation, but now backed by a real command.
- [ ] **Step 1: `SKILL.md` Setup — replace the hardcoded-off line**
Find:
```
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in AgentHub yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `agenthub ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
```
Replace with:
```
- **Integration toggles: check, don't assume.** `agenthub lanes integration tracker`, `agenthub lanes integration dev_qc`, `agenthub lanes integration ci_wait` each exit 0 (on) or 1 (off), reading the profile's `integrations.env`. No agent exists yet to actually FILE a ticket or run dev-QC even when a toggle reads on (`ticketer`/`dev-qc` are a later task) — so regardless of the check's result, Stage 9 (ticket) stays skipped, Stage 13's dev-QC and dev-CI-wait halves stay skipped, and Stage 10's CI watch keeps using the plain `gh pr checks` path (`agenthub ci` doesn't exist yet). Check the toggle where noted below anyway, so the evidence you record is honest about whether the PROFILE wants the integration on, distinct from whether AgentHub can act on it yet.
```
- [ ] **Step 2: Stage 9 (ticket) — record the toggle in evidence**
Find the Stage 9 section (search `grep -n "^### 9" .claude/skills/ship-feature-lane/SKILL.md`) and its skip line (likely something like `agenthub stage reported --status ... ` or similar noting the skip — read the current text first with `sed -n '/^### 9/,/^### 10/p' .claude/skills/ship-feature-lane/SKILL.md` to get its exact current wording before editing, since this section's precise text was written in E1 and may have shifted slightly across later edits). Add one line noting `agenthub lanes integration tracker` was checked, e.g. append to the existing skip explanation: `(checked \`agenthub lanes integration tracker\` — even when on, no ticketer agent exists yet to act on it)`.
- [ ] **Step 3: `docs/LANES.md` — add a short subsection**
Under `## The ship-feature-lane skill (E1)`, after the `### Syncing MCP servers: mcp sync` subsection (added in F1), insert:
```markdown
### Checking integration toggles: agenthub lanes integration
A profile can declare `.agenthub/profile/integrations.env` with `TRACKER_ENABLED`, `DEV_QC_ENABLED`, `CI_WAIT_ENABLED` flags (all off by default — see `profiles/_template/integrations.env`-style declarations in a profile's own docs). Check one:
```bash
agenthub lanes integration tracker # exit 0 = on, 1 = off
```
This only reports what the PROFILE wants — it doesn't file a ticket, run dev-QC, or wait on CI. The agents that would act on an enabled toggle (`ticketer`, `dev-qc`) aren't built yet; `ci_wait`'s consumer (`agenthub ci`) isn't either. `ship-feature-lane`'s Stage 9/13/10 stay skipped/fallback regardless of what the toggle reads, until those land.
```
- [ ] **Step 4: Roadmap progress line**
In `docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, find the `## F` section's `**Progress:**` line (added in F1) and append:
```
`integration` toggle reader (F3a) done 2026-08-05 — see `docs/superpowers/specs/2026-08-05-integration-toggle-design.md`. `ticketer`/`dev-qc` agents, CI-wait (`agenthub ci`), and dev-QC remain.
```
- [ ] **Step 5: Verify and commit**
```bash
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
```
```bash
git add .claude/skills/ship-feature-lane/SKILL.md docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
git commit -m "docs(lanes): wire ship-feature-lane to the real integration toggle check (F3a)"
```