Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 13 additions & 4 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
deferrals, known limitations). Keep README claims backed by tests or
evidence.
- Web (`apps/web`): content is currently hand-duplicated in three places
(backlog §F2) — if you change CLI behavior, grep the site
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
update all copies.
- Web (`apps/web`): rendered content derives from `README.md`,
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
those sources when CLI behavior changes; web lint checks their provenance.
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
`ci:`, scope in parens like `fix(macos):`).

Expand All@@ -130,3 +129,13 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
with a decision entry.

## Website deployment

Vercel deploys `apps/web` from the repository root using
[`vercel.json`](vercel.json). The production branch is `main`, and the
canonical production URL is <https://headless-web-pi.vercel.app>. Keep the
Vercel for GitHub integration enabled for pull-request previews and preview-URL
comments. Do not add a second deployment workflow that can race the integration.
Hosting setup, verification, rollback, and the custom-domain decision are in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .",
"brand": "node scripts/render-brand.mjs"
},
"dependencies": {
Expand Down
56 changes: 56 additions & 0 deletions apps/web/scripts/validate-deployment-config.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const root = resolve(import.meta.dirname, "../../..");
const read = (path) => readFile(resolve(root, path), "utf8");
const config = JSON.parse(await read("vercel.json"));

assert.deepEqual(config, {
$schema: "https://openapi.vercel.sh/vercel.json",
framework: "nextjs",
buildCommand: "pnpm --filter @headless/web build",
devCommand: "pnpm --filter @headless/web exec next dev --port $PORT",
outputDirectory: "apps/web/.next",
});

const productionUrl = "https://headless-web-pi.vercel.app";
const [metadata, deploymentDocs, agentRules, nextConfig, rootPackage, lockfile] =
await Promise.all([
read("apps/web/lib/site-metadata.ts"),
read("docs/DEPLOYMENT.md"),
read("AGENTS.md"),
read("apps/web/next.config.ts"),
read("package.json"),
read("pnpm-lock.yaml"),
]);

const packageJson = JSON.parse(rootPackage);
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
assert.equal(config.installCommand, undefined);

for (const source of [metadata, deploymentDocs, agentRules]) {
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
}

assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
for (const header of [
"Content-Security-Policy",
"Permissions-Policy",
"Referrer-Policy",
"X-Content-Type-Options",
"X-Frame-Options",
]) {
assert.match(nextConfig, new RegExp(`key: "${header}"`));
}
for (const directive of [
"base-uri 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
]) {
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
}

console.log("Vercel deployment configuration is consistent");
65 changes: 65 additions & 0 deletions docs/DEPLOYMENT.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
# Website deployment

The marketing and documentation site is deployed to Vercel from this monorepo.
The repository configuration in [`vercel.json`](../vercel.json) is the source
of truth for framework detection, build and development commands, and output
location. Vercel derives pnpm from the root lockfile. Do not add an install
override with plain `pnpm install`: Vercel uses its oldest available pnpm
runtime for that override, while this repository requires pnpm 9 or newer.

## Production contract

- **Production branch:** `main`.
- **Production URL:** <https://headless-web-pi.vercel.app>.
- **Project root:** the repository root, not `apps/web`.
- **Application:** `apps/web` (`@headless/web`).
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
`vercel.json`, where they could drift from local and CI builds.

The Vercel project alias is the canonical domain for now. The LockInTime
organization does not publish a verifiable custom domain in repository or
organization metadata, so this project must not claim one. A custom domain can
replace the alias only after a maintainer confirms control of its DNS. That
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
homepage, this document, and the Vercel production-domain assignment together.

## GitHub integration

Connect the `LockInTime/headless` repository through Vercel for GitHub with
these project settings:

1. Leave Root Directory empty so Vercel reads the root `vercel.json` and the
workspace lockfile.
2. Set the production branch to `main`.
3. Keep preview deployments enabled for pull requests and branch pushes.
4. Keep pull-request comments enabled so each PR receives its immutable preview
URL. Keep deployment status events enabled so the URL also appears in the
GitHub deployment timeline.
5. Do not add a second token-driven GitHub Actions deployment. Two independent
deployers can race production aliases and make rollback history ambiguous.

The integration is an account-level control and cannot be stored in git. If a
PR has no Vercel deployment or preview link, treat that as a disconnected or
disabled integration. A Vercel project maintainer must reconnect the repository
under Project Settings, Git before the PR is considered deployment-verified.

## Verification

Run the same web gates locally before pushing:

```sh
pnpm install --frozen-lockfile --filter @headless/web
pnpm --filter @headless/web lint
pnpm --filter @headless/web build
```

For a pull request, open the Vercel preview from the PR deployment entry and
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
`Referrer-Policy`, and `Permissions-Policy` headers declared in
`apps/web/next.config.ts`.

After merging, verify that the production deployment points at the merge commit
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
production deployments available for rollback. Roll back in Vercel, then
revert the faulty commit in git so repository history and production converge.
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
bootstrap, release checksums, and a multi-platform GHCR image. These paths
become user-visible with the next tag.
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
command reference, and a Docker sandbox wrapper.

Expand All@@ -118,9 +118,8 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
unimplemented.
- The latest features (capture formats, context pruning) are **unreleased** —
no tag since v1.0.2 (2026-07-19).
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
- The website's benchmark numbers, docs prose, and commands are hand-copied in
three places each and will drift; the site has no deploy pipeline.
- The website deployment configuration is versioned, but GitHub-to-Vercel
preview and production deployments remain unverified.
- Windows is not supported.
- A list of real code defects (thread-safety on shutdown, oversized `qa
report` responses, `@eN` ref invalidation surprises, host code duplication)
Expand DownExpand Up@@ -265,7 +264,8 @@ and drive Headless with zero manual prompting beyond repo checkout.

### Phase 5 — Website and docs as a product surface

- Deploy `apps/web` (Vercel or static export + CDN) with CI.
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
verified alongside CI.
- Kill the three-copy content drift: docs prose and benchmark numbers come
from single sources (benchmark emits JSON; site imports it; command tables
generated from the CLI) (backlog §F).
Expand Down
9 changes: 1 addition & 8 deletions docs/roadmap/improvements-backlog.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -408,14 +408,7 @@ Owner-decided scope: package managers, no hosted service.

## §F — Website & docs (Phase 5)

- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
GitHub integration, but nothing in the tree records that: no `vercel.json`,
no deploy docs, no preview-URL comment on PRs, and the temporary
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
reproducible and reviewable — check in the project config, document the
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — **Repository side ready:** root deployment settings are versioned and linted, the GitHub preview and rollback contract is documented, security headers remain in Next.js, and the proven Vercel project alias is canonical until the organization publishes a controlled custom domain. **Pending account verification:** reconnect the Vercel for GitHub integration and confirm a PR preview plus a `main` production deployment before checking this item off.
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose
Expand Down
7 changes: 7 additions & 0 deletions vercel.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"buildCommand": "pnpm --filter @headless/web build",
"devCommand": "pnpm --filter @headless/web exec next dev --port $PORT",
"outputDirectory": "apps/web/.next"
}