refactor(lanes): drop the workflow skills and Superpowers link; pipelines go dormant
The plugin stops shipping its workflow layer: - skills/ship-feature and skills/ship-feature-lane, the 18 stage/ship slash commands (plugin.json keeps agenthub-doctor/update/open), and all four agents/ (qc-local, senior-gate-reviewer, backend-reviewer, frontend-reviewer). - CLI `agenthub skills install`, `agenthub lanes agents install`, `agenthub ship-full|ship-quick`; routes POST /api/skills/install and POST /api/lanes/:id/agents/install; the Settings button, the lane Tools menu item and the Add-lane setup step that called them. - The Superpowers auto-install: server/lib/skill-plugins.js, the bootstrap's autoInstallSkillPlugins / skillPluginsSettled gate and the doctor row. Pipelines are kept but dormant: server/data/pipelines/ is deleted, so no template ships. `default` resolves to EMPTY_PIPELINE (no nodes, progress 0), createLane still accepts it, POST /api/lanes/:id/stage answers 400 ENOPIPELINE, and the Workspace hides the stage timer, pickers, PipelineMap and strip stage chip for a lane with no nodes. A template dropped into DASHBOARD_PIPELINES_DIR turns it all back on. The server suite loads its templates from server/__tests__/fixtures/pipelines/. Docs: CLAUDE.md (marker, retired rules), README(.vi), INSTALL, ARCHITECTURE, docs/LANES|PLUGINS|CLI|API, agenthub-introduction.html.
This commit is contained in:
@@ -115,7 +115,6 @@ export function AddLaneModal({
|
||||
const [browseOpen, setBrowseOpen] = useState(false);
|
||||
const [setupResult, setSetupResult] = useState<{
|
||||
profile: "scaffolded" | "skipped" | "failed";
|
||||
agents: "ok" | "failed";
|
||||
mcp: "ok" | "failed";
|
||||
router: "ok" | "skipped" | "failed";
|
||||
} | null>(null);
|
||||
@@ -294,7 +293,7 @@ export function AddLaneModal({
|
||||
// POST /worktree returns as soon as the DB row exists (202) — the
|
||||
// actual `git worktree add` runs afterward, in the background, on the
|
||||
// server. Firing setup against the lane's cwd before that finishes
|
||||
// means agents/mcp write into a directory that doesn't exist yet, so
|
||||
// means mcp writes into a directory that doesn't exist yet, so
|
||||
// wait for provisioning to leave the "provisioning" status first.
|
||||
const provisionedLane = await waitForProvisioned(result.lane.id);
|
||||
if (!provisionedLane || provisionedLane.status === "failed") {
|
||||
@@ -303,9 +302,8 @@ export function AddLaneModal({
|
||||
return;
|
||||
}
|
||||
|
||||
const [profileOutcome, agentsOutcome, mcpOutcome, routerOutcome] = await Promise.allSettled([
|
||||
const [profileOutcome, mcpOutcome, routerOutcome] = await Promise.allSettled([
|
||||
api.lanes.profileInit(result.lane.id),
|
||||
api.lanes.agentsInstall(result.lane.id),
|
||||
api.lanes.mcpSync(result.lane.id),
|
||||
applyRouter(result.lane.id),
|
||||
]);
|
||||
@@ -316,7 +314,6 @@ export function AddLaneModal({
|
||||
? "scaffolded"
|
||||
: "skipped"
|
||||
: "failed",
|
||||
agents: agentsOutcome.status === "fulfilled" ? "ok" : "failed",
|
||||
mcp: mcpOutcome.status === "fulfilled" ? "ok" : "failed",
|
||||
router:
|
||||
routerOutcome.status !== "fulfilled" ? "failed" : router === "native" ? "skipped" : "ok",
|
||||
@@ -328,7 +325,6 @@ export function AddLaneModal({
|
||||
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,
|
||||
// Never the profile itself: it would put an endpoint and a lane id in
|
||||
// the browser console. (The key is never sent here at all.)
|
||||
@@ -740,7 +736,6 @@ export function AddLaneModal({
|
||||
ok: setupResult.profile !== "failed",
|
||||
skipped: setupResult.profile === "skipped",
|
||||
},
|
||||
{ label: t("addLaneSetupAgents"), ok: setupResult.agents === "ok" },
|
||||
{ label: t("addLaneSetupMcp"), ok: setupResult.mcp === "ok" },
|
||||
{
|
||||
label: t("addLaneSetupRouter"),
|
||||
|
||||
@@ -271,7 +271,7 @@ export function since(sec: number | null): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Repo tooling (agents install / MCP sync / dev-sync check, and the
|
||||
* Repo tooling (MCP sync / dev-sync check, and the
|
||||
* create/update-readme run, which moved here from the Add-lane modal) for one lane,
|
||||
* collapsed into a single dropdown. Lives outside LaneCard so it can be
|
||||
* placed in the Workspace page header, next to "Add lane" — it polls its
|
||||
@@ -281,16 +281,11 @@ export function since(sec: number | null): string {
|
||||
export function LaneToolsMenu({ lane }: { lane: Lane }) {
|
||||
const { t } = useTranslation(["lanes"]);
|
||||
const [toolsMenuOpen, setToolsMenuOpen] = useState(false);
|
||||
const [laneActionBusy, setLaneActionBusy] = useState<
|
||||
"agents" | "mcp" | "sync-check" | "docs" | null
|
||||
>(null);
|
||||
const [laneActionBusy, setLaneActionBusy] = useState<"mcp" | "sync-check" | "docs" | null>(null);
|
||||
const [laneActionResult, setLaneActionResult] = useState<string | null>(null);
|
||||
const runtime = useLaneRuntime(lane.id, 0);
|
||||
|
||||
const runLaneAction = async (
|
||||
which: "agents" | "mcp" | "sync-check" | "docs",
|
||||
fn: () => Promise<string>
|
||||
) => {
|
||||
const runLaneAction = async (which: "mcp" | "sync-check" | "docs", fn: () => Promise<string>) => {
|
||||
setLaneActionBusy(which);
|
||||
setLaneActionResult(null);
|
||||
try {
|
||||
@@ -302,12 +297,6 @@ export function LaneToolsMenu({ lane }: { lane: Lane }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAgentsInstall = () =>
|
||||
runLaneAction("agents", async () => {
|
||||
const result = await api.lanes.agentsInstall(lane.id);
|
||||
return t("actions.agentsInstallResult", { files: result.installed.join(", ") });
|
||||
});
|
||||
|
||||
// A skill only exists inside a CLI session, so there is nothing server-side
|
||||
// to call: this opens the lane's own terminal run with the slash command as
|
||||
// its first message. Claude-only — codex has no slash commands.
|
||||
@@ -363,22 +352,6 @@ export function LaneToolsMenu({ lane }: { lane: Lane }) {
|
||||
role="menu"
|
||||
className="absolute right-0 z-10 mt-1 min-w-44 overflow-hidden rounded-md border border-border-light bg-surface-0 py-1 shadow-lg shadow-black/40"
|
||||
>
|
||||
{/* Claude-only: other CLIs have no agents to install. */}
|
||||
{lane.tool === "claude" && (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="lane-agents-install"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setToolsMenuOpen(false);
|
||||
void handleAgentsInstall();
|
||||
}}
|
||||
className="block w-full px-3 py-1.5 text-left text-fg-secondary hover:bg-surface-2"
|
||||
>
|
||||
{t("actions.agentsInstall")}
|
||||
</button>
|
||||
)}
|
||||
{lane.tool === "claude" && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -42,7 +42,7 @@ const LIVENESS_DOT: Record<Lane["liveness"], string> = {
|
||||
* recordDetection refuses any detection that isn't strictly ahead.
|
||||
*/
|
||||
function detectionLeads(lane: Lane): boolean {
|
||||
if (!lane.detected_stage) return false;
|
||||
if (!lane.detected_stage || !lane.pipeline_nodes.length) return false;
|
||||
const nodes = lane.pipeline_nodes;
|
||||
return (
|
||||
nodes.findIndex((n) => n.id === lane.detected_stage) >=
|
||||
@@ -116,9 +116,9 @@ export default function LaneStripCard({
|
||||
<span className="shrink-0 text-[10px] font-semibold tracking-widest text-fg-muted">
|
||||
#{lane.id}
|
||||
</span>
|
||||
{/* Only claude lanes carry a pipeline; the others show their CLI's
|
||||
tag in place of the stage chip. */}
|
||||
{lane.tool !== "claude" ? (
|
||||
{/* Only claude lanes carry a pipeline, and only while a template is
|
||||
installed; the others show their CLI's tag in place of the stage chip. */}
|
||||
{lane.tool !== "claude" || lane.pipeline_nodes.length === 0 ? (
|
||||
<span className="shrink-0 rounded border border-border bg-surface-2 px-1.5 py-0.5 text-[10px] text-fg-muted">
|
||||
{LANE_TOOL_LABEL[lane.tool]}
|
||||
</span>
|
||||
|
||||
@@ -27,7 +27,6 @@ vi.mock("../../../lib/api", () => ({
|
||||
browse: vi.fn(),
|
||||
get: vi.fn(),
|
||||
profileInit: vi.fn(),
|
||||
agentsInstall: vi.fn(),
|
||||
mcpSync: vi.fn(),
|
||||
routerApply: vi.fn(),
|
||||
},
|
||||
@@ -120,7 +119,6 @@ beforeEach(() => {
|
||||
vi.mocked(api.lanes.profileInit)
|
||||
.mockReset()
|
||||
.mockResolvedValue({ scaffolded: false, reason: "no detectable Node.js project" });
|
||||
vi.mocked(api.lanes.agentsInstall).mockReset().mockResolvedValue({ installed: [] });
|
||||
vi.mocked(api.lanes.mcpSync).mockReset().mockResolvedValue({ servers: [], profilesSeeded: [] });
|
||||
vi.mocked(api.lanes.routerApply).mockReset().mockResolvedValue({
|
||||
tool: "claude",
|
||||
@@ -383,7 +381,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("fires profileInit, agentsInstall, and mcpSync after a successful worktree call", async () => {
|
||||
it("fires profileInit and mcpSync after a successful worktree call", async () => {
|
||||
vi.mocked(api.lanes.branches).mockResolvedValue({
|
||||
branches: ["main"],
|
||||
current: "main",
|
||||
@@ -401,7 +399,6 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
|
||||
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());
|
||||
});
|
||||
@@ -416,7 +413,6 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
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();
|
||||
@@ -482,7 +478,6 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
||||
await waitFor(() => expect(onAdded).toHaveBeenCalled());
|
||||
expect(await screen.findByText(/auto-setup was skipped/)).toBeInTheDocument();
|
||||
expect(api.lanes.profileInit).not.toHaveBeenCalled();
|
||||
expect(api.lanes.agentsInstall).not.toHaveBeenCalled();
|
||||
expect(api.lanes.mcpSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ vi.mock("../../../lib/api", () => ({
|
||||
up: vi.fn(),
|
||||
down: vi.fn(),
|
||||
preflight: vi.fn().mockResolvedValue({ blocked: [], warnings: [] }),
|
||||
agentsInstall: vi.fn(),
|
||||
mcpSync: vi.fn(),
|
||||
syncBaseCheck: vi.fn(),
|
||||
routerApply: vi.fn(),
|
||||
@@ -309,17 +308,6 @@ describe("LaneToolsMenu", () => {
|
||||
} as unknown as LaneRuntime);
|
||||
});
|
||||
|
||||
it("clicking Install agents calls the API and shows the result", async () => {
|
||||
vi.mocked(api.lanes.agentsInstall).mockResolvedValue({
|
||||
installed: ["qc-local.md", "senior-gate-reviewer.md"],
|
||||
});
|
||||
render(<LaneToolsMenu lane={makeLane()} />);
|
||||
await userEvent.click(await screen.findByTestId("lane-tools-menu"));
|
||||
await userEvent.click(await screen.findByTestId("lane-agents-install"));
|
||||
await waitFor(() => expect(api.lanes.agentsInstall).toHaveBeenCalledWith(1));
|
||||
expect(await screen.findByTestId("lane-action-result")).toHaveTextContent("qc-local.md");
|
||||
});
|
||||
|
||||
it("clicking Sync MCP calls the API and shows the result", async () => {
|
||||
vi.mocked(api.lanes.mcpSync).mockResolvedValue({
|
||||
servers: ["playwright"],
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"action.stop": "stop",
|
||||
"actionError": "Lane action failed: {{message}}",
|
||||
"actionErrorUnknown": "Unknown error",
|
||||
"actions.agentsInstall": "Install agents",
|
||||
"actions.agentsInstallResult": "Installed: {{files}}",
|
||||
"actions.busy": "…",
|
||||
"actions.mcpSync": "Sync MCP",
|
||||
"actions.mcpSyncResult": "Synced: {{servers}}",
|
||||
@@ -49,7 +47,6 @@
|
||||
"addLaneRouterLoading": "Loading router profiles…",
|
||||
"addLaneRouterNative": "Native",
|
||||
"addLaneRouterProfileLabel": "Router profile",
|
||||
"addLaneSetupAgents": "Agents",
|
||||
"addLaneSetupClose": "Close",
|
||||
"addLaneSetupMcp": "MCP servers",
|
||||
"addLaneSetupProfile": "Profile",
|
||||
|
||||
@@ -63,9 +63,7 @@
|
||||
},
|
||||
"lanesSection": {
|
||||
"title": "Lanes",
|
||||
"description": "Machine-wide housekeeping and one-time setup for the ship-feature-lane pipeline.",
|
||||
"installSkill": "Install ship-feature-lane skill",
|
||||
"installSkillSuccess": "Installed to {{path}}",
|
||||
"description": "Machine-wide lane housekeeping.",
|
||||
"runHousekeeping": "Run housekeeping",
|
||||
"dryRun": "Dry run",
|
||||
"gcResult": "Reaped {{reaped}} process(es), capped {{capped}} log(s)"
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"action.stop": "Dừng",
|
||||
"actionError": "Thao tác làn đường thất bại: {{message}}",
|
||||
"actionErrorUnknown": "Lỗi không xác định",
|
||||
"actions.agentsInstall": "Cài agent",
|
||||
"actions.agentsInstallResult": "Đã cài: {{files}}",
|
||||
"actions.busy": "…",
|
||||
"actions.mcpSync": "Đồng bộ MCP",
|
||||
"actions.mcpSyncResult": "Đã đồng bộ: {{servers}}",
|
||||
@@ -49,7 +47,6 @@
|
||||
"addLaneRouterLoading": "Đang tải danh sách router…",
|
||||
"addLaneRouterNative": "Native",
|
||||
"addLaneRouterProfileLabel": "Profile router",
|
||||
"addLaneSetupAgents": "Agent",
|
||||
"addLaneSetupClose": "Đóng",
|
||||
"addLaneSetupMcp": "MCP server",
|
||||
"addLaneSetupProfile": "Profile",
|
||||
|
||||
@@ -63,9 +63,7 @@
|
||||
},
|
||||
"lanesSection": {
|
||||
"title": "Lanes",
|
||||
"description": "Bảo trì toàn máy và cài đặt một lần cho pipeline ship-feature-lane.",
|
||||
"installSkill": "Cài skill ship-feature-lane",
|
||||
"installSkillSuccess": "Đã cài vào {{path}}",
|
||||
"description": "Dọn dẹp lane trên toàn máy.",
|
||||
"runHousekeeping": "Chạy dọn dẹp",
|
||||
"dryRun": "Chạy thử (dry run)",
|
||||
"gcResult": "Đã dọn {{reaped}} tiến trình, cắt {{capped}} log"
|
||||
|
||||
@@ -1130,8 +1130,6 @@ export const api = {
|
||||
purged_events: number;
|
||||
purged_agents: number;
|
||||
}>("/settings/cleanup", { method: "POST", body: JSON.stringify(params) }),
|
||||
skillsInstall: () =>
|
||||
request<{ installed: true; path: string }>("/skills/install", { method: "POST", body: "{}" }),
|
||||
gc: (body: { dryRun?: boolean } = {}) =>
|
||||
request<{ reaped: number[]; capped: { path: string; sizeBefore: number }[] }>("/lanes/gc", {
|
||||
method: "POST",
|
||||
@@ -2719,11 +2717,6 @@ export const api = {
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
},
|
||||
agentsInstall: (id: number) =>
|
||||
request<{ installed: string[] }>(`/lanes/${id}/agents/install`, {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
}),
|
||||
/**
|
||||
* POST /lanes/:id/router/apply - point this lane at an AI Router profile,
|
||||
* or back at `native`. Writes the lane's own config on disk; only the
|
||||
|
||||
@@ -699,12 +699,6 @@ export function Settings() {
|
||||
return res.ok ? t("hooks.success") : t("hooks.failed");
|
||||
});
|
||||
|
||||
const handleSkillsInstall = () =>
|
||||
runAction("skills-install", async () => {
|
||||
const res = await api.settings.skillsInstall();
|
||||
return t("lanesSection.installSkillSuccess", { path: res.path });
|
||||
});
|
||||
|
||||
const handleGc = () =>
|
||||
runAction("gc", async () => {
|
||||
const res = await api.settings.gc({ dryRun: gcDryRun });
|
||||
@@ -1354,23 +1348,6 @@ export function Settings() {
|
||||
<p className="text-xs text-fg-muted mb-4">{t("lanesSection.description")}</p>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<span className="text-xs text-fg-secondary">{t("lanesSection.installSkill")}</span>
|
||||
<button
|
||||
onClick={handleSkillsInstall}
|
||||
disabled={actionLoading !== null}
|
||||
className="btn-ghost text-xs disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === "skills-install" ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{t("lanesSection.installSkill")}
|
||||
</button>
|
||||
</div>
|
||||
{actionBanner(["skills-install"])}
|
||||
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<label className="flex items-center gap-2 text-xs text-fg-secondary">
|
||||
<input
|
||||
|
||||
@@ -523,8 +523,10 @@ export function Workspace() {
|
||||
}, [refreshList]);
|
||||
|
||||
const currentLane = selectedLaneId !== null ? lanes.find((l) => l.id === selectedLaneId) : null;
|
||||
// Only claude lanes carry a pipeline (codex and antigravity emit no hooks).
|
||||
const noPipeline = !!currentLane && currentLane.tool !== "claude";
|
||||
// Only claude lanes carry a pipeline (codex and antigravity emit no hooks),
|
||||
// and only while a template is installed — none ships, so it is dormant.
|
||||
const noPipeline =
|
||||
!!currentLane && (currentLane.tool !== "claude" || currentLane.pipeline_nodes.length === 0);
|
||||
|
||||
// Feature list follows the selected lane, resets the viewer on lane switch.
|
||||
useEffect(() => {
|
||||
@@ -817,8 +819,9 @@ export function Workspace() {
|
||||
<Pencil className="h-3 w-3 shrink-0 text-fg-muted opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</button>
|
||||
)}
|
||||
{/* Non-claude lanes carry no pipeline: no time-at-stage, no
|
||||
pipeline or feature picker, no map. */}
|
||||
{/* A lane with no pipeline (non-claude, or no template
|
||||
installed): no time-at-stage, no pipeline or feature
|
||||
picker, no map. */}
|
||||
{!noPipeline && (
|
||||
<span className="shrink-0 tabular-nums text-xs text-fg-muted">
|
||||
{since(currentLane.stage_seconds)}
|
||||
@@ -882,7 +885,7 @@ export function Workspace() {
|
||||
</>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{/* Per-lane actions (agents install, mcp sync, sync-base check)
|
||||
{/* Per-lane actions (mcp sync, sync-base check)
|
||||
belong beside the lane they act on, not in a page header. */}
|
||||
<LaneToolsMenu lane={currentLane} />
|
||||
<button
|
||||
|
||||
@@ -8435,44 +8435,11 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
<p
|
||||
class="text-xs text-fg-muted mb-4"
|
||||
>
|
||||
Machine-wide housekeeping and one-time setup for the ship-feature-lane pipeline.
|
||||
Machine-wide lane housekeeping.
|
||||
</p>
|
||||
<div
|
||||
class="card p-5 space-y-4"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-wrap gap-3"
|
||||
>
|
||||
<span
|
||||
class="text-xs text-fg-secondary"
|
||||
>
|
||||
Install ship-feature-lane skill
|
||||
</span>
|
||||
<button
|
||||
class="btn-ghost text-xs disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-rotate-ccw w-3.5 h-3.5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
|
||||
/>
|
||||
<path
|
||||
d="M3 3v5h5"
|
||||
/>
|
||||
</svg>
|
||||
Install ship-feature-lane skill
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between flex-wrap gap-3"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user