Files
Claude-Code-Monitor/docs/superpowers/plans/2026-08-05-integration-toggle.md
T
nntrivi2001 36d66cda07 docs(lanes): plan F3a — ccam lanes integration toggle reader (F)
3 tasks: isIntegrationEnabled in lane-profile.js (reuses parseEnvFile),
the route + CLI, and wiring SKILL.md's hardcoded-off Setup text to the
real check (still no-op behaviorally — no ticketer/dev-qc agent exists
to act on an enabled toggle yet, but the check itself is now honest).
2026-08-05 16:07:33 +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:** `ccam lanes integration <name>` reads a lane's `.ccam/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, ".ccam", "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, ".ccam", "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
* .ccam/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/ccam.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}`. `ccam 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 .ccam/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/ccam.js`, add near `cmdLanesMcpSync`:
```js
/** `ccam lanes integration <name> [<id>]` — check whether a named
* integration (tracker, dev_qc, ci_wait) is turned on for this lane, per
* its .ccam/profile/integrations.env. Exit 0 = on, 1 = off. */
async function cmdLanesIntegration(args) {
const name = args.find((arg) => !arg.startsWith("--"));
if (!name) {
console.error("usage: ccam 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/ccam.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/ccam.js
git commit -m "feat(lanes): add ccam 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 CCAM 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 `ccam 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.** `ccam lanes integration tracker`, `ccam lanes integration dev_qc`, `ccam 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 (`ccam 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 CCAM 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 `ccam 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 `ccam lanes integration tracker` was checked, e.g. append to the existing skip explanation: `(checked \`ccam 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: ccam lanes integration
A profile can declare `.ccam/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
ccam 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 (`ccam 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 (`ccam 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)"
```