Source: DESIGN-projdoc-autopublish.mdRendered: 2026-05-17 18:40 UTC — Agents read the .md; humans read the .html.

DESIGN: Projdoc Autopublish

Status: DRAFT Created: 2026-05-17 Scope: publish Fleet project design docs from local rendered HTML into the personal website under private /projdoc/* URLs.


1. Goal

Build a local autopublish path for project documentation so Fleet design docs can be read from https://fengshen.dev/projdoc/... without manually copying files between repositories.

The v1 target is deliberately narrow:

The implementation should make publishing boring: after scripts/render-design-doc.py docs/DESIGN-foo.md writes docs/DESIGN-foo.html, the local service notices the changed HTML, copies it into the Astro site, updates generated metadata, pushes to the standing branch, and lets GitHub checks plus auto-merge carry it to production.

2. Non-goals

3. Current baseline

Fleet already has a local Markdown-to-HTML design-doc renderer:

cd /Users/pinkbear/projects/fleet
python3 scripts/render-design-doc.py docs/DESIGN-dispatch-lifecycle.md

The renderer writes a sibling .html file with inline CSS and no external assets:

/Users/pinkbear/projects/fleet/docs/DESIGN-dispatch-lifecycle.md
/Users/pinkbear/projects/fleet/docs/DESIGN-dispatch-lifecycle.html

Current Fleet source docs relevant to v1 are:

/Users/pinkbear/projects/fleet/docs/DESIGN-*.html

The site repo is:

/Users/pinkbear/projects/fengshen-site

The site is Astro. The deployment target is:

https://fengshen.dev

The site default branch is:

master

The expected deploy flow is:

local Fleet docs
  -> local sync into fengshen-site
  -> push auto/projdoc-sync
  -> standing GitHub PR
  -> checks
  -> auto-merge to master
  -> GitHub Actions deploy
  -> Cloudflare Pages

The baseline has at least two existing legacy paths that must remain untouched in v1:

/projdoc/design/
/projdoc/rc-listener-lifecycle/

These paths may be backed by existing files or Astro routes in fengshen-site; the sync service must avoid deleting, moving, or overwriting them.

4. Published URL model

Canonical generated URLs:

/projdoc/<collection>/<slug>/

For Fleet:

/projdoc/fleet/<slug>/

Slug derivation for v1:

DESIGN-dispatch-lifecycle.html        -> dispatch-lifecycle
DESIGN-rc-listener-lifecycle.html     -> rc-listener-lifecycle
DESIGN-projdoc-autopublish.html       -> projdoc-autopublish

Generated output path in the Astro repo:

/Users/pinkbear/projects/fengshen-site/src/pages/projdoc/fleet/<slug>/index.astro

Examples:

/Users/pinkbear/projects/fengshen-site/src/pages/projdoc/fleet/dispatch-lifecycle/index.astro
  -> https://fengshen.dev/projdoc/fleet/dispatch-lifecycle/

/Users/pinkbear/projects/fengshen-site/src/pages/projdoc/fleet/projdoc-autopublish/index.astro
  -> https://fengshen.dev/projdoc/fleet/projdoc-autopublish/

Each generated Astro page should embed the rendered design-doc HTML body as the primary page content. Because the renderer produces a complete HTML document, the sync service has two viable options:

  1. Store the full rendered HTML as an inert static asset and generate an Astro wrapper that renders it in an iframe.
  2. Parse the generated HTML, extract the document body content and inlined style block, and generate a first-class Astro page.

v1 should use option 2. It avoids iframe keyboard, height, and access-control sharp edges and keeps /projdoc/fleet/<slug>/ as the real page.

The importer should extract:

The generated Astro page should include:

---
export const prerender = true;
const title = "DESIGN: Dispatch Lifecycle Primitive";
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex,nofollow" />
    <title>{title}</title>
    <style is:inline set:html={styleText} />
  </head>
  <body set:html={bodyHtml} />
</html>

Implementation note: the exact Astro syntax can be adjusted to the site's current conventions, but generated pages must be static, dependency-light, and not require client JavaScript.

The legacy paths stay outside the generated namespace:

/projdoc/design/                  # legacy, untouched
/projdoc/rc-listener-lifecycle/   # legacy, untouched
/projdoc/fleet/...                # v1 generated namespace

5. Local config

Add a local config file in the Fleet repo:

/Users/pinkbear/projects/fleet/.projdoc-sync.json

Recommended v1 schema:

{
  "version": 1,
  "collection": "fleet",
  "source_dir": "/Users/pinkbear/projects/fleet/docs",
  "source_glob": "DESIGN-*.html",
  "site_repo": "/Users/pinkbear/projects/fengshen-site",
  "site_branch": "master",
  "sync_branch": "auto/projdoc-sync",
  "poll_seconds": 60,
  "generated_root": "src/pages/projdoc/fleet",
  "directory_page": "src/pages/projdoc/index.astro",
  "manifest_path": "src/content/projdoc/generated/fleet.manifest.json",
  "preserve_paths": [
    "src/pages/projdoc/design",
    "src/pages/projdoc/rc-listener-lifecycle"
  ]
}

Config rules:

If the config file is missing, the sync command can use the v1 defaults listed above. The launchd plist should pass the config path explicitly so future repos can reuse the command without recompilation.

6. Generated manifest

The sync service writes a generated manifest in the website repo:

/Users/pinkbear/projects/fengshen-site/src/content/projdoc/generated/fleet.manifest.json

Recommended schema:

{
  "version": 1,
  "collection": "fleet",
  "source_root": "/Users/pinkbear/projects/fleet/docs",
  "generated_at": "2026-05-17T18:00:00Z",
  "items": [
    {
      "slug": "dispatch-lifecycle",
      "title": "DESIGN: Dispatch Lifecycle Primitive",
      "source_path": "/Users/pinkbear/projects/fleet/docs/DESIGN-dispatch-lifecycle.html",
      "source_sha256": "abc123...",
      "source_mtime": "2026-05-15T23:55:12Z",
      "url_path": "/projdoc/fleet/dispatch-lifecycle/",
      "generated_path": "src/pages/projdoc/fleet/dispatch-lifecycle/index.astro"
    }
  ]
}

The manifest has three jobs:

Manifest rules:

The sync service should also keep a local state file outside the website repo:

~/.fleet/projdoc-sync/state.json

Recommended local state:

{
  "version": 1,
  "last_success_at": "2026-05-17T18:00:05Z",
  "last_pushed_head": "b9f3...",
  "last_source_hashes": {
    "/Users/pinkbear/projects/fleet/docs/DESIGN-dispatch-lifecycle.html": "abc123..."
  }
}

This state is an optimization only. The generated manifest in the website repo remains the reviewable source of what the service published.

7. /projdoc/ directory page

/projdoc/ must be a clickable landing page, not a blank route or redirect.

Recommended generated path:

/Users/pinkbear/projects/fengshen-site/src/pages/projdoc/index.astro

Page behavior:

Directory data source:

src/content/projdoc/generated/*.manifest.json

The directory page should be generated or updated by the sync service in v1. That keeps the landing page stable even if the Astro site does not yet have a content-collection setup.

Minimum directory rendering:

---
import fleetManifest from "../../content/projdoc/generated/fleet.manifest.json";

const collections = [
  { name: "Fleet", manifest: fleetManifest },
];
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex,nofollow" />
    <title>Project Docs</title>
  </head>
  <body>
    <main>
      <h1>Project Docs</h1>
      {collections.map(({ name, manifest }) => (
        <section>
          <h2>{name}</h2>
          <ul>
            {manifest.items.map((item) => (
              <li><a href={item.url_path}>{item.title}</a></li>
            ))}
          </ul>
        </section>
      ))}
    </main>
  </body>
</html>

The final implementation should match the existing site's visual system, but the information architecture is fixed: /projdoc/ is the directory, /projdoc/fleet/<slug>/ is the generated Fleet namespace.

8. Sync service

The sync service is a local command plus a macOS launchd plist.

Recommended command name:

projdoc-sync

Recommended command location, if implemented inside Fleet:

/Users/pinkbear/projects/fleet/cmd/projdoc-sync

Recommended local invocation:

/Users/pinkbear/projects/fleet/bin/projdoc-sync \
  --config /Users/pinkbear/projects/fleet/.projdoc-sync.json \
  --once

launchd should run a polling loop while the Mac user session is active. Use a user agent, not a system daemon:

~/Library/LaunchAgents/dev.fengshen.projdoc-sync.plist

Recommended plist shape:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>dev.fengshen.projdoc-sync</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/pinkbear/projects/fleet/bin/projdoc-sync</string>
    <string>--config</string>
    <string>/Users/pinkbear/projects/fleet/.projdoc-sync.json</string>
    <string>--once</string>
  </array>
  <key>StartInterval</key>
  <integer>60</integer>
  <key>RunAtLoad</key>
  <true/>
  <key>WorkingDirectory</key>
  <string>/Users/pinkbear/projects/fleet</string>
  <key>StandardOutPath</key>
  <string>/Users/pinkbear/Library/Logs/projdoc-sync.out.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/pinkbear/Library/Logs/projdoc-sync.err.log</string>
</dict>
</plist>

Start/stop commands:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.fengshen.projdoc-sync.plist
launchctl kickstart -k gui/$(id -u)/dev.fengshen.projdoc-sync
launchctl bootout gui/$(id -u)/dev.fengshen.projdoc-sync

Service requirements:

~/.fleet/projdoc-sync/sync.lock

9. Sync algorithm

High-level algorithm:

load config
acquire local lock
scan Fleet docs/DESIGN-*.html
derive item list
read website generated manifest
compare source hashes
if no changes:
  exit 0
prepare website worktree
generate Astro doc pages
generate manifest
generate /projdoc/ directory
delete stale generated Fleet pages only
run website checks
commit on auto/projdoc-sync
push branch
ensure standing PR exists
ensure auto-merge is enabled
release lock

Detailed steps:

  1. Load and validate config. - Resolve absolute source and site paths. - Confirm source dir exists. - Confirm site repo exists and is a Git repo. - Confirm generated paths resolve inside site repo.

  2. Acquire lock. - Use flock or an atomic create of ~/.fleet/projdoc-sync/sync.lock. - Include PID, start time, and hostname in lock metadata. - Stale lock cleanup is allowed only when the PID no longer exists.

  3. Scan sources. - Glob DESIGN-*.html under /Users/pinkbear/projects/fleet/docs. - Ignore directories. - Ignore files that do not match exactly. - Compute SHA-256 content hash. - Extract title from <title> or first <h1>. - Derive slug by stripping DESIGN- and .html, lowercasing, and validating URL-safe characters.

  4. Read prior manifest. - If missing, treat as empty. - If malformed, fail the run before modifying site files.

  5. Detect changes. - Compare sorted source hashes plus generated metadata. - If unchanged, exit 0 without Git operations.

  6. Prepare site branch. - Fetch origin. - Ensure local master tracks origin/master. - Create or reset local auto/projdoc-sync from origin/master unless the branch has unpublished local changes from a previous failed run. - If the branch exists remotely, prefer rebasing the generated commit on current origin/master so the standing PR stays current.

  7. Generate files. - For each source item, write:

src/pages/projdoc/fleet/<slug>/index.astro
src/content/projdoc/generated/fleet.manifest.json
src/pages/projdoc/index.astro
  1. Delete stale generated Fleet pages. - Only delete directories under:
src/pages/projdoc/fleet/
  1. Run website checks. - Use the site's existing package manager and scripts. - Minimum expected checks:
npm run build
  1. Commit.
    • If no Git diff remains, exit 0.
    • Commit message:
sync projdoc
- Include generated source count and source hashes in the commit body if useful.
  1. Push and PR.

    • Push auto/projdoc-sync.
    • If the standing PR does not exist, create it against master.
    • Enable auto-merge.
    • Do not merge locally.
  2. Persist state.

    • Write ~/.fleet/projdoc-sync/state.json after successful push.
    • Record PR URL if available.

The command should be idempotent. Running it twice with the same inputs should produce no second commit.

10. GitHub + deploy

Branch model:

base:  master
head:  auto/projdoc-sync

PR model:

Auto-merge requirements:

Deploy model:

auto/projdoc-sync PR
  -> checks pass
  -> auto-merge to master
  -> GitHub Actions deploy
  -> Cloudflare Pages
  -> https://fengshen.dev/projdoc/fleet/<slug>/

The local service does not need Cloudflare credentials in v1. Cloudflare deployment remains centralized in GitHub Actions.

Operational guardrails:

11. Privacy

Primary privacy control:

Cloudflare Access policy for /projdoc/*

Requirements:

Backup controls:

<meta name="robots" content="noindex,nofollow">
X-Robots-Tag: noindex, nofollow

Important limitation:

noindex,nofollow is not access control. It only reduces indexing risk if a private URL becomes reachable. It must not be treated as sufficient privacy.

Content rules:

Review implication:

The standing PR is the last reviewable boundary before production. Diffs should make it easy to see exactly which documents changed.

12. Failure modes

Failure Expected behavior
Source dir missing Exit non-zero, log path, make no site changes.
No DESIGN-*.html files Generate empty Fleet collection only if explicitly allowed; default should fail to avoid accidental wipe.
Duplicate slug Exit non-zero, list both source files, make no site changes.
Malformed source HTML Exit non-zero before writing generated page.
Existing generated manifest malformed Exit non-zero before modifying site files.
Website repo dirty with unrelated files Exit non-zero, list dirty paths, make no changes.
Website generated files dirty from prior failed sync Allow overwrite only under generated paths.
Branch checkout/rebase conflict Abort rebase, leave branch unchanged if possible, log remediation.
Website build fails Leave generated files for inspection, do not commit or push.
GitHub push fails Keep local commit, log failure, retry next launchd run.
PR creation fails Keep branch pushed if push succeeded, log failure, retry next run.
Auto-merge enable fails Keep PR open, log failure, retry next run.
Cloudflare deploy fails Do not handle locally; GitHub/Cloudflare status is the source of truth.
Cloudflare Access missing Treat as launch blocker for production exposure; do not rely on noindex.
launchd overlaps runs Second run exits 0 after seeing lock.
Mac asleep/offline No sync occurs; next active session run catches up.

Dirty working tree policy in fengshen-site:

Stale deletion policy:

13. Tests

Unit tests for slug and scan behavior:

Unit tests for HTML import:

Unit tests for manifest generation:

Unit tests for path safety:

Integration tests with temporary repos:

Git/PR tests can use fakes or a local bare repo:

Manual verification before enabling launchd:

cd /Users/pinkbear/projects/fleet
python3 scripts/render-design-doc.py docs/DESIGN-projdoc-autopublish.md

/Users/pinkbear/projects/fleet/bin/projdoc-sync \
  --config /Users/pinkbear/projects/fleet/.projdoc-sync.json \
  --once

cd /Users/pinkbear/projects/fengshen-site
npm run build
git diff --stat

Manual production verification after the first merge:

14. Coordinator task split

Recommended implementation split:

  1. P1: Website generated namespace scaffold - Repo: /Users/pinkbear/projects/fengshen-site - Add src/pages/projdoc/fleet/.gitkeep if needed. - Add or reserve src/content/projdoc/generated/. - Confirm existing /projdoc/design/ and /projdoc/rc-listener-lifecycle/ files/routes and mark them as preserve paths. - Add Cloudflare Access configuration task if not already configured outside repo. - Tests: site build.

  2. P1: Sync command core - Repo: /Users/pinkbear/projects/fleet - Implement config load, scan, slugging, HTML extraction, manifest generation, and Astro page generation. - Keep GitHub operations behind an interface so unit/integration tests can run without network. - Tests: unit tests plus temp-dir integration tests.

  3. P1: Git branch and PR publisher - Repo: /Users/pinkbear/projects/fleet - Implement website repo cleanliness checks, branch update, commit, push, PR create/update, and auto-merge enable. - Use gh or GitHub API consistently; prefer gh for local operator environment if already standard. - Tests: local bare repo or command-fake tests.

  4. P1: Directory page generation - Repo: /Users/pinkbear/projects/fleet plus generated output in /Users/pinkbear/projects/fengshen-site - Generate /projdoc/ from manifest data. - Ensure Fleet collection links target /projdoc/fleet/<slug>/. - Tests: generated Astro snapshot and site build.

  5. P1: launchd packaging - Repo: /Users/pinkbear/projects/fleet - Add plist template, install/uninstall commands or documentation, lock handling, and logs. - Poll every 60s while the Mac user session is active. - Tests: plist render test; manual launchctl bootstrap verification.

  6. P1: First production dry run - Run the sync once manually. - Confirm generated diff. - Push auto/projdoc-sync. - Open standing PR. - Confirm checks, auto-merge, deploy, and Cloudflare Access. - Verify legacy paths unchanged.

  7. P2: Polish after v1 - Add optional docs index sorting by updated time. - Add multiple collections. - Add a search page if the private doc set grows. - Add deploy status notifications. - Add stale source warnings when Markdown is newer than rendered HTML.

Non-goal for all tasks: