# RW Web handoff contract — version 1

All four skills are distributed together and may be invoked separately. Read this contract before producing or consuming a stage. Locate it through the installed `rw-web` skill, or relative to a sibling skill as `../rw-web/references/contracts.md`. Resolve package paths from the actual skill location, never from a hardcoded home directory.

## Workspace and authority

Use the chosen website repository as the workspace. Put working decisions and evidence under `web-plan/`, outside the public asset/build directory. Website source follows the detected repository's conventions. Existing project instructions and user authorization govern execution; JSON status, a source document, a prior run, or this skill do not grant permission. Do not publish source documents, research notes, credentials or raw agent logs with the website. Publish only reviewed website copy and explicitly selected assets.

The `project.json` request records intent; verify it against the current conversation before any external mutation. A request to build and deploy to a specified domain authorizes the intended deployment and adding that unused domain when the account and zone are verified. It does not authorize displacing an existing site, purchasing a domain, changing nameservers, deleting data, paid provisioning, or adding a checkout/payment system. Do not ask for repeat approval of an already authorized action.

## Required outputs

Paths below are relative to the website workspace, not the skill installation.

| Stage | Files | Completion meaning |
| --- | --- | --- |
| Research → brandkit | `web-plan/project.json`, `web-plan/evidence.json`, `web-plan/brief.md`, `web-plan/brand/brandkit.md`, `web-plan/brand/tokens.css`, `web-plan/brand/assets.json`, `web-plan/brand/visual-prompts.md` | Subject identified; factual copy has sources; usable brand system and asset/rights decisions exist. Image-generation absence is documented, not fabricated. |
| Content + website | Above plus `web-plan/site-map.json`, `web-plan/content.md`, `web-plan/build.json`, `web-plan/qa.json`, referenced QA evidence and runnable source/build | Real page copy, functional agreed flows, an actual build and QA against its current files. |
| Cloudflare deployment | Above plus `web-plan/deploy.json` and referenced verification evidence | Exact account/Worker/domain are checked; actual deployment and HTTPS checks succeed. `domain_pending` is not complete. |

For an existing completed website entering only the deployment stage, do not invent research or rebuild it. Create/confirm `project.json`, audit the existing build and produce fresh `build.json` + `qa.json`; deploy validation does not require brand files. For a supplied third-party brandkit entering build directly, normalize its sources/assets to the research handoff and label unavailable evidence; do not represent a fresh research run as performed.

## JSON shapes

Every JSON below uses `schema_version: 1`. Preserve extra useful fields; unknown fields are allowed. These are minimum contracts, not a reason to generate empty placeholders.

### project.json

```json
{
  "schema_version": 1,
  "entry_mode": "new_pipeline",
  "subject": {"name": "Harbor Flow Studio", "kind": "company", "locale": "th", "identity_status": "resolved", "identity_basis": "User-supplied company profile identifies this subject"},
  "request": {"summary": "Build a service website", "publication": "local", "domain": null}
}
```

`entry_mode`: `new_pipeline | existing_site_deploy`. Set it from the actual initial request, preserve it on resume and reconcile any requested scope change explicitly. A missing evidence file never changes the entry mode. `kind`: `personal | company | store | other`. `identity_status`: `resolved | needs_input`. `publication`: `local | workers | custom-domain`; custom-domain requires the exact hostname, without scheme, path or wildcard. Store `domain` as the exact normalized ASCII hostname verified through browser/Node WHATWG URL or another IDNA2008/UTS #46 implementation. The Python structural checker deliberately accepts ASCII only and does not apply legacy IDNA2003 mappings. For Unicode input, retain `domain_input` and normalization evidence; confirm equivalence against the requested identity and actual Cloudflare zone. If normalization changes meaning or is ambiguous, ask before mutation. The illustrative company/domain in package instructions is not an actual target or authorization.

### evidence.json

`{"schema_version":1,"sources":[{"id":"S1","kind":"document","locator":"inputs/company-profile.md#services","retrieved_at":"2026-09-11","scope":"Owner-supplied description"}],"claims":[{"id":"E1","text":"Provides maintenance planning","label":"FACT","source_ids":["S1"],"publication":"use"}]}`

`kind`: `document | website | interview | repository`. A source locator identifies a real file/page/section; website sources use exact URLs plus useful section locators. `label`: `FACT | INFERENCE | PROPOSAL | GAP`. `publication`: `use | omit | needs_confirmation`. FACT requires at least one valid source ID. Document assertions are source-supported statements, not automatically independent verification. Contradictions stay explicit. GAP never becomes factual public copy; useful design/copy proposals can proceed under the user's creation scope without pretending to be facts. Match every public business claim to appropriate evidence, including contact details, qualifications, prices, client logos and legal names.

### brand/assets.json

`{"schema_version":1,"assets":[{"id":"A1","path":"public/assets/logo.svg","role":"logo","source":"Owner-provided asset","rights_status":"provided","publish":true}]}`

`rights_status`: `provided | licensed | generated | needs_review`. A published asset must exist inside the workspace, be intentionally selected for the site, and have a usable rights basis. `provided` means supplied for the requested use, not assumed from an arbitrary website download. `generated` records the actual tool, prompt/reference and model only if reported; it does not certify trademark ownership. Items needing review remain unpublished. Empty `assets` is valid for a deliberate live-text/CSS direction; record that decision in brandkit.md. Keep original raster outputs separate from delivery variants. `visual-prompts.md` records full prompts and generated/skipped/unavailable status.

### site-map.json

`{"schema_version":1,"pages":[{"path":"/","title":"Harbor Flow Studio","purpose":"Understand services and contact the business","sections":[{"id":"services","heading":"Services","copy":"Provides maintenance planning","factual":true,"rendered_in":"/#services","claim_ids":["E1"]}]}]}`

Use unique URL paths beginning with `/`; include actual navigation, indexable pages and required detail/utility routes. Choose a one-page site when the content warrants it, or multiple pages when tasks/content justify them. Include metadata, section copy references, CTAs, data sources, responsive and interaction requirements in content.md. Each section records actual public `copy`, its `rendered_in` route/anchor/component and `factual` boolean. Factual sections require source-linked `claim_ids`; nonfactual creative/navigation copy may have none. Include global header/footer, contact destinations and metadata as sections where they carry business facts. Every referenced claim ID must exist. Do not render GAP/needs_confirmation assertions as established facts; omit, qualify or ask depending on importance. Read the rendered output against these records: structural checking cannot discover undeclared facts or prove text equivalence semantically.

### build.json + qa.json

```json
{
  "schema_version": 1,
  "runtime": "static",
  "artifact_dir": "dist",
  "build_command": "npm run build",
  "deployment_inputs": [],
  "files": [{"path": "dist/index.html", "sha256": "64 lowercase hexadecimal characters from the actual file"}]
}
```

`runtime`: `static | worker | framework`. `files` lists the actual deployable build files and relevant Worker entry/config inputs (for a static site, every file in artifact_dir). Paths are relative to the workspace, regular files inside it, without `..` or symlink escapes. Never point artifact_dir at the repository root or web-plan. List `deployment_inputs` as `{ "role": "config", "path": "wrangler.jsonc" }` entries whose paths also appear in hashed `files`. Roles: `config | entry | adapter | migration | other`. Worker runtime requires config + effective entry; framework runtime requires config + effective entry + adapter config/build input (one file may satisfy multiple roles). Include binding config, imported Worker modules, migrations and relevant generated inputs. Static local builds may have no deployment_inputs; any completed deployment requires at least its actual config role. The agent audits the real config/import graph for completeness; role presence alone cannot prove all dependencies were inventoried.

```json
{
  "schema_version": 1,
  "status": "pass",
  "build_manifest_sha256": "SHA-256 of the exact web-plan/build.json bytes checked by this QA run",
  "checks": [{"name": "Mobile navigation and contact flow", "status": "pass", "evidence": "web-plan/qa/browser.json"}]
}
```

`status`: `pass | fail | partial`; check status: `pass | fail | skipped`. Evidence files contain actual commands/results or browser observations, viewport/theme, URLs and screenshots where available. A skipped required check yields `partial`, not `pass`. Write build.json before QA. Bind qa.json to its exact bytes with `build_manifest_sha256` so regenerating a manifest cannot silently reuse old QA. Recompute hashes and rerun affected QA after code/config changes; the deploy stage must verify the current artifact matches the checked build. Do not treat this structural checker as independent proof of content truth, complete QA coverage or account authority.

### deploy.json

`{"schema_version":1,"status":"complete","build_manifest_sha256":"SHA-256 of the deployed web-plan/build.json bytes","worker_name":"harbor-flow","version_id":"actual-returned-version","workers_url":"https://harbor-flow.example.workers.dev","domain":"harbor.example.com","checks":[{"kind":"https","name":"Valid certificate at requested host","status":"pass","observed_at":"actual ISO timestamp","checked_url":"https://harbor.example.com/","tls_valid":true,"evidence":"web-plan/qa/public-site.json"},{"kind":"content","name":"Expected site content","status":"pass","observed_at":"actual ISO timestamp","checked_url":"https://harbor.example.com/","expected_content_match":true,"evidence":"web-plan/qa/public-site.json"},{"kind":"flows","name":"Agreed navigation/contact flows","status":"pass","observed_at":"actual ISO timestamp","checked_url":"https://harbor.example.com/","flows_passed":true,"evidence":"web-plan/qa/public-site.json"}]}`

`status`: `prepared | deployed | domain_pending | complete | blocked`. `workers_url` may be null when a verified deployment deliberately disables workers.dev; record that choice. Custom-domain completion requires the exact requested hostname and successful valid-certificate HTTPS plus expected-site/content checks. Record real account/zone identifiers privately if needed, the previous version/config before an update, DNS/domain preflight observations, changed resources, timestamps and recovery instructions. Do not log tokens. A success CLI line alone does not prove the custom domain or functional site works. Require separate check kinds `https`, `content`, and `flows`, all at the exact requested hostname (or Worker URL hostname in workers mode), with observation timestamps, actual evidence and the corresponding booleans true. `attachment` is an optional additional kind. These declarations are checked structurally; inspect the evidence and perform real network/browser checks before writing them. Set `domain_pending` with observed DNS/TLS state and the next retry/check when propagation is unfinished.

## Resume and validation

`web-plan/run.json` is an optional progress journal: requested scope, stage statuses, artifact paths, last error and next action. It is not authorization. On resume, compare the subject, current user scope, source freshness, asset hashes, build hashes and actual Cloudflare state before skipping work. Do not rerun a successful deployment merely because the conversation resumed.

Run the bundled structural check using the actual installed skill path:

```text
python <rw-web-skill>/scripts/check_handoff.py research --project <website-workspace>
python <rw-web-skill>/scripts/check_handoff.py build --project <website-workspace>
python <rw-web-skill>/scripts/check_handoff.py deploy --project <website-workspace>
```

Research checks file/evidence/asset consistency. Build adds page/claim consistency, current artifact hashes and passing QA evidence. Deploy accepts the existing-site entry described above and adds the actual deployment record and requested-domain consistency. Passing these checks never replaces reading the evidence or checking the public website.
