Skip to content

feat: template gallery#3840

Open
bdervishi wants to merge 13 commits intopaperclipai:masterfrom
bdervishi:feat/templates-gallery
Open

feat: template gallery#3840
bdervishi wants to merge 13 commits intopaperclipai:masterfrom
bdervishi:feat/templates-gallery

Conversation

@bdervishi
Copy link
Copy Markdown

Thinking Path

  • Paperclip orchestrates AI agents for zero-human companies
  • Onboarding a new company today requires an operator to hand-author every agent, role, and governance rule from scratch
  • That first-run experience is the steepest cliff for new users — there is no "shipping team in 5 minutes" path
  • A curated template gallery gives users a one-click install of a pre-configured multi-agent company
  • This pull request adds a server-side template registry (JSON-backed, cached), install + refresh endpoints, and a /templates UI that lists and installs them
  • The benefit is that new users can go from empty Paperclip instance to a functioning agent org in seconds, using battle-tested configurations

What Changed

  • Shared types for the template registry schema (packages/shared)
  • Docs: seed docs/templates/registry.json with four curated companies
  • Server:
    • Template registry loader service with in-memory caching
    • GET /api/templates/companies — list templates
    • POST /api/templates/companies/install — install selected template
    • POST /api/templates/refresh — admin-only cache invalidate
    • Templates router mounted in app.ts
  • UI (ui package):
    • Templates API client module
    • CompanyCard component (with jest-dom / vitest setup for the UI package)
    • TemplatesPage with React Query + per-card install mutation → redirects to new company's dashboard
    • Route /templates registered and nav link added to Sidebar (Company section)
  • E2E: Playwright smoke test that lists templates and installs one end-to-end

Verification

  • pnpm test:run — full UI test suite: 85/85 files, 446 tests passing on this branch
  • pnpm typecheck — clean (requires NODE_OPTIONS=--max-old-space-size=8192 on the templates-gallery UI package due to project size, unchanged from master)
  • pnpm exec playwright test --config tests/e2e/playwright.config.ts tests/e2e/templates-gallery.spec.ts --list — spec registers correctly
  • Manual smoke on staging (maestro.trifti.ch) planned post-merge per spec §Task 13

Risks

  • Low risk overall. New surface area only; no changes to existing endpoints, schemas, or user data flows.
  • Install endpoint performs a real outbound fetch during company import; bounded by the registry contents. If GitHub is unreachable, the E2E test will time out at 120s (acceptable, surfaces the problem clearly).
  • POST /api/templates/refresh is admin-gated. Non-admin boards cannot invalidate the registry cache.

Model Used

  • Provider: Anthropic Claude
  • Model: claude-opus-4-7 (Opus 4.7), 1M context window
  • Reasoning mode: standard
  • Capabilities: tool use (file edits, shell, subagents)
  • Workflow: 13-task phased implementation plan, each task shipped by a dispatched subagent with verification before commit

Spec

Implements /root/docs/superpowers/plans/2026-04-16-paperclip-templates-gallery.md and the companion design spec 2026-04-16-paperclip-templates-gallery-design.md.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 16, 2026

Greptile Summary

Adds a template gallery — a curated registry of pre-built agent companies that users can install in one click. The implementation covers shared Zod types, a file-backed registry service with in-memory caching, three new API endpoints (GET /templates/companies, POST /templates/companies/install, POST /templates/refresh), and a React frontend with a TemplatesPage, CompanyCard, sidebar nav link, and React Query mutations.

  • P1 — TemplatesPage.tsx line 43: installing is only true for the card matching install.variables, so every other card's Install button stays enabled during an active install. TanStack Query v5 fires onSuccess for each completed mutate() call, meaning rapid multi-card clicks can produce duplicate company creations and multiple navigate() calls.

Confidence Score: 4/5

One P1 bug (concurrent installs can create duplicate companies) should be fixed before merging; the two P2 findings are non-blocking.

The concurrent-install defect in TemplatesPage is a present correctness issue — other cards stay clickable during an install and TanStack Query v5 calls onSuccess for each completing mutation, making duplicate company creation a realistic outcome. The remaining two findings (CWD-based path and error detail leakage) are P2 quality/hardening items that don't block the primary flow.

ui/src/pages/TemplatesPage.tsx (P1 concurrent-install bug), server/src/app.ts (fragile CWD path), server/src/routes/templates.ts (error detail field)

Important Files Changed

Filename Overview
ui/src/pages/TemplatesPage.tsx New template gallery page — P1: only the active card's Install button is disabled during an ongoing install, leaving other cards clickable and allowing duplicate company creation.
server/src/routes/templates.ts New Express routes for listing, installing, and refreshing templates — auth gating is correct; 503 detail fields expose absolute server file paths to authenticated clients.
server/src/app.ts Mounts templateRoutes and shares portabilityService — registry path resolved via process.cwd(), which breaks if the server starts from a non-root directory.
server/src/services/template-registry.ts File-backed registry with in-memory cache and invalidation — clean implementation; concurrent cold-start reads are harmless.
packages/shared/src/template-types.ts Adds Zod schemas and TypeScript types for template registry — well-structured and validates slug format, URL, and counts correctly.
ui/src/components/templates/CompanyCard.tsx Simple presentational card component — renders correctly; the disabled prop is driven by the installing flag passed from TemplatesPage.
server/src/routes/companies.ts Accepts optional portabilityOverride to share the service instance with the templates router — minimal, non-breaking change.
ui/src/components/Sidebar.tsx Adds Templates nav link to the Company section using LayoutGrid icon — consistent with existing sidebar items.
ui/src/api/templates.ts Thin API client wrapping list, install, and refresh endpoints with correct shared types.
tests/e2e/templates-gallery.spec.ts Playwright smoke test for list + install flow with appropriate 120s timeout for the real GitHub fetch.
docs/templates/registry.json Seed registry with four curated companies — all entries conform to the templateRegistrySchema.
ui/src/App.tsx Registers /templates route inside a Layout wrapper — consistent with existing route patterns.
ui/vitest.config.ts Adds globals and test-setup file to wire up jest-dom matchers for the UI package.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/pages/TemplatesPage.tsx
Line: 42-43

Comment:
**Concurrent installs not guarded — duplicate company creation possible**

`installing` is `false` for every card whose slug doesn't match the active mutation, so their buttons stay enabled while an install is in flight. In TanStack Query v5, calling `mutate()` while a prior mutation is pending fires both requests and calls `onSuccess` for each one that completes — which would create two companies and trigger two `navigate()` calls in quick succession.

Fix: gate the disabled state on `install.isPending` globally, while keeping the per-slug check only for the label:

```suggestion
            onInstall={(slug) => install.mutate(slug)}
            installing={install.isPending && install.variables === company.slug}
            disabled={install.isPending}
```

Then update `CompanyCard` to accept and forward a separate `disabled` prop to `<Button>`, or simply change `installing={...}` to `installing={install.isPending && install.variables === company.slug}` and `disabled` on the card to `install.isPending` so all buttons are blocked until the current install resolves.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: server/src/routes/templates.ts
Line: 41

Comment:
**Error detail leaks absolute server file path**

When the registry file is missing, `createTemplateRegistry.get()` throws `"Template registry not found at /absolute/path/docs/templates/registry.json"`. That full path is surfaced verbatim to authenticated clients via the `detail` field here (and again on the `/refresh` 503 at line 93). Board-level auth limits exposure, but it's still an unnecessary information disclosure of the server's filesystem layout.

```suggestion
      res.status(503).json({ error: "registry unavailable" });
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: server/src/app.ts
Line: 192-195

Comment:
**`process.cwd()` makes registry path fragile**

`path.resolve(process.cwd(), "docs/templates/registry.json")` only resolves correctly when the server is started from the repository root. Running from `server/` (e.g. `node dist/server.js` in the package directory) silently produces a wrong path and the registry will 503 on every request.

Using a path anchored to the source file is more robust:

```ts
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const templateRegistry = createTemplateRegistry(
  path.resolve(__dirname, "../../../docs/templates/registry.json"),
);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "test(e2e): smoke test for template galle..." | Re-trigger Greptile

Comment on lines +42 to +43
onInstall={(slug) => install.mutate(slug)}
installing={install.isPending && install.variables === company.slug}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Concurrent installs not guarded — duplicate company creation possible

installing is false for every card whose slug doesn't match the active mutation, so their buttons stay enabled while an install is in flight. In TanStack Query v5, calling mutate() while a prior mutation is pending fires both requests and calls onSuccess for each one that completes — which would create two companies and trigger two navigate() calls in quick succession.

Fix: gate the disabled state on install.isPending globally, while keeping the per-slug check only for the label:

Suggested change
onInstall={(slug) => install.mutate(slug)}
installing={install.isPending && install.variables === company.slug}
onInstall={(slug) => install.mutate(slug)}
installing={install.isPending && install.variables === company.slug}
disabled={install.isPending}

Then update CompanyCard to accept and forward a separate disabled prop to <Button>, or simply change installing={...} to installing={install.isPending && install.variables === company.slug} and disabled on the card to install.isPending so all buttons are blocked until the current install resolves.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/pages/TemplatesPage.tsx
Line: 42-43

Comment:
**Concurrent installs not guarded — duplicate company creation possible**

`installing` is `false` for every card whose slug doesn't match the active mutation, so their buttons stay enabled while an install is in flight. In TanStack Query v5, calling `mutate()` while a prior mutation is pending fires both requests and calls `onSuccess` for each one that completes — which would create two companies and trigger two `navigate()` calls in quick succession.

Fix: gate the disabled state on `install.isPending` globally, while keeping the per-slug check only for the label:

```suggestion
            onInstall={(slug) => install.mutate(slug)}
            installing={install.isPending && install.variables === company.slug}
            disabled={install.isPending}
```

Then update `CompanyCard` to accept and forward a separate `disabled` prop to `<Button>`, or simply change `installing={...}` to `installing={install.isPending && install.variables === company.slug}` and `disabled` on the card to `install.isPending` so all buttons are blocked until the current install resolves.

How can I resolve this? If you propose a fix, please make it concise.

const registry = await deps.registry.get();
res.json({ companies: registry.companies });
} catch (err) {
res.status(503).json({ error: "registry unavailable", detail: (err as Error).message });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Error detail leaks absolute server file path

When the registry file is missing, createTemplateRegistry.get() throws "Template registry not found at /absolute/path/docs/templates/registry.json". That full path is surfaced verbatim to authenticated clients via the detail field here (and again on the /refresh 503 at line 93). Board-level auth limits exposure, but it's still an unnecessary information disclosure of the server's filesystem layout.

Suggested change
res.status(503).json({ error: "registry unavailable", detail: (err as Error).message });
res.status(503).json({ error: "registry unavailable" });
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/routes/templates.ts
Line: 41

Comment:
**Error detail leaks absolute server file path**

When the registry file is missing, `createTemplateRegistry.get()` throws `"Template registry not found at /absolute/path/docs/templates/registry.json"`. That full path is surfaced verbatim to authenticated clients via the `detail` field here (and again on the `/refresh` 503 at line 93). Board-level auth limits exposure, but it's still an unnecessary information disclosure of the server's filesystem layout.

```suggestion
      res.status(503).json({ error: "registry unavailable" });
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread server/src/app.ts
Comment on lines 192 to +195
companyDeletionEnabled: opts.companyDeletionEnabled,
}),
);
api.use("/companies", companyRoutes(db, opts.storageService));
const portabilityService = companyPortabilityService(db, opts.storageService);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 process.cwd() makes registry path fragile

path.resolve(process.cwd(), "docs/templates/registry.json") only resolves correctly when the server is started from the repository root. Running from server/ (e.g. node dist/server.js in the package directory) silently produces a wrong path and the registry will 503 on every request.

Using a path anchored to the source file is more robust:

import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const templateRegistry = createTemplateRegistry(
  path.resolve(__dirname, "../../../docs/templates/registry.json"),
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/app.ts
Line: 192-195

Comment:
**`process.cwd()` makes registry path fragile**

`path.resolve(process.cwd(), "docs/templates/registry.json")` only resolves correctly when the server is started from the repository root. Running from `server/` (e.g. `node dist/server.js` in the package directory) silently produces a wrong path and the registry will 503 on every request.

Using a path anchored to the source file is more robust:

```ts
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const templateRegistry = createTemplateRegistry(
  path.resolve(__dirname, "../../../docs/templates/registry.json"),
);
```

How can I resolve this? If you propose a fix, please make it concise.

@serenakeyitan
Copy link
Copy Markdown

🌱 gardener · 🆕 NEW_TERRITORY · severity: medium · commit: 43f35e07

What is this? repo-gardener checks whether PRs and issues fit the project's product decisions, architecture, and roadmap — not code correctness. Think of it as a product-context review layer. For code review, see Greptile/CodeRabbit.

Context fit

Context match
Area This PR Tree guidance Fit
Template registry + one-click install of pre-configured companies docs/templates/registry.json (four curated companies pointing at paperclipai/companies); Zod-validated shape in packages/shared/template-types.ts; server templateRegistry with in-memory cache; GET /api/templates/companies, POST /api/templates/companies/install, POST /api/templates/refresh; TemplatesPage + CompanyCard + Playwright smoke Company Model NODE — Company Portability: "Exportable company configs enable sharing templates ('pre-built marketing agency'), version-controlling org structure, and duplicating/forking companies." Also: import supports creating new companies, collision strategies, dry-run preview. ❓ Partial
Install path reuses the portability bundle contract templatesRouter builds a { source: { type: "github", url }, target: { mode: "new", newCompanyName }, include: [...], collision: "skip" } payload and delegates to portability.importBundle(...) Company Model NODE anticipates import via the portable package contract (markdown-first, .paperclip.yaml sidecar, collision strategies). Reusing the portability adapter preserves that contract. ✅ Aligned
Authorization model for the templates routes requireBoard on list + install; requireAdmin (instance admin) on refresh Governance NODE — Board Powers (Always Available): board has full project management access; Unified API, Different Authorization: one API surface with actor-scoped checks. Gating templates install behind board fits the board-power model. ✅ Aligned
Registry source-of-truth and trust boundary Registry JSON lives in-repo, published with the server; install fetches template bundles from arbitrary GitHub URLs via the portability service Tree is silent on where curated templates live, who can propose or refresh them, and the trust boundary when pulling arbitrary GitHub content into a Paperclip instance. 🆕 New
Cache + refresh lifecycle In-memory cache; invalidate() exposed via admin-only refresh endpoint; no server-startup refresh policy documented Tree has no decision on template-registry staleness or refresh cadence. 🆕 New
UI shell surface New /templates route; nav link added to Sidebar Company section Frontend NODE enumerates pages but does not yet include a Templates page; this is a visible product surface. 🆕 New
Tree nodes referenced

Recommendation

Why: The intent is squarely aligned with the tree: Company Portability explicitly calls out "sharing templates" and import/export as a V1 goal, and the install path reuses the portability bundle contract rather than inventing a parallel one. Where the tree is silent:

  1. Registry governance — where does the curated list live, who adds to it, what's the trust boundary for pulling bundles from arbitrary GitHub URLs (today: https://github.com/paperclipai/companies/tree/main/...)?
  2. Refresh lifecycle — in-memory cache with a board-admin refresh endpoint is a reasonable default, but future PRs will want to know whether a startup warmup, a periodic refresh, or an external CDN-backed registry is on the roadmap.
  3. Page catalog/templates is a new top-level route and nav entry; the frontend page list should mention it so other contributors don't miss it.

Suggested path forward:

  1. Add a decision record under product/company-model/ (e.g. template-gallery.md) describing: the registry shape and source of truth (docs/templates/registry.json pointing at paperclipai/companies), authorization (board to list/install, instance admin to refresh), install semantics (reuses the portability importBundle with collision: "skip" and target.mode: "new"), and the known limitation around metadata.user_id tagging that the cost-monitoring work in feat: cost-monitoring reconciliation L1+L2+L3 (subscription billing fix) #3842 also references.
  2. Cross-reference from engineering/frontend/NODE.md Page Structure and engineering/backend/NODE.md Route groups to mention /templates.
  3. Consider the trust boundary explicitly: today registry.json is co-maintained with the server binary, but tpl.url is a runtime-fetched GitHub URL. Decide whether the portability service does (or should) pin commits, verify signatures, or otherwise bound what the server can fetch.
  4. Once these are captured, this moves to ALIGNED.

Severity medium because this adds a visible product surface (install flow, new route, new admin endpoint) with a new trust/source-of-truth boundary that other contributors will want documented before extending.


Reviewed commit: 43f35e07 · Tree snapshot: ddfe6b6 · Commands: @gardener re-review · @gardener pause · @gardener ignore

🌱 Posted by repo-gardener — an open-source context-aware review bot built on First-Tree. Reviews this repo against serenakeyitan/paperclip-tree, a user-maintained context tree. Not affiliated with this project's maintainers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants