---
name: maker-publish
description: How to author file artifacts for a Maker project. Read this before calling create_project or the project file write tools.
version: maker-publish-v1
---

# Maker artifact contract (v1)

This skill teaches you the file contract Maker enforces for projects. Conform to it on first generation and your call to `project file write tools` will be accepted.

## Order of operations

Tokens act on exactly one org (or the user’s personal account) at a time; tools take no `org` arg. The typical flow:

1. `read_skill({ slug: 'maker-publish' })` — read this contract first. Existing projects may predate the contract; do not infer the format from them. (Calling `read_skill({})` with no slug returns this same contract.)
2. `whoami` (or `list_organizations({})`) — find out which org this session is currently acting on. Tell the user the org name before creating anything. To switch, call `switch_organization({ org_id })` — the rebind is session-scoped (a token refresh later in the day will revert to the originally-consented org).
3. `create_project({ name?, prompt?, playbook_slug? })` — **first call must omit `confirm_org_id`**. The server replies with `{ status: "confirmation_required", active_org_id, active_org_name }`. **Stop and ask the user every single time.** Send a new message to the user that names the active organization and asks them to confirm (e.g. *"I'm about to create this project in &lt;name&gt; — should I go ahead?"*), then end your turn and wait for their reply. The user's earlier *"create a project"* request is NOT consent for this org — they cannot see which org the session is bound to until you tell them. Never auto-confirm, never echo `confirm_org_id` back inside the same turn that produced the confirmation_required response. Only after the user replies with explicit approval do you call `create_project` again with the same args plus `confirm_org_id` set to the `active_org_id` from the first response. The second call returns `{ project_id, url, scaffold, next_step }` and pre-scaffolds a hello-world placeholder. **The scaffold is a stub, not the answer.**
4. Use `create_project_file`, `replace_project_file_text`, `insert_project_file_text`, or `delete_project_file` with their operation-specific schemas. Use `read_project_file({ project_id, path, view_range? })` to read files or list directories. The scaffolded files already exist after `create_project`, so use `replace_project_file_text` to replace the placeholder body in `src/main.tsx` with what the user actually asked for; use `create_project_file` for new files (e.g. `src/components/Foo.tsx`). The merged artifact is validated against the contract before each write. **Keep iterating until the project reflects the user's request — do not stop after a single edit unless the user only asked for one tweak.**
5. Share the project link: `https://ai.maker.co/p/<project_id>` — anyone with that URL who has access to the org can preview the project in Maker.

## Editor commands

Each file write has its own tool. Shared fields match `text_editor_20250728`:

- `read_project_file({ project_id, path, view_range? })` — read a file. Pass an empty path `''` (or a directory prefix) to list files. Returns `{ content, content_hash }`.
- `create_project_file(project_id, path, file_text)` — create a new file. Fails if the path already has content.
- `replace_project_file_text(project_id, path, old_str, new_str)` — replace exactly one occurrence of `old_str`. Fails on 0 or >1 matches; pass enough surrounding context to make the match unique.
- `insert_project_file_text(project_id, path, insert_line, insert_text)` — insert text after the given 0-indexed line (0 = top of file).
- `delete_project_file(project_id, path)` — remove a file from the project.

**Optimistic concurrency.** `read_project_file` returns a `content_hash`. Pass it back as `expected_hash` on the next mutation to detect concurrent edits (Maker UI, another agent, another device). Mismatch returns `{ status: 'rejected', code: 'stale', current_hash }`; re-read with `read_project_file` and retry. Pass no `expected_hash` to skip the check (best-effort write).

**Change summary.** Every mutation (`create`, `str_replace`, `insert`, `delete`) takes an additional `change_summary: string` — a single sentence describing what this edit accomplishes for the human reading the project chat (not a restatement of the file path or command). Examples: `"Add price filter to ProductGrid props"`, `"Fix off-by-one in pagination"`. Always pass it on mutations; the editor chat surfaces it so the user can follow your run. Omitting it is accepted by the server (for backwards compatibility) but leaves the user without context for that edit. `read_project_file` does not take `change_summary`.

## Other skills — load them proactively

This contract is one of several skills Maker exposes. Most non-trivial tasks need at least one more skill loaded **before** you start writing files — don’t wait for the user to name a skill, scan the catalog and load whatever fits.

**Default opening move on any non-trivial build:**
1. Call `list_skills({})` once at the start of the task. The response is `[{ slug, name, description, source, has_references }]` — read each `description` and decide which skills apply to the user's request.
2. For any **multi-step build** (a full page, a multi-section site, anything beyond a single tweak), call `read_skill({ slug: 'planning' })` first and follow its structured planning workflow — produces a clearer scope, fewer follow-ups, and a shareable plan the user can redirect before code is written.
3. For any task with a clear specialist angle, read the matching skill before generating code. Common examples (use whatever shows up in your `list_skills({})` call): SEO audit / on-page SEO work → SEO skill; copy improvements, headlines, CTAs → copywriting skill; conversion-rate work, button placement, pricing UX → CRO skill; importing a design from Figma / Claude Design (or any static HTML page that needs to become a React app) → `read_skill({ slug: 'claude-design-import' })`; pre-publish review → `security-audit` + `accessibility` + `web-quality-audit`.
4. **Heuristic:** if a user-authored skill in this org sounds anywhere close to the task, read it. Org-authored skills usually encode internal preferences the user expects you to follow without being asked.

Load any skill with `read_skill({ slug })`. Resource files inside a skill folder (referenced when `has_references: true`) are accessible via `read_skill_resource({ slug, path })`. MCP-spec-conformant clients can also discover skills through `resources/list` (URI scheme `maker-skill://<slug>`); tools and resources resolve to the same underlying content.

## Project URL

Every project has a stable URL of the form `https://ai.maker.co/p/<project_id>`. After `create_project` returns a `project_id`, surface this URL to the user (they can open it in Maker to preview, edit, and eventually publish). When the user asks for "the link" or "the URL" to a project, give them this.

## Minimum viable artifact

The smallest file set that passes the validator. Copy this and modify.

### index.html
```html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Preview</title>
  <script src="https://cdn.tailwindcss.com/3.4.16"></script>

  <!--
    NEVER REMOVE THIS COMMENT!
    Don't create separate .css files, use tailwind configuration here instead
    -->
  <!-- start: design-system -->
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
  <script>
    tailwind.config = {
      theme: {
        extend: {}
      }
    }
  </script>
  <style type="text/tailwindcss"></style>
  <!-- end: design-system -->

  <!--
    NEVER REMOVE THIS COMMENT!
    IMPORTANT:Instead of installing dependencies, update the importmap.
    When using react-router-dom, always use only the following URL: https://esm.sh/react-router-dom@6?deps=react@18
  -->
  <script type="importmap">
    {
      "imports": {
        "react": "https://esm.sh/react@18",
        "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime",
        "react-dom/client": "https://esm.sh/react-dom@18/client"  
      }
    }
  </script>

</head>

<body>
  <div id="root"></div>

  <script type="module" src="/src/main.tsx"></script>
</body>

</html>

```
### src/main.tsx
```tsx
import React from 'react';
import { createRoot } from 'react-dom/client';

export default function App() {
  return (
    <div className="flex justify-center items-center h-screen">
      {/* REMOVE ME! */}
      <h1>Your Maker site will show up here!</h1>
    </div>
  );
}

const root = createRoot(document.getElementById('root')!);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

The entry point is **`src/main.tsx`**, referenced from `index.html` via `<script type="module" src="/src/main.tsx">`. The **publishable artifact** lives at `index.html`, `src/`, `public/`, or `assets/` — those are the files that ship. Other paths (`context/`, `original_project/`, `user-context/`, `system-context/`) are reachable but are not part of the published app; see the **Path constraints** section.

The page renders **through React**. `index.html`'s `<body>` must contain the mount node `<div id="root"></div>` that matches the `document.getElementById('root')` call in `src/main.tsx` — if that element is missing, publish is **rejected** (`root-mount-missing`). All visible markup belongs in `src/App.tsx` (rendered by `main.tsx`), not pasted directly into the `<body>`; the `<body>` should be just the mount node plus `<script>` tags. Leaving static `<header>/<section>` markup in the `<body>` raises a **warning** (`static-body-outside-root`) — publish still succeeds, but it signals the page isn't really a React app and won't be editable in Maker. A `<body>` full of static markup with a `main.tsx` that returns `null` is the classic broken import.

## Converting a static HTML page / Claude Design export

If the project currently holds a **static HTML page** — all markup sits in `index.html`'s `<body>`, `src/main.tsx` returns `null`, or there is no `<div id="root">` — it will not render through React and `publish_project` will be rejected with `root-mount-missing`. This happens when a design is brought in outside the Maker UI (e.g. fetched from the Anthropic design API and dumped into `index.html`).
Do **not** try to fix it by hand-editing the body. Load the dedicated skill and follow it: `read_skill({ slug: 'claude-design-import' })`. It converts the page into the correct shape — restores `<div id="root"></div>` in `index.html`, moves the `<body>` markup into a single `src/App.tsx` (with JSX conversions), and wires `src/main.tsx` to render `App`. Run this **before** publishing.

## Path constraints

- Any project file is reachable: `index.html`, `src/`, `public/`, `assets/`, `context/` (project notes & tasks.json), `original_project/` (makeover clone source), and any other project folder
- `user-context/` — this org’s cross-project memory (read + write). Not part of the publishable artifact.
- `system-context/` — platform files (read-only; writes are rejected). Not part of the publishable artifact.
- Build internals are rejected: `package.json`, `tsconfig.json`, `.env*`, `node_modules/`, `dist/`, `build/`
- No absolute paths (leading `/`), `..`, backslashes, spaces, or control characters

## Source files vs. assets

Two storage surfaces exist; they are not interchangeable.

- **Project tree** (via `project file write tools`) — everything the project itself renders: `index.html`, source under `src/`, static files under `public/`, bundled assets under `assets/`. These get versioned, validated against this contract, and rendered live in Maker’s visual editor. **All HTML, JS/TS, JSX/TSX, CSS, and config the browser executes belongs here.**
- **Asset storage** (via `upload_asset` for a public URL, or `create_asset_upload({ project_id, filename, mime_type, size_bytes })` for local bytes) — media and reference documents your source code links to: images, video, audio, PDFs, downloadable docs. Returns a public S3 URL you reference from a `<img src>`, `<video src>`, `<a href>`, fetch call, etc.

Anti-pattern to avoid: uploading the project's `index.html` (or its JS bundles) as an asset and then shipping a thin `index.html` in the project tree that iframes the S3 URL. This works mechanically but breaks the visual editor, locks the project out of further edits (every change requires re-uploading new asset URLs), and adds an iframe round-trip to every page load. If you find yourself reaching for `upload_asset` to host the page the user actually sees, you want `project file write tools` instead.

## Rules

## index-html-required

**rule_id:** `maker-publish/v1/index-html-required` (severity: `error`)

index.html is required at the project root.

## index-html-script-reference

**rule_id:** `maker-publish/v1/index-html-script-reference` (severity: `error`)

index.html must contain a <script type="module" src="..."> referencing the entry file.

## entry-missing

**rule_id:** `maker-publish/v1/entry-missing` (severity: `error`)

The entry file referenced by index.html must exist in the artifact.

## entry-default-export

**rule_id:** `maker-publish/v1/entry-default-export` (severity: `error`)

The entry file must have a default export.

## root-mount-missing

**rule_id:** `maker-publish/v1/root-mount-missing` (severity: `error`)

index.html must contain the element the entry file mounts React into (e.g. <div id="root"></div>).

## static-body-outside-root

**rule_id:** `maker-publish/v1/static-body-outside-root` (severity: `warning`)

index.html's <body> should be just the React mount node plus scripts — visible markup belongs in src/App.tsx, not the body.

## importmap-missing

**rule_id:** `maker-publish/v1/importmap-missing` (severity: `error`)

index.html must declare an importmap with React 18 imports.

## no-build-artifacts

**rule_id:** `maker-publish/v1/no-build-artifacts` (severity: `error`)

index.html must not reference local build artifacts.

## tailwind-cdn

**rule_id:** `maker-publish/v1/tailwind-cdn` (severity: `warning`)

index.html should load Tailwind from the official CDN.

## nbsp-in-class

**rule_id:** `maker-publish/v1/nbsp-in-class` (severity: `warning`)

class/className attributes must separate names with regular spaces, not &nbsp; (a non-breaking space fuses class names into one invalid token so the styles never apply).

## inline-script

**rule_id:** `maker-publish/v1/inline-script` (severity: `warning`)

express behavior in React (src/App.tsx), not imperative inline <script> blocks in index.html.

## separate-css

**rule_id:** `maker-publish/v1/separate-css` (severity: `warning`)

fold CSS into the <style type="text/tailwindcss"> design-system block — do not ship separate .css files or link local stylesheets.

## prefer-tailwind-utilities

**rule_id:** `maker-publish/v1/prefer-tailwind-utilities` (severity: `warning`)

prefer Tailwind utility classes over a large bespoke CSS class system in the <style> block.

## ts-nocheck

**rule_id:** `maker-publish/v1/ts-nocheck` (severity: `warning`)

do not disable type checking with // @ts-nocheck — fix the types instead.

## unscoped-selector

**rule_id:** `maker-publish/v1/unscoped-selector` (severity: `warning`)

:unscoped(...) opts a CSS selector out of project scoping — confirm this is deliberate before it ships.

## per-file-size

**rule_id:** `maker-publish/v1/per-file-size` (severity: `warning`)

Each file should be smaller than 200000 bytes.

## total-size

**rule_id:** `maker-publish/v1/total-size` (severity: `warning`)

Total artifact size should be smaller than 500000 bytes.

## tracking-id-duplicate-in-loop

**rule_id:** `maker-publish/v1/tracking-id-duplicate-in-loop` (severity: `error`)

data-tracking-id must not be a literal string inside a loop/.map() callback.

## tracking-id-duplicate-cross-component

**rule_id:** `maker-publish/v1/tracking-id-duplicate-cross-component` (severity: `error`)

data-tracking-id must not be a literal string on a component invoked from multiple call sites.

## tracking-id-literal-collision

**rule_id:** `maker-publish/v1/tracking-id-literal-collision` (severity: `error`)

data-tracking-id must not be hard-coded identically in two unrelated places.

## tracking-id-dynamic-prop-collision

**rule_id:** `maker-publish/v1/tracking-id-dynamic-prop-collision` (severity: `error`)

data-tracking-id template literal must not be keyed on a non-unique prop.

## tracking-id-dynamic-fallback-collision

**rule_id:** `maker-publish/v1/tracking-id-dynamic-fallback-collision` (severity: `warning`)

data-tracking-id template literal should not fall back to a shared literal.

## Operational errors

Diagnostics raised outside the file-content rules above. They aren’t fixed by editing the artifact — they’re state checks against your token, the project, or the request payload.

## path-rejected

A file path failed `assertSafeArtifactPath`. See the **Path constraints** section above. Common causes: a build internal (`package.json`, `package-lock.json`, `tsconfig.json`, `yarn.lock`, `node_modules/`, `dist/`, `build/`, `.env*`); the reserved `template-<id>/…` prefix; leading `/` or `..`; backslashes; spaces; control characters. (A `system-context/` path is not rejected here — it is accepted read-only, and a *write* to it fails at execute time instead.)

## project-not-found

The `project_id` you passed does not exist (or is soft-deleted). Call `list_projects` to see valid ids in the bound org, or `create_project` to make a new one.

## forbidden

The project belongs to a different organization than the session is currently acting on. Use `list_organizations({})` to see your memberships, then `switch_organization({ org_id })` to rebind this session to the right org and retry. (Re-running OAuth is also valid but unnecessary — the switch action re-checks Clerk membership and the billing gate.)

## billing-required

Publishing is not available under the bound organization’s current entitlement. Explain that the requested operation is unavailable.

## importmap-invalid-json

The `<script type="importmap">` block in `index.html` is not parseable as JSON. Check for stray commas, unquoted keys, or unescaped quotes. See **importmap-missing** for the required imports.

## Versioning

This contract is versioned as `maker-publish/v1`. Rule ids include the version, so future changes ship as `v2`, etc., without breaking existing integrations.
