Work Template Catalog — Implementation Plan
Status: Draft v1 · Owner: Product/Eng · Date: 2026-07-18 Spec: spec.md · Tasks: tasks.md
Additive-only. Every phase layers on top of shipped surfaces. Nothing is removed or renamed except the one requested i18n display value ("Website Template" → "Template"). Each phase is independently shippable and falls back to today's behavior if the catalog is unavailable.
The precedent being mirrored is the agent-template catalog (apps/api/src/agents/agent-template-catalog.service.ts + agent-templates.controller.ts + agent-templates.server.ts, registered in agents.module.ts). Read those three files before starting each backend/web task — they are the reference implementation.
Phase 1 — Catalog service + endpoint + manifest consume
Goal: GET /api/work-templates serves sanitized, cached blueprints from ever-works/works. No UI change. Verifiable by curling the endpoint.
1.1 Repo: land manifest.json in ever-works/works
- Add
manifest.json(version: 1,generatedBy,blueprints[]) with 3 production rows —directory→ever-works/directory-web-template,directory-minimal→ever-works/directory-web-minimal-template,marketing-site→ever-works/ever-works-website-template— pluscompany/storeasstatus: "placeholder",template.repo: null. Exactly onedefault: trueperchipType. - Add
schema/works-manifest.schema.json(draft 2020-12;required: [slug, name, title, summary, kind, chipType, status, template];template.repopattern^ever-works/[a-z0-9-]+$). - Add
scripts/build-manifest.mjs+.github/workflows/validate.yml(schema-validate +default-uniqueness-per-chipTypegate), mirroring theagents/orgsrepos.
1.2 API: WorksTemplateCatalogService
- New
apps/api/src/works/works-template-catalog.service.ts, cloned fromAgentTemplateCatalogService. Copy verbatim:SAFE_SLUG_RE,stripHtml,kebabToPascal,asStringArray,MAX_*caps, the mutable-ref warning, thewarnedNoTokenone-shot log. - Diverge on the read path: primary
fetch('https://raw.githubusercontent.com/ever-works/works/<ref>/manifest.json', { headers: { 'User-Agent': 'ever-works-platform' } }); on non-2xx fall back togit.getFileContent('ever-works', 'works', 'manifest.json', { token, providerId: 'github' }, ref)withtokenfromgit.getInstallationTokenForOwner('ever-works') ?? EVER_WORKS_WORKS_TOKEN ?? GITHUB_TOKEN. list(chipType?): cache keywork-templates:<ref>(1h,cache_entriesviaCACHE_MANAGER), cache the full unfiltered list, filter bychipTypein memory. Dropplaceholder/null-repo rows and rows failingSAFE_SLUG_REor thetemplate.repopattern. Map toWorkBlueprintEntry.- Unit spec
works-template-catalog.service.spec.ts, cloned fromagent-template-catalog.service.spec.ts: no-manifest →[], malformed →[], XSS/HTML fields stripped, bad slug/repo dropped,chipTypefilter, cache hit, mutable-ref warning.
1.3 API: controller + module wiring
apps/api/src/works/work-templates.controller.ts—WorkTemplatesController,@Controller('api/work-templates'),@Get() @Public() @HttpCode(200),@Query('chipType'),@ApiTags('Works').- Register in
apps/api/src/works/works.module.ts: addWorksTemplateCatalogServicetoproviders,WorkTemplatesControllertocontrollers. ConfirmGitFacadeService+CACHE_MANAGERresolve inWorksModulescope (FacadesModule already imported transitively; add if missing).
1.4 Web: server fetch + fallback
apps/web/src/lib/api/work-templates.ts—WorkBlueprintEntrytype +listBuiltinWorkBlueprints(chipType?)(theclassic/minimalpair typed as blueprints; isomorphic, noserver-only).apps/web/src/lib/api/work-templates.server.ts(server-only) —fetchWorkTemplateCatalog(chipType?)→serverFetch<WorkBlueprintEntry[]>('/work-templates?chipType=…'), fallback tolistBuiltinWorkBlueprints. Cloneagent-templates.server.ts.
Exit: curl /api/work-templates and ?chipType=directory return the expected rows; killing the manifest returns []; unit spec green.
Phase 2 — Chips + selector + rename on Create-Work-Manually
Goal: /works/new → "Create Work Manually" shows a type-chip-filtered, searchable "Template" selector; picking one creates a Work from that blueprint.
2.1 i18n rename
- Add
dashboard.templateSelector.label="Template"anddashboard.templateSelector.searchPlaceholdertoapps/web/messages/en.jsonand every locale file. WebsiteTemplateSelector.tsx: defaultlabelreadst('label')instead of the literal'Website Template'. No prop/test-id change.
2.2 chipType lookup + filter + visibility rule
- Add the chip-value →
chipTypemap next toWORK_KIND_ORDER(and reuse on/newin P3). - Compute the filtered blueprint list for
effectiveKind; only render the selector line when the list is non-empty.
2.3 Searchable selector
- Replace the native
<Select>insideWebsiteTemplateSelector.tsxwith a shadcnCommand-in-Popovercombobox filtering onname/tags/category,featuredfirst. Preserve the "inherited default / pinned" status card, origin badge,resolveWebsiteTemplateSelection, and alldata-testids. Keepvalue= blueprintslug,onChange(value)unchanged.
2.4 Wire WorkAICreator + create mapping
/works/new/page.tsx: also fetchfetchWorkTemplateCatalog()(unfiltered) and pass tonew-work-client.tsx→WorkAICreator. Filter client-side by the selected kind.WorkAICreator: feed the chip-filtered + custom-merged (P3) list to the selector; keep bindingformData.websiteTemplateId.- Backend create mapping (§5.1–5.2): in
resolveValidatedWebsiteTemplateSelection/ the catalog service, when the id is a blueprint slug not yet intemplates, upsert it as abuilt_inrowworks-blueprint:<slug>(kindwebsite, repo coords + ref from the blueprint) sogetVisibleTemplateForUserresolves it. Pre-fill provider/organization selectors fromblueprint.defaultsinWorkAICreator(user choice + saved defaults still win viaresolveProviderDefaults).
Exit: manual create with a picked blueprint forks the right repo and stands up the Work; selector hidden for chips with zero blueprints; label reads "Template".
Phase 3 — /new chips + custom merge + search-at-scale
Goal: the unified /new page gets a second template-chip line; custom templates lead the list everywhere; the selector holds up at hundreds.
3.1 /new second chip line
NewPageClient.tsx: below the existingPromptChipsRow, add a smaller secondary template line (chips or the horizontally-scrollable strip) filtered by the selected chip'schipType, only when ≥1 blueprint. ReusefetchWorkTemplateCatalogfrom/new/page.tsx.- On submit for a work-kind chip, carry the chosen blueprint slug as
?template=<slug>intoCHIP_TO_CANVAS_ROUTEalongside?mode=ai&kind=<workKind>(reuse the existinginitialTemplateIdplumbing).
3.2 Custom-first merge
- Server-merge
[...customRows, ...repoBlueprints]deduped by id in both Create-Work surfaces; custom rows fromlistTemplatesForUser('work'|'website', userId). Group "Your templates" / "Blueprints"; preselect honoringUserTemplatePreference→ blueprintdefault→classic.
3.3 Search-at-scale hardening
- Autocomplete grouping,
featuredpinning, tag/category facets, dedupe + stable ordering for 100s of blueprints; empty/loading states.
Exit: /new template line works and hands off to /works/new; custom templates appear first; selector is usable with a large synthetic manifest.
Testing
- API unit:
works-template-catalog.service.spec.ts(clone the agent spec) — failure→[], sanitize, filter, cache, ref warning. - API e2e:
GET /api/work-templates(200 + shape +chipTypefilter +[]when unavailable), and a create-from-blueprint flow asserting theworks-blueprint:<slug>row upsert +work.websiteTemplateId. - Web e2e (Playwright, PROD-web harness): selector hidden for zero-blueprint chip; visible + searchable for
directory; custom template listed first;/newtemplate line handoff carries?template=. Environment-adaptive (catalog may be[]in CI) — assert the fallback path too. - tsc gate:
apps/web**/*.tscovers e2e; runpnpm type-checkbefore dispatch.
Rollout / flags
- No new user-facing flag required; the feature is invisible until the manifest has blueprints.
EVER_WORKS_WORKS_REFdefaults tomain— pin to a SHA/tag in prod env before enabling. - Fully backward compatible: with an empty/unreachable manifest, both Create-Work surfaces behave exactly as today (classic/minimal via the DB catalog).