Files
Claude-Code-Monitor/docs/superpowers/plans/2026-08-06-add-lane-auto-setup.md
T
nntrivi2001 64c00dea85 docs(lanes): plan F5 — auto-setup after Add Lane (E)
3 tasks: POST /:id/profile/init route (wraps already-built
lane-detect.js), AddLaneModal.tsx wiring (Promise.allSettled over
profile-init + agents-install + mcp-sync, none blocking lane creation
or each other) + client API method + tests, and docs.
2026-08-06 11:35:36 +07:00

15 KiB

F5 — Auto-setup after Add Lane 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: After AddLaneModal creates a worktree lane, automatically fire profile-init, agents-install, and mcp-sync — one click instead of four.

Architecture: One new backend route (POST /:id/profile/init, wrapping already-built lane-detect.js), one new client API method, one modification to AddLaneModal.tsx's submit handler (three parallel best-effort calls after worktree() succeeds), plus a client test.

Tech Stack: Express route (existing pattern), React (existing pattern), Promise.allSettled.

Global Constraints

  • Every applicable .js/.tsx source file MUST start with the project's authorship header — verify with bash .claude/skills/file-headers/scripts/check-headers.sh.
  • None of the three setup calls may block lane creation, or block each other. Promise.allSettled, not sequential awaits or Promise.all (which would short-circuit on the first rejection). A lane exists the moment worktree() returns — setup failing is informational, never a reason to not show the lane.
  • POST /:id/profile/init matches its two sibling routes' exact pattern (/:id/agents/install, /:id/mcp/sync, both in server/routes/lanes.js): inline lanesLib.getLane(req.params.id) + 404 check, NOT the laneOr404 helper used elsewhere in the file — match the immediate neighbors, not a different convention from further away in the same file.
  • 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 backend-touching commit; run npm run test:client (or cd client && npm test — NEVER a bare npx vitest run, which skips the required NODE_OPTIONS=--no-experimental-webstorage and produces spurious failures) before every frontend-touching commit.
  • Kill any stray npm run dev/node --watch server/index.js/vite process you start for a manual check before finishing your task — verify with ps aux | grep -E "server/index.js|client/node_modules/.bin/vite".
  • If the backend suite fails with EPORTBUSY in lane-runtime.test.js, that's stray leftover python3 -m http.server processes on ports 19000-20999 from unrelated past work — kill them (ss -ltnp to find), then retry. Not your bug.
  • Never bypass the pre-commit hook with --no-verify. If it fails, find and fix the real cause.

Task 1: POST /api/lanes/:id/profile/init

Files:

  • Modify: server/routes/lanes.js

Interfaces:

  • Consumes: detectNode(repoPath) => object|null, scaffoldProfile(repoPath, facts, {force}) => {written: string[], todos: string[]} (throws .code === "EPROFILEEXISTS" if a profile exists and force isn't set) — both from require("../lib/lane-detect"), already built.

  • Produces: POST /api/lanes/:id/profile/init — body {force?: boolean}. 200 with {scaffolded: true, written: string[], todos: string[]} on success, 200 with {scaffolded: false, reason: string} when no Node.js project is detected (not an error — matches the CLI's own framing), 400 with {error: {code: "EPROFILEEXISTS", message}}, 404 for an unknown lane.

  • Step 1: Add the import

In server/routes/lanes.js, add near the other lib requires (after the lane-gc import added in F4):

const { detectNode, scaffoldProfile } = require("../lib/lane-detect");
  • Step 2: Add the route

Insert directly after the /:id/mcp/sync route closes (server/routes/lanes.js, search router.post("/:id/mcp/sync" — insert right after its closing });):

/**
 * Detect a Node.js project at this lane's OWN directory and scaffold
 * .ccam/profile/ if one is found — the HTTP equivalent of
 * `ccam lanes profile init`, always targeting lane.cwd (never an arbitrary
 * path; the CLI's <repo> argument has no meaning here, this lane's own
 * directory is the only sensible target). "No Node.js project detected" is
 * a normal 200 outcome, not an error — most lanes won't be auto-detectable
 * and that's fine, same as every other optional profile declaration.
 */
router.post("/:id/profile/init", sameOriginGuard, (req, res) => {
  const lane = lanesLib.getLane(req.params.id);
  if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });

  const facts = detectNode(lane.cwd);
  if (!facts) {
    return res.json({ scaffolded: false, reason: "no detectable Node.js project" });
  }
  try {
    const result = scaffoldProfile(lane.cwd, facts, { force: req.body?.force === true });
    res.json({ scaffolded: true, written: result.written, todos: result.todos });
  } catch (err) {
    if (err.code === "EPROFILEEXISTS") {
      return res.status(400).json({ error: { code: err.code, message: err.message } });
    }
    res.status(500).json({ error: { code: err.code || "ERUNTIME", message: err.message } });
  }
});
  • Step 3: Manual smoke check
npm run dev &
sleep 3
# Replace 1 with a real managed lane id.
curl -s -X POST http://localhost:4820/api/lanes/1/profile/init | head -c 300
echo

Expected: {"scaffolded":false,"reason":"no detectable Node.js project"} for a lane with no backend/frontend package.json layout, or {"scaffolded":true,"written":[...],"todos":[...]} for one that has it. Either is correctly wired. Stop the dev server afterward (verify no stray process remains).

  • Step 4: Run the full suite + header check
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
  • Step 5: Commit
git add server/routes/lanes.js
git commit -m "feat(lanes): add POST /:id/profile/init route (F5)"

Task 2: AddLaneModal.tsx auto-setup + client API + test

Files:

  • Modify: client/src/lib/api.ts
  • Modify: client/src/components/lanes/AddLaneModal.tsx
  • Modify: client/src/components/lanes/__tests__/AddLaneModal.test.tsx

Interfaces:

  • Consumes: POST /:id/profile/init (Task 1), the already-existing api.lanes.agentsInstall(id)/api.lanes.mcpSync(id) (built in F4).

  • Produces: api.lanes.profileInit(id, force = false) => Promise<{scaffolded: boolean, written?: string[], todos?: string[], reason?: string}>.

  • Step 1: Add the client API method

In client/src/lib/api.ts's lanes object, add right after the existing mcpSync method (matching its exact style):

    profileInit: (id: number, force = false) =>
      request<{ scaffolded: boolean; written?: string[]; todos?: string[]; reason?: string }>(
        `/lanes/${id}/profile/init`,
        { method: "POST", body: JSON.stringify({ force }) }
      ),
  • Step 2: Add setup-result state to AddLaneModal.tsx

Near the existing state hooks (client/src/components/lanes/AddLaneModal.tsx:37-43):

  const [setupResult, setSetupResult] = useState<{
    profile: "scaffolded" | "skipped" | "failed";
    agents: "ok" | "failed";
    mcp: "ok" | "failed";
  } | null>(null);
  • Step 3: Run the three setup calls after worktree() succeeds

Replace the current submit handler's success path (client/src/components/lanes/AddLaneModal.tsx:87-106):

  const submit = async () => {
    const repo = sourceRepo.trim();
    const name = title.trim();
    if (!repo || !branches || !name) return;
    setBusy(true);
    setError(null);
    try {
      const result = await api.lanes.worktree({
        sourceRepo: repo,
        title: name,
        base: base || undefined,
      });

      const [profileOutcome, agentsOutcome, mcpOutcome] = await Promise.allSettled([
        api.lanes.profileInit(result.lane.id),
        api.lanes.agentsInstall(result.lane.id),
        api.lanes.mcpSync(result.lane.id),
      ]);
      setSetupResult({
        profile:
          profileOutcome.status === "fulfilled"
            ? profileOutcome.value.scaffolded
              ? "scaffolded"
              : "skipped"
            : "failed",
        agents: agentsOutcome.status === "fulfilled" ? "ok" : "failed",
        mcp: mcpOutcome.status === "fulfilled" ? "ok" : "failed",
      });

      reset();
      onAdded(result.lane);
      onClose();
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
      setBusy(false);
    }
  };

Note: this still closes the modal immediately (matching the plan's design decision — the result is informational and briefly visible via setupResult, but the modal closing and the user landing on their new lane isn't blocked on it). setSetupResult is called for a future consumer (e.g. a toast the Workspace page could read from a shared state, or a follow-up enhancement) — for THIS task, capturing and logging the outcome to the console is the minimum visible signal:

Add right after the setSetupResult(...) call:

      if (import.meta.env.DEV) {
        console.info("[add-lane] auto-setup result:", {
          profile: profileOutcome.status === "fulfilled" ? profileOutcome.value : profileOutcome.reason,
          agents: agentsOutcome.status === "fulfilled" ? agentsOutcome.value : agentsOutcome.reason,
          mcp: mcpOutcome.status === "fulfilled" ? mcpOutcome.value : mcpOutcome.reason,
        });
      }

(A full inline result banner in the modal is a reasonable follow-up but out of scope for this task — the modal closes and the lane is usable either way; Task 2's job is making the three calls fire and land somewhere observable, not designing a new toast system.)

  • Step 4: Update the existing test's mock

In client/src/components/lanes/__tests__/AddLaneModal.test.tsx, extend the vi.mock("../../../lib/api", ...) block:

vi.mock("../../../lib/api", () => ({
  api: {
    lanes: {
      branches: vi.fn(),
      worktree: vi.fn(),
      profileInit: vi.fn(),
      agentsInstall: vi.fn(),
      mcpSync: vi.fn(),
    },
  },
}));

Add a beforeEach (or extend an existing one) so tests not specifically about setup don't need to stub these individually:

beforeEach(() => {
  vi.mocked(api.lanes.profileInit).mockResolvedValue({ scaffolded: false, reason: "no detectable Node.js project" });
  vi.mocked(api.lanes.agentsInstall).mockResolvedValue({ installed: [] });
  vi.mocked(api.lanes.mcpSync).mockResolvedValue({ servers: [], profilesSeeded: [] });
});
  • Step 5: Write the failing tests

Add new test cases, following the file's existing renderModal()/focusField() helper pattern (read the existing test file for their exact signatures before using them):

  it("fires profileInit, agentsInstall, and mcpSync after a successful worktree call", async () => {
    vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" });
    vi.mocked(api.lanes.worktree).mockResolvedValue({
      lane: { id: 42, title: "demo", cwd: "/lanes/demo", status: "provisioning" } as Lane,
    });
    const onAdded = vi.fn();
    renderModal({ onAdded });
    const user = userEvent.setup();

    const repoField = screen.getByLabelText("Source repository");
    await focusField(user, repoField);
    await user.type(repoField, "/Users/tester/projects/repo");
    await screen.findByLabelText("Branch to fork from");
    await user.type(screen.getByLabelText("Feature title"), "demo");
    await user.click(screen.getByRole("button", { name: /add lane/i }));

    await waitFor(() => expect(api.lanes.worktree).toHaveBeenCalled());
    await waitFor(() => expect(api.lanes.profileInit).toHaveBeenCalledWith(42));
    expect(api.lanes.agentsInstall).toHaveBeenCalledWith(42);
    expect(api.lanes.mcpSync).toHaveBeenCalledWith(42);
    await waitFor(() => expect(onAdded).toHaveBeenCalled());
  });

  it("still calls onAdded and closes even when every setup call fails", async () => {
    vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" });
    vi.mocked(api.lanes.worktree).mockResolvedValue({
      lane: { id: 43, title: "demo2", cwd: "/lanes/demo2", status: "provisioning" } as Lane,
    });
    vi.mocked(api.lanes.profileInit).mockRejectedValue(new Error("boom"));
    vi.mocked(api.lanes.agentsInstall).mockRejectedValue(new Error("boom"));
    vi.mocked(api.lanes.mcpSync).mockRejectedValue(new Error("boom"));
    const onAdded = vi.fn();
    const onClose = vi.fn();
    renderModal({ onAdded, onClose });
    const user = userEvent.setup();

    const repoField = screen.getByLabelText("Source repository");
    await focusField(user, repoField);
    await user.type(repoField, "/Users/tester/projects/repo");
    await screen.findByLabelText("Branch to fork from");
    await user.type(screen.getByLabelText("Feature title"), "demo2");
    await user.click(screen.getByRole("button", { name: /add lane/i }));

    await waitFor(() => expect(onAdded).toHaveBeenCalled());
    expect(onClose).toHaveBeenCalled();
  });

Adapt the exact screen.getByLabelText/button-name selectors to match what the existing test file's OTHER passing tests actually use (read them first — the labels above are inferred from the earlier research and may not be verbatim; the existing "looks up branches" test is the ground truth for exact label text).

  • Step 6: Run the tests, fix any mismatches
cd client && npm test -- --run src/components/lanes/__tests__/AddLaneModal.test.tsx

Expected: PASS. Adjust selectors/assertions to match the ACTUAL rendered output if anything doesn't line up — the component is the source of truth.

  • Step 7: Regenerate the screens snapshot if needed
cd client && npm test -- -u

Review the diff (git diff client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap). AddLaneModal is not open by default in any snapshot render, so this diff should be EMPTY — if it isn't, investigate before accepting (same discipline as F4's Task 6).

  • Step 8: Run the full client suite + header check
cd client && npm test
bash .claude/skills/file-headers/scripts/check-headers.sh
  • Step 9: Commit
git add client/src/lib/api.ts client/src/components/lanes/AddLaneModal.tsx client/src/components/lanes/__tests__/AddLaneModal.test.tsx
git commit -m "feat(lanes): auto-setup (profile/agents/mcp) after Add Lane (F5)"

Task 3: Docs

Files:

  • Modify: docs/LANES.md

Interfaces: none — documentation only.

  • Step 1: Note the auto-setup behavior

In docs/LANES.md, find the ## Creating a lane section (search grep -n "^## Creating a lane" docs/LANES.md) and add a short paragraph after its existing content:

Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `ccam lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `ccam lanes agents install`, and `ccam lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
  • Step 2: Verify and commit
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
git add docs/LANES.md
git commit -m "docs(lanes): document Add Lane auto-setup (F5)"