# Project export: SiteKapında

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Built in Türkiye, SiteKapinda uses Codex agents and GPT Image to find businesses without websites and bring them a tailored first version before the sales pitch. Humans approve every launch.
- Devpost: https://devpost.com/software/sitekapinda
- GitHub: https://github.com/haakanergun/sitekapinda
- Video: https://www.youtube.com/embed/ktev7Dng_jk?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — haakanergun (1 commits)

## Devpost submission (written by the team)

### Inspiration

Many local businesses in Türkiye still depend on social media, messaging apps, or map listings because building a professional website can feel expensive, technical, and risky. But these businesses do not begin with a website brief. They begin with a harder question: Who can I trust to build it? SiteKapında means “your website at your doorstep” in Turkish. We reversed the usual website sales process: Do not sell the promise. Bring the first version. Our goal is to let a business see something tailored and tangible before committing—while keeping the owner in control of every decision that can affect their brand, money, or public presence.

### What it does

SiteKapında is a human-supervised workflow that discovers businesses without strong websites, creates a business-specific first version, manages the sales conversation, and carries an approved website toward launch. The end-to-end flow has eight stages: Discover Account-local Codex automation experiments, configured with GPT-5.6 Sol and an hourly schedule, inspect permitted public sources. They verify the missing-website signal, collect public business contact channels, reject low-confidence cases, and save only evidence-backed opportunities. Create a tailored first version with GPT Image Business facts and image rights are handled separately. When visuals are licensed, owner-supplied, synthetic, or explicitly authorized, GPT Image can preserve the real venue or products while rebuilding the imagery for a professional website. The project demonstrates distinct directions for restaurants, beauty businesses, dental practices, cafés, automotive services, fitness studios, florists, pet groomers, education providers, and other local-business categories. Save everything in the Sales Ops panel Qualified leads enter the SiteKapında panel with: Source and qualification context Public contact channels Desktop and mobile website concepts Opportunity score and rationale Sales notes and next actions Contacted, Interested, Not interested, Approved, and Do not contact states The salesperson sees the reason to believe, the proposed first version, and the current relationship state in one place. Contact transparently A human sales operator reviews the lead and the proposed design before contacting the business. Concepts remain private, are never presented as an official website, and are never published automatically. The owner can decline, request no further contact, express interest, or continue to a working demo. Show a working demo through Codex Sites When a lead is interested, the demonstrated Codex Sites workflow produces a live private demo—not the final public website. The customer opens a Sites link and can request specific changes to: Copy Imagery Layout Services Calls to action Mobile presentation Revise and approve Codex continues from the same customer context until the owner explicitly approves the version that may be released. Human approval is required before publication. The workflow does not treat silence or initial interest as permission to launch. Register the domain and go live After the Sites version is approved, the demonstrated release workflow preserves that exact version. With explicit purchase confirmation, a Cloudflare integration can check and register the custom domain and apply the required DNS records. Domain purchase, DNS changes, and deployment are separate, human-authorized actions and are disabled in the judge quick-start. Keep a persistent customer developer Each customer can retain a persistent Codex task containing the website’s decisions and development history. The same context can support later bug fixes, copy changes, new sections, and product improvements instead of starting from zero with a new developer. In the future, this context can be packaged as a customer-owned Codex skill so the business owner can continue improving the website with their own agent.

### How we built it

We built SiteKapında as two connected layers. 1. A portable, judge-ready application core The submitted repository contains a deterministic Python and SQLite pipeline for: Candidate normalization Deduplication Compliance and category rejection Website-strength classification Opportunity scoring Lead persistence Sales lifecycle states Reports Sales Ops workspace generation The default judge path uses twelve fictional businesses and makes no network requests. It does not require an OpenAI API key, Google API key, Cloudflare credentials, or production customer records. The repository includes: Twelve fictional business examples Twenty-four GPT Image website UI mockups Twelve independent desktop designs Twelve independently composed mobile designs A searchable English Sales Ops panel Nineteen offline tests A one-command bootstrap Five reusable SiteKapında Codex workflow skills 2. A Codex-assisted production workflow During Build Week, GPT-5.6-class models in Codex—primarily the gpt-5.6-sol profile—helped us: Research and understand the original project Improve discovery-agent prompts Strengthen rejection and qualification rules Add image-rights safeguards Create distinct design directions Develop and test the Sales Ops interface Build responsive website examples Review safety and release gates Produce the Remotion demonstration video Package the project as a clean GitHub repository for judges The demonstrated discovery configurations use: model = "gpt-5.6-sol" schedule = "hourly" reasoning = "xhigh" source_policy = "permitted_public" These scheduled Codex experiments remain paused in the submitted package. The offline Python application does not hide an autonomous GPT-5.6 API runtime. GPT Image was used during the authoring workflow to create rights-safe, business-specific visual directions. The offline runtime presents the resulting local assets but does not regenerate them. Codex Sites demonstrates the private review stage through a working hosted example: Open the Codex Sites demo Cloudflare domain registration and DNS deployment are represented as separately authorized release steps rather than automatic judge-path actions. Challenges we faced The hardest challenge was not generating a website. It was building an agentic workflow that remains useful without becoming misleading or unsafe. Consent and identity A generated concept must never be presented as the official website of a business. Private concepts, outreach, approval, purchasing, and publication require different controls. Image and brand rights A public image is not automatically reusable. We separated business facts from image rights and restricted the visual workflow to synthetic, licensed, owner-supplied, or explicitly authorized material. Preventing invented claims Website designs can easily introduce fictional reviews, awards, prices, statistics, or guarantees. Our prompts and review rules explicitly reject those additions. Combining many workflows Discovery, qualification, image generation, sales operations, responsive development, revisions, domain registration, and maintenance all require different tools and approval boundaries. Portability Codex tasks and schedules belong to the creator’s environment. We therefore packaged the transferable parts as: AGENTS.md repository guidance Reusable Codex skills Versioned prompts Transparent automation examples An offline deterministic runtime Judges can clone the repository, open it in Codex, and reproduce the safe synthetic workflow without our credentials or account state. Accomplishments that we are proud of We are proud that SiteKapında became more than a concept deck. We built a working Sales Ops product, a reproducible discovery and qualification pipeline, twelve sector-specific examples, twenty-four desktop/mobile GPT Image designs, a Codex Sites review experience, reusable Codex skills, and a GitHub-ready judge package. We also tested the central business assumption manually. In a founder-reported pilot of ten businesses: Four liked the design direction, but the conversations stalled Five said they were not interested One became the first paying customer The panel preserves all three outcomes—Contacted, Interested, and Not interested—because failed conversations are product evidence, not vanity-metric failures. Çağrı Karakaş became the first customer implementation: a polished, responsive personal-training website running on its own domain. Visit cagrikarakas.com This was important proof that the workflow could move beyond a generated concept and become a real customer website.

### What we learned

We learned that the most useful AI system is not always the one with the fewest humans. For business identity, communication, payments, domains, and publication, the stronger design is: Agents do the repetitive work. Humans make the consequential decisions. We also learned that showing a tailored first version creates a very different conversation from selling an abstract website service. The visual concept helps the owner react to something concrete. Codex Sites turns that reaction into revision requests. Persistent Codex history then transforms a one-time website build into an ongoing developer relationship. Finally, we learned that negative outcomes matter. Recording why a lead declined or why a conversation stalled helps improve discovery, qualification, design direction, and outreach.

### What's next

Our next steps are to: Harden the paused Codex automation experiments into monitored production workflows Add auditable consent and image-rights records Connect approved CRM and messaging channels Build a secure customer review and approval portal Expand industry-specific design systems Support Turkish and English website generation Improve qualification using real sales outcomes Add owner-approved domain, analytics, SEO, and maintenance workflows Preserve customer context across long-term fixes and new features Package customer histories as owner-controlled Codex skills Expand from Türkiye market by market Our long-term vision is not only to help local businesses get websites. It is to show how Codex agents, GPT Image, Sites, and deployment extensions can work together as a human-controlled digital production team. Discover the need. Show the value. Earn the trust. Then launch. Built in Türkiye. Designed to expand market by market.

## README (from the GitHub repository)

# SiteKapında — OpenAI Build Week submission

**A first working website arrives before the sales pitch.**

SiteKapında is an AI-assisted, human-approved website production workflow for local businesses that do not yet have a strong website. It discovers a qualified opportunity, produces a private first version, gives a salesperson something tangible to review with the owner, and keeps publication behind an explicit human approval gate.

This repository is the safe, judge-ready edition of the project. Its default path uses fictional businesses, requires no API key, makes no network request, and does not contact or publish anything.

## See it first

| Artifact | Link |
|---|---|
| 2:40 product video | [Watch on YouTube](https://youtu.be/ktev7Dng_jk) |
| Codex Sites demo | [Open the private-demo example](https://sedirra-sites-demo.haakanergun.chatgpt.site) |
| SiteKapında product site | [sitekapinda.com](https://sitekapinda.com) |
| Customer implementation example | [cagrikarakas.com](https://cagrikarakas.com) |

The Codex Sites demo is a hosted companion artifact. The Çağrı Karakaş link is a separate customer implementation example; neither is generated by the synthetic judge run below.

## Clone and open in Codex

1. Clone this GitHub repository, or use GitHub's **Code → Download ZIP** and extract it normally.
2. In the Codex desktop app, IDE extension, or CLI, open the repository root—the folder that directly contains `AGENTS.md`, `README.md`, `pyproject.toml`, and `src/`.
3. Start a new task from that root. No plugin installation is required for the default path; Codex automatically receives the checked-in `AGENTS.md` repository guidance.
4. Send the prompt below. Codex can run the platform-specific bootstrap, verify the tests, and show the generated Sales Ops workspace and one synthetic preview.

```text
Read README.md and AGENTS.md. Run the safe synthetic judge path, verify all
tests, and show me the generated English Sales Ops workspace plus one preview.
Do not enable real discovery, contact anyone, upload data, or publish anything.
```

Expected local outputs are `runtime/generated/index.html`, `runtime/generated/mockups/`, `runtime/generated/demos/`, `runtime/reports/`, and `runtime/sitekapinda.sqlite3`. Installing the bundled SiteKapında plugin is optional; it adds five named reusable workflows but is not required to run or judge the repository.

## Five-minute judge path

Prerequisite: Python 3.10 or newer. Node, Docker, an OpenAI API key, and a Google API key are not required.

Windows PowerShell:

```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1
```

macOS or Linux:

```bash
bash ./scripts/bootstrap.sh
```

The bootstrap is idempotent. It creates an isolated `.venv`, writes a local `.env` only when one does not already exist, runs the environment doctor, runs the unit tests, and executes one synthetic discovery cycle. It does not install third-party Python packages or use the network.

After it finishes, inspect:

- `runtime/generated/index.html` — the English Sales Ops master-detail workspace, populated only from the local synthetic pipeline
- `runtime/generated/mockups/` — twenty-four full website UI mockups produced with GPT Image: a separately composed desktop and mobile design for each fictional business
- `runtime/generated/demos/` — supplementary deterministic HTML preview packages used to demonstrate the downstream build stage; the Sales Ops workspace deliberately presents the GPT Image mockups instead
- `runtime/reports/` — JSON, Markdown, CSV, and lead reports
- `runtime/sitekapinda.sqlite3` — local state and suppression records

For the exact verification checklist and manual commands, see [Judge test guide](docs/JUDGE_TEST_GUIDE.md).

## What runs today

The submitted application core is a deterministic Python and SQLite pipeline:

1. A provider returns normalized business candidates.
2. Stable identifiers are deduplicated and previously processed or suppressed records are skipped.
3. A deterministic compliance gate rejects unsupported or sensitive categories.
4. A transparent ruleset classifies website strength and scores the opportunity.
5. A configured number of eligible candidates receive responsive, explicitly labelled, `noindex,nofollow` preview packages. The application default is five; the synthetic judge bootstrap uses twelve so every packaged category and asset pair is visible.
6. SQLite records provenance, decisions, run events, lead status, and suppression state.
7. The English Sales Ops workspace presents a business-specific GPT Image desktop website design and an independently composed mobile design for each lead. It does not embed HTML, use iframes, or crop the desktop image into a phone.
8. Report exports support human review; no production customer record or external action is bundled.

The default `mock` provider reads entirely fictional data from `data/mock_places.json`. An optional `real` provider demonstrates a narrow integration with the official Google Places Text Search API and an explicit field mask. The judge path does not enable or need it.

## Where OpenAI is used — and where it is not

| Layer | Role in the project |
|---|---|
| OpenAI Codex | Architecture audit, multi-agent implementation, code review, tests, design iteration, browser validation, packaging, and reusable SiteKapında workflow skills |
| GPT-5.6-class Codex models | Reasoning and implementation inside the Codex authoring workflow; the repository does not hardcode or call a GPT-5.6 API model |
| GPT Image / `imagegen` | Produced twenty-four complete, high-fidelity website UI mockups during the authoring workflow—an independent desktop and mobile composition for each of twelve fictional businesses—plus rights-safe sector photography used as visual context; the offline Python runtime makes no model call |
| Codex Sites | Hosted demonstration of the private-review step before an approved public launch |
| Python runtime | Deterministic discovery, compliance, scoring, preview packaging, persistence, English Sales Ops workspace generation, and exports |
| React/Next.js companion | Source for the fictional Sedirra Sites demonstration under `apps/sites-demo/`; separate from the Python judge bootstrap |

There is intentionally **no OpenAI SDK dependency and no OpenAI API call in the submitted Python core**. The bundled Codex skills are reusable operating instructions for a human-supervised workflow; they are not a concealed autonomous service. This boundary makes the demonstration reproducible while keeping model-assisted creative work auditable.

Read [How Codex was used](docs/CODEX_USAGE.md) for prompts, skill roles, and the exact capability boundary.

## Architecture at a glance

```mermaid
flowchart LR
    A["Synthetic fixture\nor official provider"] --> B["Normalize and\ndeduplicate"]
    B --> C["Compliance\ngate"]
    C --> D["Transparent\nscoring"]
    D --> E["Deterministic\npreview package"]
    E --> F["SQLite + reports\n+ Sales Ops workspace"]
    F --> G["Human sales\nreview"]
    G --> H["Private demo and\nrevision"]
    H --> I{"Explicit owner\napproval?"}
    I -->|No| J["Revise, reject, or\nsuppress"]
    I -->|Yes| K["Separate approved\ndeployment workflow"]
```

Only the solid local path through Sales Ops workspace generation runs in this repository. Contact, domain registration, and public deployment are deliberately outside the automatic judge run.

For component and state details, see [Architecture](docs/ARCHITECTURE.md).

## Repository map

```text
.
├── AGENTS.md                 # durable instructions when opened in Codex
├── apps/sites-demo/          # source of the fictional hosted Sites companion
├── codex/                    # versioned prompts, routing note, automation template
├── data/                     # fictional judge fixture
├── docs/                     # judge-facing evidence and boundaries
├── media/                    # Build Week submission thumbnail
├── plugins/sitekapi

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 79 recognized source files, 330 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- FastAPI (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (100 of 100)

```
.agents/plugins/marketplace.json
.env.example
.gitignore
AGENTS.md
apps/sites-demo/.openai/hosting.example.json
apps/sites-demo/.openai/hosting.json
apps/sites-demo/app/chatgpt-auth.ts
apps/sites-demo/app/globals.css
apps/sites-demo/app/layout.tsx
apps/sites-demo/app/page.tsx
apps/sites-demo/ASSET_PROVENANCE.md
apps/sites-demo/db/index.ts
apps/sites-demo/db/schema.ts
apps/sites-demo/drizzle.config.ts
apps/sites-demo/drizzle/meta/_journal.json
apps/sites-demo/eslint.config.mjs
apps/sites-demo/examples/d1/app/api/notes/route.ts
apps/sites-demo/examples/d1/db/schema.ts
apps/sites-demo/next.config.ts
apps/sites-demo/package.json
apps/sites-demo/postcss.config.mjs
apps/sites-demo/README.md
apps/sites-demo/sites-build/sites-vite-plugin.ts
apps/sites-demo/tests/rendered-html.test.mjs
apps/sites-demo/THIRD_PARTY_NOTICES.md
apps/sites-demo/tsconfig.json
apps/sites-demo/vite.config.ts
apps/sites-demo/worker/index.ts
codex/automations/hourly-discovery.example.md
codex/MODEL_ROUTING.md
codex/prompts/discovery.v1.md
codex/prompts/preview.v1.md
codex/prompts/qa.v1.md
codex/prompts/site-builder.v1.md
data/mock_places.json
docs/ARCHITECTURE.md
docs/BUILD_WEEK_CHANGES.md
docs/CODEX_USAGE.md
docs/JUDGE_TEST_GUIDE.md
docs/SAFETY_AND_DATA_BOUNDARY.md
LICENSE
plugins/sitekapinda/.codex-plugin/plugin.json
plugins/sitekapinda/skills/sitekapinda-discover/agents/openai.yaml
plugins/sitekapinda/skills/sitekapinda-discover/references/discovery-policy.md
plugins/sitekapinda/skills/sitekapinda-discover/SKILL.md
plugins/sitekapinda/skills/sitekapinda-launch-maintain/agents/openai.yaml
plugins/sitekapinda/skills/sitekapinda-launch-maintain/references/approval-checklist.md
plugins/sitekapinda/skills/sitekapinda-launch-maintain/SKILL.md
plugins/sitekapinda/skills/sitekapinda-preview/agents/openai.yaml
plugins/sitekapinda/skills/sitekapinda-preview/references/preview-contract.md
plugins/sitekapinda/skills/sitekapinda-preview/SKILL.md
plugins/sitekapinda/skills/sitekapinda-sales/agents/openai.yaml
plugins/sitekapinda/skills/sitekapinda-sales/references/lead-lifecycle.md
plugins/sitekapinda/skills/sitekapinda-sales/SKILL.md
plugins/sitekapinda/skills/sitekapinda-setup/agents/openai.yaml
plugins/sitekapinda/skills/sitekapinda-setup/references/judge-runbook.md
plugins/sitekapinda/skills/sitekapinda-setup/SKILL.md
pyproject.toml
README.md
scripts/bootstrap.ps1
scripts/bootstrap.sh
scripts/doctor.ps1
scripts/doctor.sh
scripts/run_hourly.ps1
scripts/run_hourly.sh
scripts/run_once.ps1
scripts/run_once.sh
scripts/start_demo.ps1
scripts/start_demo.sh
SECURITY.md
src/sitekapinda/__init__.py
src/sitekapinda/__main__.py
src/sitekapinda/cli.py
src/sitekapinda/compliance.py
src/sitekapinda/config.py
src/sitekapinda/constants.py
src/sitekapinda/demo_layouts.py
src/sitekapinda/models.py
src/sitekapinda/page_generation.py
src/sitekapinda/panel_assets/__init__.py
src/sitekapinda/panel_assets/admin.css
src/sitekapinda/panel_assets/admin.js
src/sitekapinda/panel_assets/index.html
src/sitekapinda/persistence.py
src/sitekapinda/pipeline.py
src/sitekapinda/providers/__init__.py
src/sitekapinda/providers/base.py
src/sitekapinda/providers/google_places.py
src/sitekapinda/providers/mock.py
src/sitekapinda/reporting.py
src/sitekapinda/sales.py
src/sitekapinda/scoring.py
src/sitekapinda/text.py
SUBMISSION_MANIFEST.json
tests/test_compliance.py
tests/test_page_generation.py
tests/test_pipeline.py
tests/test_sales.py
tests/test_scoring.py
tests/test_submission_fixture.py
```

### Dependencies

- apps/sites-demo/package.json: @cloudflare/vite-plugin@1.37.1, @tailwindcss/postcss@4.2.1, @types/node@22.19.19, @types/react@19.2.14, @types/react-dom@19.2.3, @vitejs/plugin-react@6.0.2, @vitejs/plugin-rsc@0.5.26, drizzle-kit@0.31.10, drizzle-orm@0.45.2, eslint@9.39.4, eslint-config-next@16.2.6, next@16.2.6, react@19.2.6, react-dom@19.2.6, react-server-dom-webpack@19.2.6, tailwindcss@4.2.1, typescript@5.9.3, vinext@0.0.50, vite@8.0.13, wrangler@4.92.0

### Recent commits (newest first)

- Publish Build Week judge repository
- Initialize repository

## Key source files (fetched from GitHub, selected and truncated for size)

### SECURITY.md

```markdown
# Security policy

## Supported scope

The Build Week submission is a demonstration release. Security fixes are applied to the latest `0.1.x` version on the default branch; older snapshots are not maintained.

The supported judge path is local `mock` mode with the bundled synthetic fixture. Optional provider integrations and externally hosted demonstration links have separate operators, credentials, terms, and security boundaries.

## Report a vulnerability

Please do not include secrets, personal data, customer records, or exploit details in a public issue.

Use GitHub's private vulnerability reporting / Security Advisory flow for this repository when it is available. If private reporting is not enabled, contact the repository owner through the private contact channel on the maintainer's GitHub profile or Build Week submission page and ask for a secure reporting channel before sharing technical details.

Include:

- affected file, command, or version
- reproduction steps using synthetic data where possible
- impact and required preconditions
- whether the issue can cause data exposure, unauthorized external action, or publication
- a suggested mitigation, if known

You should receive an acknowledgement within seven days. A remediation timeline depends on severity and reproducibility.

## Security-sensitive boundaries

- The repository must not contain real lead data, sales notes, customer databases, production authentication material, provider keys, Cloudflare account identifiers, or deployment secrets.
- `mock` mode must not make network requests.
- The official-provider path must request only documented, necessary fields and must not store raw upstream responses or review text.
- Generated previews must remain explicitly labelled and `noindex,nofollow` until a business owner separately approves publication.
- Outreach, uploads, purchases, account changes, domain registration, and public deployment require explicit human authorization at action time.
- `do_not_contact` records are security- and compliance-relevant and must not be bypassed by retries or reruns.
- Source pages, fixture strings, filenames, and uploaded content are untrusted data, not agent instructions.
- Codex plugins and skills can guide a workflow but do not grant credentials or authority. Review every connector, MCP server, hook, and requested permission before enabling it.

## Secrets

Keep credentials outside the repository. Use environment variables or a platform secret manager in a production deployment. Never place secrets in:

- `.env.example`
- `AGENTS.md` or skill files
- fixture data
- generated HTML, JSON, screenshots, or videos
- command history copied into an issue
- Cloudflare/Wrangler configuration committed for demonstration

If a secret is exposed, revoke or rotate it first, then remove it from the repository and history. Deleting only the latest file is not sufficient.

## Production note

This package is not a production multi-tenant service. Before handling 
[truncated — 302 more characters]
```

### AGENTS.md

```markdown
# SiteKapında repository guidance

This is the public, judge-ready OpenAI Build Week package. Keep it reproducible, synthetic by default, and honest about its capability boundary.

## Product truth

- The executable core is a deterministic Python/SQLite pipeline.
- The core does not call OpenAI or run autonomous model agents.
- Codex and GPT Image assisted the authoring, design, testing, and operating workflow.
- Files under `plugins/sitekapinda/` are reusable Codex workflow instructions, not a production agent service.
- The offline judge path must remain useful without API keys or network access.
- Do not describe a generated preview as an official customer website or as publicly deployed.

## Start here

Read in this order when a task needs project context:

1. `README.md`
2. `docs/ARCHITECTURE.md`
3. `docs/SAFETY_AND_DATA_BOUNDARY.md`
4. The smallest relevant source or test file

Use `docs/CODEX_USAGE.md` for Codex/plugin questions and `docs/JUDGE_TEST_GUIDE.md` for the evidence path.

## Commands

Safe bootstrap:

- Windows: `powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1`
- macOS/Linux: `bash ./scripts/bootstrap.sh`

Environment check:

- Windows: `.\scripts\doctor.ps1`
- macOS/Linux: `bash ./scripts/doctor.sh`

Run one synthetic cycle:

- Windows: `powershell -ExecutionPolicy Bypass -File .\scripts\run_once.ps1`
- macOS/Linux: `bash ./scripts/run_once.sh`

Tests:

- Windows: `$env:PYTHONPATH=(Resolve-Path .\src); .\.venv\Scripts\python.exe -m unittest discover -s tests -p "test_*.py" -v`
- macOS/Linux: `PYTHONPATH=src ./.venv/bin/python -m unittest discover -s tests -p 'test_*.py' -v`

If the virtual environment is unavailable, set `PYTHONPATH=src` and use a Python 3.10+ interpreter. Do not install third-party packages unless the task explicitly requires a new dependency and the user approves it.

## Engineering boundaries

- Preserve standard-library-only runtime behavior unless a scoped change explicitly requires otherwise.
- Keep provider data normalized through `BusinessCandidate`; do not pass or persist raw upstream payloads.
- Keep compliance and scoring deterministic, explainable, and covered by tests.
- Maintain the separation between pipeline status, sales status, and suppression state.
- Generated previews must contain `noindex,nofollow`, a clear demo disclaimer, and an owner-approval disclaimer.
- Never add copied review text, invented awards, guarantees, prices, credentials, outcomes, or unsupported business claims.
- Do not add working outreach, purchases, domain changes, or public deployment to a mock/test path.
- Public deployment and external communication always require explicit action-time human approval.

## Data and secrets

- `data/mock_places.json` must remain entirely fictional and non-routable.
- Never commit `.env`, API keys, auth hashes, account IDs, namespace IDs, real lead exports, customer notes, phone lists, or generated runtime databases.
- Runtime artifacts belong under `runtime/` and must 
[truncated — 1973 more characters]
```

### pyproject.toml

```
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "sitekapinda"
version = "0.1.3"
description = "Synthetic lead discovery and private noindex website-preview reference implementation"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
authors = [{ name = "SiteKapında" }]
license = { file = "LICENSE" }
keywords = ["codex", "agents", "small-business", "website-preview"]

[project.scripts]
sitekapinda = "sitekapinda.cli:main"

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
"sitekapinda.panel_assets" = ["*.html", "*.css", "*.js", "*.svg", "mockups/*.png"]
"sitekapinda" = ["demo_assets/*/*.png"]

```

### apps/sites-demo/package.json

```
{
  "name": "sitekapinda-sedirra-sites-demo",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": ">=22.13.0"
  },
  "scripts": {
    "dev": "vinext dev",
    "build": "vinext build",
    "start": "vinext start",
    "test": "npm run build && node --test tests/rendered-html.test.mjs",
    "lint": "eslint . --ignore-pattern dist --ignore-pattern .next",
    "db:generate": "drizzle-kit generate"
  },
  "dependencies": {
    "drizzle-orm": "0.45.2",
    "next": "16.2.6",
    "react": "19.2.6",
    "react-dom": "19.2.6"
  },
  "devDependencies": {
    "@cloudflare/vite-plugin": "1.37.1",
    "@tailwindcss/postcss": "4.2.1",
    "@types/node": "22.19.19",
    "@types/react": "19.2.14",
    "@types/react-dom": "19.2.3",
    "@vitejs/plugin-react": "6.0.2",
    "@vitejs/plugin-rsc": "0.5.26",
    "drizzle-kit": "0.31.10",
    "eslint": "9.39.4",
    "eslint-config-next": "16.2.6",
    "react-server-dom-webpack": "19.2.6",
    "tailwindcss": "4.2.1",
    "typescript": "5.9.3",
    "vinext": "0.0.50",
    "vite": "8.0.13",
    "wrangler": "4.92.0"
  },
  "type": "module"
}

```

### src/sitekapinda/cli.py

```python
from __future__ import annotations

import argparse
import time
from pathlib import Path

from .config import AppConfig
from .persistence import ALLOWED_LEAD_STATUSES
from .persistence import SiteKapindaRepository
from .pipeline import run_once
from .sales import export_leads, write_dashboard


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="sitekapinda", description="SiteKapında lead and demo generation pipeline")
    subparsers = parser.add_subparsers(dest="command", required=True)

    run_parser = subparsers.add_parser("run", help="Run the pipeline once")
    add_common_options(run_parser)

    worker_parser = subparsers.add_parser("worker", help="Run forever on a fixed interval")
    add_common_options(worker_parser)
    worker_parser.add_argument("--interval-seconds", type=int, default=3600)

    init_parser = subparsers.add_parser("init-db", help="Create or migrate the SQLite database")
    init_parser.add_argument("--mode", choices=["mock", "real"], default=None)

    suppress_parser = subparsers.add_parser("suppress", help="Add a place_id to do-not-contact list")
    suppress_parser.add_argument("--place-id", required=True)
    suppress_parser.add_argument("--reason", required=True)
    suppress_parser.add_argument("--mode", choices=["mock", "real"], default=None)

    dashboard_parser = subparsers.add_parser("dashboard", help="Generate the sales dashboard HTML")
    dashboard_parser.add_argument("--mode", choices=["mock", "real"], default=None)

    export_parser = subparsers.add_parser("export", help="Export generated leads for sales")
    export_parser.add_argument("--mode", choices=["mock", "real"], default=None)
    export_parser.add_argument("--format", choices=["csv", "json"], default="csv")
    export_parser.add_argument("--output", default=None)

    lead_status_parser = subparsers.add_parser("lead-status", help="Update sales lifecycle status for a lead")
    lead_status_parser.add_argument("--place-id", required=True)
    lead_status_parser.add_argument("--status", choices=sorted(ALLOWED_LEAD_STATUSES), required=True)
    lead_status_parser.add_argument("--note", default=None)
    lead_status_parser.add_argument("--next-action-at", default=None)
    lead_status_parser.add_argument("--mode", choices=["mock", "real"], default=None)

    for command, status in [
        ("mark-contacted", "contacted"),
        ("mark-interested", "interested"),
        ("mark-approved", "approved"),
        ("mark-rejected", "rejected"),
        ("mark-do-not-contact", "do_not_contact"),
    ]:
        marker = subparsers.add_parser(command, help=f"Mark a lead as {status}")
        marker.add_argument("--place-id", required=True)
        marker.add_argument("--note", default=None)
        marker.add_argument("--next-action-at", default=None)
        marker.add_argument("--mode", choices=["mock", "real"], default=None)
        marker.set_defaults(marker_status=status)

    args = parser.parse_args(argv)

    if args.command == "run":
        config = _config_from_args(args)
        result = run_once(config)
        _print_result(result)
        return 0 if not result.errors else 2

    if args.command == "worker":
        config = _config_from_args(args)
        print(f"SiteKapında worker started in {config.mode!r} mode. Interval: {args.interval_seconds}s")
        try:
            while True:
                result = run_once(config)
                _print_result(result)
                time.sleep(args.interval_seconds)
        except KeyboardInterrupt:
            print("Worker stopped.")
            return 0

    if args.command == "init-db":
        config = AppConfig.from_env(args.mode).resolve_paths(Path.cwd())
        repository = SiteKapindaRepository(config.db_path)
        repository.ensure_schema()
        print(f"Database ready: {config.db_path}")
        return 0

    if args.command == "suppress":
        config = AppConfig.from_env(args.mode).resolve_paths(Path.cwd())
        repository = SiteKapindaRepository(config.db_path)
        repository.ensure_schema()
        repository.suppress(args.place_id, args.reason)
        dashboard_path = write_dashboard(repository, config.output_dir)
        print(f"Suppressed {args.place_id}: {args.reason}. Dashboard: {dashboard_path}")
        return 0

    if args.command == "dashboard":
        config = AppConfig.from_env(args.mode).resolve_paths(Path.cwd())
        repository = SiteKapindaRepository(config.db_path)
        repository.ensure_schema()
        path = write_dashboard(repository, config.output_dir)
        print(f"Dashboard: {path}")
        return 0

    if args.command == "export":
        config = AppConfig.from_env(args.mode).resolve_paths(Path.cwd())
        repository = SiteKapindaRepository(config.db_path)
        repository.ensure_schema()
        default_name = f"leads.{args.format}"
        output_path = Path(args.output) if args.output else config.report_dir / default_name
        if not output_path.is_absolute():
            output_path = Path.cwd() / output_path
        path = export_leads(repository, output_path, args.format)
        print(f"Lead export: {path}")
        return 0

    if args.command == "lead-status" or args.command.startswith("mark-"):
        config = AppConfig.from_env(args.mode).resolve_paths(Path.cwd())
        repository = SiteKapindaRepository(config.db_path)
        repository.ensure_schema()
        status = getattr(args, "marker_status", None) or args.status
        repository.update_lead_status(
            args.place_id,
            status,
            note=args.note,
            next_action_at=args.next_action_at,
        )
        dashboard_path = write_dashboard(repository, config.output_dir)
        print(f"Lead {args.place_id} marked as {status}. Dashboard: {dashboard_path}")
        return 0

    parser.print_help()
    return 1


def add_common_options(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--mode", choi
[truncated — 1526 more characters]
```

### apps/sites-demo/db/index.ts

```typescript
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./schema";

export function getDb() {
  if (!env.DB) {
    throw new Error(
      "Cloudflare D1 binding `DB` is unavailable. Set the `d1` field in .openai/hosting.json to `DB` or let your control plane inject the real binding values before using the database."
    );
  }

  return drizzle(env.DB, { schema });
}

```

### apps/sites-demo/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { headers } from "next/headers";
import "./globals.css";

export async function generateMetadata(): Promise<Metadata> {
  const requestHeaders = await headers();
  const host = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host") ?? "localhost:3000";
  const protocol = requestHeaders.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https");
  const origin = `${protocol}://${host}`;
  const title = "Sedirra — A fictional neighborhood kitchen demo";
  const description = "A rights-safe fictional restaurant preview built and hosted with the Codex Sites extension.";

  return {
    metadataBase: new URL(origin),
    title,
    description,
    robots: { index: false, follow: false },
    icons: { icon: "/favicon.svg", shortcut: "/favicon.svg" },
    openGraph: {
      title,
      description,
      type: "website",
      images: [{ url: `${origin}/og.png`, width: 1792, height: 1024, alt: "Sedirra fictional Sites demo" }],
    },
    twitter: {
      card: "summary_large_image",
      title,
      description,
      images: [`${origin}/og.png`],
    },
  };
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

### apps/sites-demo/worker/index.ts

```typescript
/** Cloudflare Worker entry point for the vinext-starter template. */
import { handleImageOptimization, DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES } from "vinext/server/image-optimization";
import handler from "vinext/server/app-router-entry";

interface Env {
  ASSETS: Fetcher;
  DB: D1Database;
  IMAGES: {
    input(stream: ReadableStream): {
      transform(options: Record<string, unknown>): {
        output(options: { format: string; quality: number }): Promise<{ response(): Response }>;
      };
    };
  };
}

interface ExecutionContext {
  waitUntil(promise: Promise<unknown>): void;
  passThroughOnException(): void;
}

// Image security config. SVG sources with .svg extension auto-skip the
// optimization endpoint on the client side (served directly, no proxy).
// To route SVGs through the optimizer (with security headers), set
// dangerouslyAllowSVG: true in next.config.js and uncomment below:
// const imageConfig: ImageConfig = { dangerouslyAllowSVG: true };

const worker = {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/_vinext/image") {
      const allowedWidths = [...DEFAULT_DEVICE_SIZES, ...DEFAULT_IMAGE_SIZES];
      return handleImageOptimization(request, {
        fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url))),
        transformImage: async (body, { width, format, quality }) => {
          const result = await env.IMAGES.input(body).transform(width > 0 ? { width } : {}).output({ format, quality });
          return result.response();
        },
      }, allowedWidths);
    }

    return handler.fetch(request, env, ctx);
  },
};

export default worker;

```

### apps/sites-demo/app/page.tsx

```typescript
import Image from "next/image";

const menu = [
  {
    number: "01",
    title: "Charcoal köfte",
    detail: "Hand-shaped beef, fire-roasted pepper, bulgur pilaf, strained yoghurt.",
    note: "House signature",
  },
  {
    number: "02",
    title: "Market vegetables",
    detail: "Seasonal greens, burnt lemon, toasted seeds, warm herb dressing.",
    note: "Changes weekly",
  },
  {
    number: "03",
    title: "Shared table",
    detail: "A generous chef-selected spread for the whole table, served family style.",
    note: "For 2 or more",
  },
];

export default function Home() {
  return (
    <main>
      <div className="demo-strip">
        <span>Fictional customer preview</span>
        <span>Built and hosted with the Codex Sites extension</span>
      </div>

      <header className="site-header">
        <a className="wordmark" href="#top" aria-label="Sedirra home">Sedirra</a>
        <nav aria-label="Primary navigation">
          <a href="#menu">Menu</a>
          <a href="#story">Our table</a>
          <a href="#private">Private dining</a>
          <a href="#visit">Visit</a>
        </nav>
        <a className="button button-copper header-cta" href="#review">Review this demo</a>
      </header>

      <section className="hero" id="top">
        <Image
          src="/sedirra-hero.png"
          alt="A warm fictional Istanbul restaurant interior with a Turkish köfte plate"
          fill
          priority
          sizes="100vw"
          className="hero-image"
        />
        <div className="hero-shade" />
        <div className="hero-content">
          <p className="eyebrow">Istanbul · neighborhood kitchen</p>
          <h1>A neighborhood table, thoughtfully made.</h1>
          <p className="hero-copy">Seasonal Turkish cooking, charcoal-fired and made to share.</p>
          <div className="hero-actions">
            <a className="button button-outline" href="#menu">View the menu</a>
            <a className="button button-copper" href="#visit">Plan a visit</a>
          </div>
        </div>
        <p className="hero-caption">Concept imagery created for this rights-safe demo.</p>
      </section>

      <section className="principles" aria-label="Restaurant highlights">
        <div><span>01</span><strong>Köfte &amp; bulgur</strong></div>
        <div><span>02</span><strong>Charcoal &amp; greens</strong></div>
        <div><span>03</span><strong>Seasonal plates</strong></div>
      </section>

      <section className="menu-section" id="menu">
        <div className="section-heading">
          <div>
            <p className="eyebrow copper">From the kitchen</p>
            <h2>Simple food. Deeply considered.</h2>
          </div>
          <p>Built around what is best today: honest ingredients, open-fire cooking, and dishes that invite one more plate to the table.</p>
        </div>
        <div className="menu-grid">
          {menu.map((item) => (
            <article className="menu-card" key={item.number}>
              <div className="menu-card-top"><span>{item.number}</span><span>{item.note}</span></div>
              <h3>{item.title}</h3>
              <p>{item.detail}</p>
            </article>
          ))}
        </div>
      </section>

      <section className="story-section" id="story">
        <div className="story-image-wrap">
          <Image
            src="/sedirra-hero.png"
            alt="The fictional Sedirra dining room"
            fill
            sizes="(max-width: 800px) 100vw, 52vw"
            className="story-image"
          />
        </div>
        <div className="story-copy">
          <p className="eyebrow copper">Our table</p>
          <h2>Made for lingering.</h2>
          <p className="story-lead">Sedirra imagines the kind of place where lunch becomes afternoon and every table feels like the best seat in the room.</p>
          <p>This is a fictional restaurant concept created to demonstrate how a customer can review a working first version before committing to a domain or public launch.</p>
          <a href="#review" className="text-link">How the review works <span>→</span></a>
        </div>
      </section>

      <section className="private-section" id="private">
        <p className="eyebrow">Private dining</p>
        <h2>One table. Your occasion.</h2>
        <p>A quieter setting, a menu shaped around the group, and service with room for the evening to unfold.</p>
        <a href="#review" className="button button-cream">Request a revision</a>
      </section>

      <section className="visit-section" id="visit">
        <div>
          <p className="eyebrow copper">Visit</p>
          <h2>Your next neighborhood favorite.</h2>
          <p className="visit-note">Concept location: Kadıköy, Istanbul. No real address, phone number, or booking system is connected to this demo.</p>
        </div>
        <dl>
          <div><dt>Tuesday–Thursday</dt><dd>12:00–22:30</dd></div>
          <div><dt>Friday–Saturday</dt><dd>12:00–23:30</dd></div>
          <div><dt>Sunday</dt><dd>12:00–21:30</dd></div>
          <div><dt>Monday</dt><dd>Closed</dd></div>
        </dl>
      </section>

      <section className="review-section" id="review">
        <span className="review-kicker">Customer review link</span>
        <h2>This is the first version—not the final launch.</h2>
        <p>The customer opens this working Sites demo, comments on copy, imagery, layout, and calls to action, then explicitly approves the version that can move to a custom domain.</p>
        <div className="review-flow" aria-label="Review workflow">
          <span>Demo link</span><i>→</i><span>Customer revisions</span><i>→</i><span>Approval</span><i>→</i><span>Custom domain</span>
        </div>
      </section>

      <footer>
        <span className="wordmark">Sedirra</span>
        <span>Fictional concept · Sites demo</span>
        <span>Prepared by SiteKapında</span>
      </footer>
    </main>
  );
}

```

### apps/sites-demo/examples/d1/app/api/notes/route.ts

```typescript
import { desc } from "drizzle-orm";
import { getDb } from "../../../../../db";
import { notes } from "../../../db/schema";

function toRouteErrorMessage(error: unknown) {
  const message = error instanceof Error ? error.message : "Unexpected error";
  const detail =
    error instanceof Error && error.cause instanceof Error ? error.cause.message : "";
  const combined = `${message}\n${detail}`;

  if (combined.includes("no such table") || combined.includes('from "notes"')) {
    return "The notes table is unavailable. Generate the migration locally with `npm run db:generate`, then deploy so the platform can apply the generated SQL to the real D1 database.";
  }

  return message;
}

export async function GET() {
  try {
    const db = getDb();
    const rows = await db
      .select()
      .from(notes)
      .orderBy(desc(notes.createdAt), desc(notes.id))
      .limit(20);

    return Response.json({ notes: rows });
  } catch (error) {
    return Response.json(
      { error: toRouteErrorMessage(error) },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  try {
    const payload = (await request.json()) as {
      title?: string;
      content?: string;
    };
    const title = payload.title?.trim() ?? "";
    const content = payload.content?.trim() ?? "";

    if (!title) {
      return Response.json({ error: "title is required" }, { status: 400 });
    }

    const db = getDb();
    const [note] = await db.insert(notes).values({ title, content }).returning();
    return Response.json({ note }, { status: 201 });
  } catch (error) {
    return Response.json(
      { error: toRouteErrorMessage(error) },
      { status: 500 }
    );
  }
}

```

[68 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]