From 64c00dea8555cce3ad42ddcc393d2997354ae3cd Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 6 Aug 2026 11:35:36 +0700 Subject: [PATCH] =?UTF-8?q?docs(lanes):=20plan=20F5=20=E2=80=94=20auto-set?= =?UTF-8?q?up=20after=20Add=20Lane=20(E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../plans/2026-08-06-add-lane-auto-setup.md | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-06-add-lane-auto-setup.md diff --git a/docs/superpowers/plans/2026-08-06-add-lane-auto-setup.md b/docs/superpowers/plans/2026-08-06-add-lane-auto-setup.md new file mode 100644 index 0000000..90653fd --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-add-lane-auto-setup.md @@ -0,0 +1,338 @@ +# 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 `await`s 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): + +```js +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 `});`): + +```js +/** + * 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 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** + +```bash +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 +bash .claude/skills/file-headers/scripts/check-headers.sh +npm run test:server +``` + +- [ ] **Step 5: Commit** + +```bash +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): + +```typescript + 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`): + +```typescript + 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`): + +```typescript + 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: + +```typescript + 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: + +```typescript +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: + +```typescript +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): + +```typescript + 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** + +```bash +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** + +```bash +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** + +```bash +cd client && npm test +bash .claude/skills/file-headers/scripts/check-headers.sh +``` + +- [ ] **Step 9: Commit** + +```bash +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: + +```markdown +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 +bash .claude/skills/file-headers/scripts/check-headers.sh +npm run test:server +``` + +```bash +git add docs/LANES.md +git commit -m "docs(lanes): document Add Lane auto-setup (F5)" +```