diff --git a/client/src/components/lanes/LaneCard.tsx b/client/src/components/lanes/LaneCard.tsx index a6c0570..93f5487 100644 --- a/client/src/components/lanes/LaneCard.tsx +++ b/client/src/components/lanes/LaneCard.tsx @@ -180,9 +180,17 @@ function since(sec: number | null): string { export default function LaneCard({ lane, onAction, + childWorktrees, + onSelectLane, }: { lane: Lane; onAction: (action: string, body?: Record) => void; + /** Other lanes whose `source_repo` is this lane's `cwd` — populated only + * when this lane is itself a source repo (typically an adopted one) that + * other lanes were provisioned as worktrees from. */ + childWorktrees?: Lane[]; + /** Jumps the Workspace page's selection to another lane's card. */ + onSelectLane?: (id: number) => void; }) { const { t } = useTranslation(["lanes"]); const [destructiveAction, setDestructiveAction] = useState<"reset" | "remove" | "purge" | null>( @@ -452,6 +460,31 @@ export default function LaneCard({ + {childWorktrees && childWorktrees.length > 0 && ( +
+
+ {t("worktrees.heading", { count: childWorktrees.length })} +
+ +
+ )} + {integrations && (
{(["tracker", "dev_qc", "ci_wait"] as const).map((name) => ( diff --git a/client/src/components/lanes/__tests__/AddLaneModal.test.tsx b/client/src/components/lanes/__tests__/AddLaneModal.test.tsx index a0a4d86..a1f1c6a 100644 --- a/client/src/components/lanes/__tests__/AddLaneModal.test.tsx +++ b/client/src/components/lanes/__tests__/AddLaneModal.test.tsx @@ -37,6 +37,7 @@ function laneFixture(over: Partial = {}): Lane { cwd: "/lanes/repo__feature", branch: "feat/feature", kind: "managed", + source_repo: null, pipeline: "default", session_id: null, run_id: null, diff --git a/client/src/components/lanes/__tests__/LaneCard.test.tsx b/client/src/components/lanes/__tests__/LaneCard.test.tsx index 0140e1b..ec25250 100644 --- a/client/src/components/lanes/__tests__/LaneCard.test.tsx +++ b/client/src/components/lanes/__tests__/LaneCard.test.tsx @@ -54,6 +54,7 @@ function makeLane(overrides: Partial = {}): Lane { cwd: "/work/demo", branch: "lane/demo", kind: "adopted", + source_repo: null, pipeline: "default", session_id: null, run_id: null, @@ -90,6 +91,31 @@ describe("LaneCard status badge", () => { } }); +describe("LaneCard child worktrees", () => { + it("lists worktrees provisioned from this lane and jumps to one on click", async () => { + const onSelectLane = vi.fn(); + const worktree = makeLane({ id: 8, title: "Worktree A", status: "running" }); + render( + + ); + + expect(screen.getByTestId("lane-child-worktrees")).toBeInTheDocument(); + const user = userEvent.setup(); + await user.click(screen.getByRole("button", { name: /Worktree A/ })); + expect(onSelectLane).toHaveBeenCalledWith(8); + }); + + it("renders nothing when there are no child worktrees", () => { + render(); + expect(screen.queryByTestId("lane-child-worktrees")).not.toBeInTheDocument(); + }); +}); + const pipelineNodes: Lane["pipeline_nodes"] = [ { id: "intake", label: "intake", icon: "📥", gate: false, state: "done" }, { id: "plan", label: "plan", icon: "🧭", gate: false, state: "done" }, diff --git a/client/src/i18n/locales/en/lanes.json b/client/src/i18n/locales/en/lanes.json index 28e728e..cd7b537 100644 --- a/client/src/i18n/locales/en/lanes.json +++ b/client/src/i18n/locales/en/lanes.json @@ -29,6 +29,8 @@ "addLaneSetupProfile": "Profile", "addLaneSetupTitle": "Setup", "addLaneTitleLabel": "Title", + "worktrees.heading_one": "{{count}} worktree", + "worktrees.heading_other": "{{count}} worktrees", "addLaneTitlePlaceholder": "Optional", "autoStage": "auto: {{stage}}", "cardId": "Lane {{id}}", diff --git a/client/src/i18n/locales/vi/lanes.json b/client/src/i18n/locales/vi/lanes.json index dd469aa..5d344c3 100644 --- a/client/src/i18n/locales/vi/lanes.json +++ b/client/src/i18n/locales/vi/lanes.json @@ -29,6 +29,8 @@ "addLaneSetupProfile": "Profile", "addLaneSetupTitle": "Thiết lập", "addLaneTitleLabel": "Tiêu đề", + "worktrees.heading_one": "{{count}} worktree", + "worktrees.heading_other": "{{count}} worktree", "addLaneTitlePlaceholder": "Không bắt buộc", "autoStage": "tự động: {{stage}}", "cardId": "Làn đường {{id}}", diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index 7e38257..6d2291a 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -2319,6 +2319,9 @@ export interface Lane { cwd: string; branch: string | null; kind: "managed" | "adopted"; + /** For a managed worktree lane, the repo it was provisioned from. Null for + * an adopted lane (it IS a source repo, not a worktree of one). */ + source_repo: string | null; pipeline: string; session_id: string | null; run_id: string | null; diff --git a/client/src/pages/Workspace.tsx b/client/src/pages/Workspace.tsx index 0679cde..d5b6a9b 100644 --- a/client/src/pages/Workspace.tsx +++ b/client/src/pages/Workspace.tsx @@ -1079,6 +1079,10 @@ export function Workspace() { handleLaneAction(currentLane.id, a, b)} + childWorktrees={lanes.filter( + (l) => l.source_repo === currentLane.cwd && l.id !== currentLane.id + )} + onSelectLane={setSelectedLaneId} />
diff --git a/docs/LANES.md b/docs/LANES.md index 5dcad93..01b76f3 100644 --- a/docs/LANES.md +++ b/docs/LANES.md @@ -32,6 +32,8 @@ ccam lanes add --repo /path/to/repo --title "My Feature" --base main --slug my-f `--title`, `--base`, and `--slug` are optional. The CLI waits for background provisioning to finish and reports either the ready lane or its failure notes. +Adopting your main repo (`ccam lanes add --cwd $(pwd)`) is also how you get stage detection working for sessions that work directly in it rather than in a worktree — see "Superpowers skill invocations" below. Once adopted, that lane's own card lists every managed-worktree lane whose `source_repo` matches its `cwd`, each a clickable link to jump to that lane. + 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`. The modal shows a ✓/✗ summary of the three results and stays open until dismissed (Cancel/X) — it does not auto-close. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply. ## Destructive lane actions @@ -672,6 +674,23 @@ to the flattened input. Either way the result is capped at 120 characters lane's tooltip sometimes shows a file path or a skill name rather than a shell command: it's whichever of the fields above the matching rule's `tool` carried. +### Superpowers skill invocations + +The built-in `default` pipeline's `plan`, `implement`, `review`, and `ship` +nodes each carry a `{"tool": "Skill", "match": "..."}` rule matching the +Superpowers workflow skill names (`brainstorming`/`writing-plans`, +`executing-plans`/`subagent-driven-development`, `code-review`/ +`requesting-code-review`, `finishing-a-development-branch`). Invoking one of +these skills is a much stronger signal than a matched Bash command, but it is +still detection, not declaration — it renders dashed amber and never `done`, +same as every other detected stage. + +Detection only ever attributes to a lane whose `cwd` matches the hook's +session `cwd` (see "Which lane a signal is credited to" above). A session +working directly in a source repo that was never itself adopted as a lane — +run `ccam lanes add --cwd $(pwd)` from that repo to fix that — gets no +detection at all, because no lane owns that `cwd`. + ### Detection expires Forward-only would otherwise park a lane at the highest stage it ever touched: diff --git a/server/data/pipelines/default.json b/server/data/pipelines/default.json index a18226d..cbb21a9 100644 --- a/server/data/pipelines/default.json +++ b/server/data/pipelines/default.json @@ -45,6 +45,10 @@ "build" ], "detect": [ + { + "tool": "Skill", + "match": "executing-plans|subagent-driven-development" + }, { "tool": "Edit" }, @@ -118,6 +122,10 @@ "push" ], "detect": [ + { + "tool": "Skill", + "match": "finishing-a-development-branch" + }, { "tool": "Bash", "match": "\\bgit\\b(?:\\s+-c\\s+[\\w.-]+=(?:'[^']*'|\\\"[^\\\"]*\\\"|\\S+)|\\s+-{1,2}[\\w.-]+(?:=(?:'[^']*'|\\\"[^\\\"]*\\\"|\\S+))?)*\\s+push\\b|\\bgh\\b[^;&|]*\\bpr create\\b"