Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .claude/skills/create-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
name: create-pr
description: Generate a pull request title and description from the current branch's commits. Produces a concise summary, optional feature highlights, and collapsible technical details.
---

# Create PR Description

Generate a pull request title and description that's scannable, informative, and has just enough personality to feel human.

## Instructions

### 1. Gather context (do ALL of these)

Run these commands to build a complete picture before writing anything:

```bash
# Commit overview
git log main..HEAD --oneline --stat

# Full diff stat for file-level scope
git diff main..HEAD --stat

# Actual code changes — read the diff, don't just skim filenames
git diff main..HEAD
```

If the full diff is too large, diff individual areas (backend routes, frontend, storage, etc.) in batches. You must understand **what the code actually does**, not just which files were touched.

### 2. Write the PR file

Write the file to `.pr/YYYY-MM-DD.md` (using today's date). Create the `.pr/` directory if it doesn't exist. If a file for today's date already exists, append a counter: `YYYY-MM-DD-2.md`, `YYYY-MM-DD-3.md`, etc.

The structure depends on whether the PR introduces user-facing features or is purely internal (refactors, bug fixes, infra).

#### When the PR has user-facing features:

~~~markdown
# <Title>

<2-3 sentence summary>

### Highlights

- Highlight 1
- Highlight 2
- ...

<details>
<summary>Technical changes</summary>

- Detail 1
- Detail 2
- ...

</details>
~~~

#### When the PR is purely internal (no user-facing features):

~~~markdown
# <Title>

<2-3 sentence summary>

<details>
<summary>Technical changes</summary>

- Detail 1
- Detail 2
- ...

</details>
~~~

Omit the Highlights section entirely for internal-only PRs — don't force it.

### Style Rules

#### Title
- Imperative mood, start with a verb (Add, Fix, Refactor, etc.)
- Summarize the entire PR scope — not just one commit

#### Summary
- **2-3 sentences max.** This is the elevator pitch, not the full story.
- **Open with a touch of personality.** One line that makes the reader smile — a wry observation, a lighthearted remark, a playful metaphor. Not forced, just human. Examples of the energy (don't copy these literally, invent your own each time):
- "This one's mostly about cleaning house."
- "Turns out the type checker was right to complain."
- A playful metaphor about what the code was doing wrong
- A wry observation about the state of things before this PR
- **Then say what the PR does at a high level.** Name the main change areas (new feature, refactor target, bug fixed) but don't enumerate every file. The personality is in *how* you describe the changes, not in being vague.
- **Do not repeat what Highlights or Technical changes already cover.** The summary is the "why" and the big picture; details live below.

#### Highlights (only when applicable)
- One bullet per user-facing feature, behavior change, or notable improvement.
- Write from the user's perspective — what they'll notice, not internal implementation.
- Plain language, no code references. "Schedules now respect your configured timezone" not "`SchedulerService` gains a `timezone` attribute".
- 3-7 bullets is the sweet spot. If you can only think of 1-2, fold them into the summary and skip this section.

#### Technical changes (inside the accordion)
- One bullet per discrete change. Be specific — name files, classes, functions, patterns.
- Format: `backtick code references` for identifiers, plain text for descriptions.
- Every meaningful change in the diff must have a bullet. If a change touches security (CORS, auth, SQL injection), error handling, accessibility, or concurrency, it gets its own bullet — do not bury these.
- Bullets should describe the mechanism, not just the intent. "Race condition in `get_or_create_chat` fixed by moving creation inside the lookup session" is good. "Fix database issues" is not.
- Group related changes together (all typing fixes, all security hardening, all API changes, etc.)

#### General
- **No test plan section.** Do not include "Test plan" or "Testing".
- **No mention of tests.** Do not reference test files, test results, or testing.
- **No emoji.**
- **No "Generated by" footer.**
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ frontend/node_modules/
.marketing
prompture_cost_tracking.md
prompture_tracking_migration.md
/.pr
122 changes: 122 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Under the hood, the pipeline enforces **quality gates**. The Reviewer agent scor
- [Features](#features)
- [CLI Reference](#cli-reference)
- [Web UI](#web-ui)
- [Embeddable Component](#embeddable-component)
- [Configuration](#configuration)
- [Project Structure](#project-structure)
- [Tech Stack](#tech-stack)
Expand Down Expand Up @@ -181,6 +182,126 @@ cd frontend && npm run dev

---

## Embeddable Component

Use AgentSite as a library inside any Python application — no server, database, or frontend required. Two async functions expose the full pipeline:

```python
from agentsite import generate_website, regenerate_page, GenerationConfig

# Generate a site from a prompt
result = await generate_website(
"A dark portfolio site with projects and contact page",
output_dir=Path("./websites"),
config=GenerationConfig(
model="openai/gpt-4o",
provider_keys={"openai": os.environ["OPENAI_API_KEY"]},
max_cost=0.50,
),
on_event=lambda e: print(f"{e.agent}: {e.type}"),
)

for path, html in result.files_content.items():
print(f"{path}: {len(html)} bytes")

# Iterate on the same project with new feedback
v2 = await regenerate_page(
"Make the hero section taller and add a testimonials page",
output_dir=Path("./websites"),
project_id=result.project_id,
config=GenerationConfig(model="openai/gpt-4o"),
)
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The embeddable API example uses Path and os.environ but doesn't import Path or os, and uses await at top-level (which will error outside an async context). Adding the missing imports and indicating that this runs inside async def (or showing asyncio.run(...)) would make the snippet copy/pasteable.

Suggested change
from agentsite import generate_website, regenerate_page, GenerationConfig
# Generate a site from a prompt
result = await generate_website(
"A dark portfolio site with projects and contact page",
output_dir=Path("./websites"),
config=GenerationConfig(
model="openai/gpt-4o",
provider_keys={"openai": os.environ["OPENAI_API_KEY"]},
max_cost=0.50,
),
on_event=lambda e: print(f"{e.agent}: {e.type}"),
)
for path, html in result.files_content.items():
print(f"{path}: {len(html)} bytes")
# Iterate on the same project with new feedback
v2 = await regenerate_page(
"Make the hero section taller and add a testimonials page",
output_dir=Path("./websites"),
project_id=result.project_id,
config=GenerationConfig(model="openai/gpt-4o"),
)
import os
import asyncio
from pathlib import Path
from agentsite import generate_website, regenerate_page, GenerationConfig
async def main() -> None:
# Generate a site from a prompt
result = await generate_website(
"A dark portfolio site with projects and contact page",
output_dir=Path("./websites"),
config=GenerationConfig(
model="openai/gpt-4o",
provider_keys={"openai": os.environ["OPENAI_API_KEY"]},
max_cost=0.50,
),
on_event=lambda e: print(f"{e.agent}: {e.type}"),
)
for path, html in result.files_content.items():
print(f"{path}: {len(html)} bytes")
# Iterate on the same project with new feedback
v2 = await regenerate_page(
"Make the hero section taller and add a testimonials page",
output_dir=Path("./websites"),
project_id=result.project_id,
config=GenerationConfig(model="openai/gpt-4o"),
)
if __name__ == "__main__":
asyncio.run(main())

Copilot uses AI. Check for mistakes.
```

### API

| Function | Description |
| --- | --- |
| `generate_website(prompt, *, output_dir, config, on_event, project_name, slug)` | One-shot generation. Creates a project, runs the full pipeline, writes files to `output_dir`. |
| `regenerate_page(prompt, *, output_dir, project_id, slug, version, config, on_event)` | Iterate on an existing project. Auto-detects next version number and preserves the StyleSpec from prior runs. |
| `load_project(output_dir, project_id)` | Restore a project's full state from disk — metadata, conversation history, site plan, and latest page files. Returns `None` if not found. |

### GenerationConfig

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `model` | `str` | `"openai/gpt-4o"` | LLM model to use |
| `max_cost` | `float \| None` | `None` | Budget cap in USD |
| `budget_policy` | `str \| None` | `None` | Budget enforcement policy |
| `provider_keys` | `dict[str, str] \| None` | `None` | API keys per provider |
| `agent_configs` | `dict[str, AgentConfig] \| None` | `None` | Per-agent overrides |
| `style_spec` | `StyleSpec \| None` | `None` | Pre-defined design tokens |
| `logo_url` | `str` | `""` | Logo URL for the site |
| `icon_url` | `str` | `""` | Favicon URL |
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

GenerationConfig gained new fields (max_review_iterations, review_threshold, cancel_event, conversation_context) but the README table doesn't list them. Please document these fields (and defaults/behavior) so embedders know how to enable review gating and cancellation/context features.

Suggested change
| `icon_url` | `str` | `""` | Favicon URL |
| `icon_url` | `str` | `""` | Favicon URL |
| `max_review_iterations` | `int` | `0` | Maximum number of automated review/fix cycles per page. `0` disables review gating and accepts the first draft. |
| `review_threshold` | `float` | `0.0` | Minimum review score (0.0–1.0) required to accept a page when review gating is enabled. Only used when `max_review_iterations > 0`. |
| `cancel_event` | `Any \| None` | `None` | Optional cooperative-cancellation flag (e.g. a `threading.Event`). When set during generation, the run aborts as soon as possible. |
| `conversation_context` | `dict[str, Any] \| None` | `None` | Extra context injected into all agent prompts (e.g. user/session/site metadata). Must be JSON-serializable. |

Copilot uses AI. Check for mistakes.

### GenerationResult

| Field | Type | Description |
| --- | --- | --- |
| `project_id` | `str` | Unique project identifier |
| `files` | `list[str]` | List of generated file paths |
| `files_content` | `dict[str, str]` | File path → content mapping |
| `output_dir` | `Path` | Directory where files were written |
| `usage` | `dict` | Aggregate token/cost usage |
| `agent_runs` | `list[dict]` | Per-agent run data |
| `style_spec` | `StyleSpec \| None` | Parsed design spec (auto-saved for reuse) |
| `success` | `bool` | Whether generation completed |
| `error` | `str \| None` | Error message if failed |

### ProjectState

| Field | Type | Description |
| --- | --- | --- |
| `project_id` | `str` | Unique project identifier |
| `name` | `str` | Project name |
| `model` | `str` | LLM model used |
| `style_spec` | `StyleSpec \| None` | Design tokens from the Designer agent |
| `site_plan_raw` | `str` | Raw site plan JSON |
| `pages` | `list[PageState]` | Latest version of each page with files |
| `messages` | `list[ConversationMessage]` | Full conversation history |

### ConversationMessage

| Field | Type | Description |
| --- | --- | --- |
| `role` | `str` | `"user"` or `"assistant"` |
| `content` | `str` | Human-readable message text |
| `timestamp` | `str` | ISO 8601 UTC timestamp |
| `meta` | `dict` | Structured data (slug, version, files, action, etc.) |

### Conversation Persistence

Prompts and agent responses are auto-persisted to `messages.json` on disk. Use `load_project` to restore the full conversation thread days later:

```python
from agentsite import generate_website, load_project, GenerationConfig

# Day 1: generate a site
result = await generate_website(
"A dark portfolio site",
output_dir=Path("./websites"),
config=GenerationConfig(model="openai/gpt-4o"),
)
project_id = result.project_id # save this

# Day 4: restore everything and continue
state = load_project(Path("./websites"), project_id)
print(state.messages) # full conversation history
print(state.pages) # latest files per page
print(state.style_spec) # design tokens ready to reuse
```

### Design Notes

- **No database** — files and metadata live on disk via `ProjectManager`
- **No server** — direct async function calls, runs in-process
- **StyleSpec auto-persisted** — after generation, the designer's output is saved to `project.json` so `regenerate_page` picks up the brand
- **Error recovery** — budget exceeded and pipeline failures still return partial files if any were written
- **Conversation auto-persisted** — user prompts and agent responses are saved to `messages.json` for session restoration via `load_project`
- **Sync/async callbacks** — `on_event` accepts either sync or async functions

---

## Configuration

| Variable | Description | Default |
Expand Down Expand Up @@ -208,6 +329,7 @@ agentsite/
websocket.py # WebSocket manager for real-time progress
engine/ # Core generation logic
pipeline.py # Orchestrates agents, handles file output and events
component.py # Embeddable API (generate_website, regenerate_page)
storage/ # Persistence layer
database.py # Async SQLite via aiosqlite
repository.py # CRUD operations for projects and generations
Expand Down
17 changes: 16 additions & 1 deletion agentsite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,28 @@

from .api.app import create_app
from .config import settings
from .engine.component import GenerationConfig, GenerationResult, generate_website, regenerate_page
from .engine.component import (
ConversationMessage,
GenerationConfig,
GenerationResult,
PageState,
ProjectState,
delete_project,
generate_website,
load_project,
regenerate_page,
)

__all__ = [
"ConversationMessage",
"GenerationConfig",
"GenerationResult",
"PageState",
"ProjectState",
"create_app",
"delete_project",
"generate_website",
"load_project",
"regenerate_page",
"settings",
]
17 changes: 16 additions & 1 deletion agentsite/engine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
"""AgentSite engine — pipeline execution, project management, and assets."""

from .asset_handler import AssetHandler
from .component import GenerationConfig, GenerationResult, generate_website, regenerate_page
from .component import (
ConversationMessage,
GenerationConfig,
GenerationResult,
PageState,
ProjectState,
delete_project,
generate_website,
load_project,
regenerate_page,
)
from .pipeline import GenerationPipeline
from .project_manager import ProjectManager

__all__ = [
"AssetHandler",
"ConversationMessage",
"GenerationConfig",
"GenerationPipeline",
"GenerationResult",
"PageState",
"ProjectManager",
"ProjectState",
"delete_project",
"generate_website",
"load_project",
"regenerate_page",
]
Loading