2026 10 19 doit front page - xopherdeep/do-it-for-the-xp GitHub Wiki

The doit.forthexp.com front page on Magic Hat — implementation plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Publish a new Magic Hat front page for doit.forthexp.com from this repo with one idempotent script, rehearsed on the local Docker WordPress first.

Architecture: Pure helpers (tools/site/markup.ts, tools/site/publishPlan.ts) are unit-tested under jest; a thin Node script (tools/site/publish-doit.ts) does the REST calls (media, page, template override, settings) and writes a report that --rollback reads. The page markup is a static block document built from Magic Hat's own patterns. A Cypress spec checks the rendered page on the local site and again on live.

Tech Stack: TypeScript run by Node 22 (type stripping, relative imports carry .ts), fetch/FormData from node: globals, WordPress REST (wp/v2) with Basic auth over Application Passwords, jest via chemx test, Cypress 8 (--browser chrome), WP-CLI through docker exec u-wordpress wp --allow-root.

Spec: docs/superpowers/specs/2026-10-19-doit-front-page-design.md

Global Constraints

  • Copy and structure come from spec section 1 verbatim; no invented numbers, no prices, no exclamation marks, no crypto.
  • Every <img> has non-empty alt; every href is absolute, or starts with / or #.
  • Phone-first: readable at 390 px with a 16 px gutter and no horizontal scroll.
  • Credentials are read from DOIT_WP_ENV_FILE (default ../Xophz-COMPASS/.env.local) and never printed or logged; the report file tools/site/.doit-publish-report.json is gitignored.
  • Nothing is deleted on the site: the previous front page becomes a draft, media and pages stay.
  • No new npm dependencies. Node 22.23 is on PATH.
  • Tests run through chemx only: chemx test tests/unit/<spec> and chemx typecheck. Never bare pnpm test.
  • Shared checkout: stage only your own paths (git add <paths>), commit with git commit -- <paths>, never git add -A, never git stash. Commit messages end with Co-Authored-By: Claude Fable 5.1 <[email protected]>.
  • The live publish (Task 5) runs only after the owner has approved the rehearsal from Task 4.

Review Focus

  1. A second run of the script must change nothing: no second page, no second media item, no second template, and no drafting when the front page is already home. Pinned in Task 3 (planFrontPage) and Task 4 step 8.
  2. A leftover {{placeholder}} must stop the run before any write. Pinned in Task 1 (renderMarkup throws) and Task 3 step 7.
  3. A home page that exists as a draft or in the trash must be reused and republished, never duplicated as home-2. Pinned in Task 3 (statusQuery, upsertPage).
  4. Wrong or missing credentials must fail at the first request with the site's error code and write nothing. Pinned in Task 3 (loadAuth, the users/me check).
  5. A dead link in the markup must be listed in the report with its status and make the script exit 1, with the page left published. Pinned in Task 3 (checkLinks).

Task 1: Markup helpers

Files:

  • Create: tools/site/markup.ts
  • Test: tests/unit/siteMarkup.spec.ts

Interfaces:

  • Produces:

    • renderMarkup(template: string, vars: Record<string, string>): string — replaces every {{key}} (keys may contain : and -, e.g. media:hometown, media-id:hometown, app); throws Error("Unknown placeholder: {{x}}") for a key not in vars.
    • findPlaceholders(html: string): string[] — the distinct {{…}} keys still present, in order of appearance.
    • collectHrefs(html: string): string[] — every href="…" value, distinct, in order.
    • checkMarkup(html: string): string[] — problems, empty when clean: placeholder {{x}}; invented number: "<text>" for a digit run followed within 3 words by users, achievements or tasks (case-insensitive); relative href: <href> for an href that is not ^https?://, ^/ or ^#; img without alt: <src> for an <img> whose alt is missing or empty.
  • Step 1: Write the failing tests

import { renderMarkup, findPlaceholders, collectHrefs, checkMarkup } from "../../tools/site/markup";

describe("renderMarkup", () => {
  it("replaces media, media-id and app placeholders", () => {
    const out = renderMarkup('<img src="{{media:hometown}}" data-id="{{media-id:hometown}}"><a href="{{app}}">', {
      "media:hometown": "https://s/h.jpg", "media-id:hometown": "12", app: "https://app.doit.forthexp.com" });
    expect(out).toBe('<img src="https://s/h.jpg" data-id="12"><a href="https://app.doit.forthexp.com">');
  });
  it("throws on an unknown placeholder", () => {
    expect(() => renderMarkup("{{media:nope}}", {})).toThrow("Unknown placeholder: {{media:nope}}");
  });
});
describe("findPlaceholders", () => {
  it("lists distinct keys in order", () => {
    expect(findPlaceholders("{{a}} {{b}} {{a}}")).toEqual(["a", "b"]);
  });
});
describe("collectHrefs", () => {
  it("returns distinct hrefs in order", () => {
    expect(collectHrefs('<a href="/x"><a href="#y"><a href="/x">')).toEqual(["/x", "#y"]);
  });
});
describe("checkMarkup", () => {
  it("is empty for clean markup", () => {
    expect(checkMarkup('<a href="https://a/"><img src="/i.jpg" alt="Hometown">')).toEqual([]);
  });
  it("flags invented numbers, relative hrefs, missing alt and placeholders", () => {
    const problems = checkMarkup('<p>Over 5000 achievements unlocked</p><a href="page"><img src="/i.jpg" alt="">{{app}}');
    expect(problems).toEqual(expect.arrayContaining([
      expect.stringMatching(/^invented number: /), "relative href: page", "img without alt: /i.jpg", "placeholder {{app}}" ]));
    expect(problems).toHaveLength(4);
  });
});
  • Step 2: Run the spec to see it fail

Run: chemx test tests/unit/siteMarkup.spec.ts Expected: FAIL, module tools/site/markup not found.

  • Step 3: Implement the four functions in tools/site/markup.ts

Plain regexes; no imports. Export only the four names above.

  • Step 4: Run the spec to see it pass, and confirm Node loads the module

Run: chemx test tests/unit/siteMarkup.spec.ts Expected: PASS, 6 tests. Run: node --input-type=module -e 'import("./tools/site/markup.ts").then(m => console.log(Object.keys(m).sort().join(",")))' Expected: checkMarkup,collectHrefs,findPlaceholders,renderMarkup

  • Step 5: Commit
git add tools/site/markup.ts tests/unit/siteMarkup.spec.ts
git commit -- tools/site/markup.ts tests/unit/siteMarkup.spec.ts -m "feat(site): markup helpers for the published front page"

Task 2: The page markup

Files:

  • Create: tools/site/doit-front-page.html
  • Create: tools/site/front-page-template.html
  • Test: tests/unit/siteFrontPage.spec.ts

Interfaces:

  • Consumes: checkMarkup, findPlaceholders (Task 1).

  • Produces: doit-front-page.html, a block document using only these placeholders: {{app}}, and {{media:<name>}} / {{media-id:<name>}} for the eight names hometown, battle, victory, temple-map, beast-log, boss, small-key, my-home. front-page-template.html is the spec's section 3 markup, no placeholders.

  • Step 1: Write the failing test

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { checkMarkup, findPlaceholders, renderMarkup } from "../../tools/site/markup";

const NAMES = ["hometown", "battle", "victory", "temple-map", "beast-log", "boss", "small-key", "my-home"];
const page = readFileSync(join(__dirname, "../../tools/site/doit-front-page.html"), "utf8");
const vars: Record<string, string> = { app: "https://app.doit.forthexp.com" };
for (const n of NAMES) { vars[`media:${n}`] = `https://site/${n}.jpg`; vars[`media-id:${n}`] = "1"; }

describe("doit-front-page.html", () => {
  it("uses only the agreed placeholders", () => {
    expect(findPlaceholders(page).every((k) => k in vars)).toBe(true);
  });
  it("renders clean markup", () => {
    expect(checkMarkup(renderMarkup(page, vars))).toEqual([]);
  });
  it("carries the spec's copy and links", () => {
    const html = renderMarkup(page, vars);
    expect(html).toContain("Do It For The XP");
    expect(html).toContain("Chores become quests. Kids become heroes. Parents are the Game Masters.");
    expect(html).toContain('id="how-it-works"');
    expect(html).toContain('href="https://app.doit.forthexp.com"');
    expect(html).toContain('href="https://www.forthexp.com"');
    expect(html).toContain('href="/cafeteria-board/feature-requests/"');
    expect(html).toContain('href="https://github.com/xopherdeep/do-it-for-the-xp"');
    expect(html).toContain('<iframe');
    const text = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<[^>]+>/g, " ");
    expect(text).not.toMatch(/\$\d|per month|lifetime/i);
    expect(text).not.toContain("!");
  });
  it("ships the template override from the spec", () => {
    const tpl = readFileSync(join(__dirname, "../../tools/site/front-page-template.html"), "utf8");
    expect(tpl).toContain('wp:template-part {"slug":"header"');
    expect(tpl).toContain("wp:post-content");
    expect(tpl).not.toContain("wp:xophz-magic-hat/hero");
  });
});
  • Step 2: Run the spec to see it fail

Run: chemx test tests/unit/siteFrontPage.spec.ts Expected: FAIL, doit-front-page.html not found.

  • Step 3: Write tools/site/doit-front-page.html

Build it from Magic Hat's pattern files in ../Xophz-COMPASS/wp-content/themes/xophz-magic-hat/patterns/ (strip each file's PHP header comment; the rest is block HTML): hero.php for section 1, features.php for section 2 (give the outer group "anchor":"how-it-works" and id="how-it-works"), phone-slider.php for section 3, cta.php for section 5; sections 4 and 6 are a wp:group with wp:heading/wp:paragraph/wp:image blocks in the same spacing presets. Keep every class and style the patterns use; change only copy, links and media. Copy is spec section 1, exactly. In the slider, slide 1 keeps the pattern's <iframe class="mh-phone-iframe"> with src="{{app}}" and title="Do It For The XP, live"; slides 2 to 5 replace the <iframe> element with <img class="mh-phone-iframe" src="{{media:battle}}" alt="A chore beast approaches"> and so on (victory "Victory screen paying out XP, GP and AP", temple-map "Wind Temple dungeon map", beast-log "Beast Log, today's chores as beasts"). Image blocks use {"id":{{media-id:x}}} in the block comment and {{media:x}} in src. Alt text for the rest: hometown "Hometown map with the XP and GP guide", boss "A boss fight", small-key "A small key found in a temple", my-home "My Home".

  • Step 4: Write tools/site/front-page-template.html as spec section 3, byte for byte.

  • Step 5: Run the spec to see it pass

Run: chemx test tests/unit/siteFrontPage.spec.ts Expected: PASS, 4 tests.

  • Step 6: Commit
git add tools/site/doit-front-page.html tools/site/front-page-template.html tests/unit/siteFrontPage.spec.ts
git commit -- tools/site/doit-front-page.html tools/site/front-page-template.html tests/unit/siteFrontPage.spec.ts -m "feat(site): the doit front page as Magic Hat blocks"

Task 3: The publish script

Files:

  • Create: tools/site/publishPlan.ts
  • Create: tools/site/publish-doit.ts
  • Modify: .gitignore (add tools/site/.doit-publish-report.json)
  • Test: tests/unit/sitePublish.spec.ts

Interfaces:

  • Consumes: Task 1 helpers; Task 2 files.

  • Produces (in publishPlan.ts, pure):

    • pickAuth(site: string, env: Record<string, string>): { user: string; password: string } — host mycompass, localhost or 127.0.0.1 → LOCAL_WP_USER/LOCAL_WP_APP_PASSWORD, else DOIT_WP_USER/DOIT_WP_APP_PASSWORD; throws Error("Missing <KEY> in env file") naming the first missing key.
    • parseEnvFile(text: string): Record<string, string> — KEY=value lines, # comments and blanks ignored, surrounding quotes stripped, value kept verbatim otherwise (spaces allowed).
    • statusQuery(): string — "publish,draft,pending,private,future,trash".
    • planFrontPage(previousId: number, homeId: number): { draftPrevious: number | null } — null when equal or previousId <= 0.
    • linksToCheck(hrefs: string[], site: string): string[] — drops #…, resolves /… against site, keeps absolute, distinct.
    • SCREENSHOTS: Array<{ name: string; file: string; alt: string }> — the eight names of Task 2, file = .github/readme/<name>.jpg, alt as in Task 2; media slug is doit-shot-<name>.
  • Produces (in publish-doit.ts): CLI node tools/site/publish-doit.ts --site <url> [--dry-run] [--rollback] [--env-file <path>]; exit 0 on success, 1 when any checked link is not 200 or a step fails. Writes tools/site/.doit-publish-report.json as spec section 4 step 8 ({site, at, previousShowOnFront, previousFrontPage, page:{id,url}, template:{id}, media:{name:{id,url}}, links:[{href,status}]}).

  • Step 1: Write the failing tests for the pure parts

import { pickAuth, parseEnvFile, statusQuery, planFrontPage, linksToCheck, SCREENSHOTS } from "../../tools/site/publishPlan";

describe("parseEnvFile", () => {
  it("reads keys, ignores comments, keeps spaces, strips quotes", () => {
    expect(parseEnvFile('# c\nA=1\nB="two words"\nC=x y z\n\n')).toEqual({ A: "1", B: "two words", C: "x y z" });
  });
});
describe("pickAuth", () => {
  const env = { DOIT_WP_USER: "d", DOIT_WP_APP_PASSWORD: "dp", LOCAL_WP_USER: "l", LOCAL_WP_APP_PASSWORD: "lp" };
  it("uses the local pair for mycompass and the doit pair otherwise", () => {
    expect(pickAuth("http://mycompass", env)).toEqual({ user: "l", password: "lp" });
    expect(pickAuth("https://doit.forthexp.com", env)).toEqual({ user: "d", password: "dp" });
  });
  it("names the missing key", () => {
    expect(() => pickAuth("https://doit.forthexp.com", { DOIT_WP_USER: "d" })).toThrow("Missing DOIT_WP_APP_PASSWORD in env file");
  });
});
describe("planFrontPage", () => {
  it("drafts the previous page only when it differs", () => {
    expect(planFrontPage(1141369, 5)).toEqual({ draftPrevious: 1141369 });
    expect(planFrontPage(5, 5)).toEqual({ draftPrevious: null });
    expect(planFrontPage(0, 5)).toEqual({ draftPrevious: null });
  });
});
describe("linksToCheck", () => {
  it("resolves site-relative links, drops anchors, dedupes", () => {
    expect(linksToCheck(["#how-it-works", "/blog/", "https://a/", "/blog/"], "https://doit.forthexp.com"))
      .toEqual(["https://doit.forthexp.com/blog/", "https://a/"]);
  });
});
it("statusQuery includes trash", () => { expect(statusQuery()).toBe("publish,draft,pending,private,future,trash"); });
it("SCREENSHOTS lists the eight agreed names with files and alt text", () => {
  expect(SCREENSHOTS.map((s) => s.name)).toEqual(["hometown", "battle", "victory", "temple-map", "beast-log", "boss", "small-key", "my-home"]);
  expect(SCREENSHOTS.every((s) => s.file === `.github/readme/${s.name}.jpg` && s.alt.length > 0)).toBe(true);
});
  • Step 2: Run the spec to see it fail

Run: chemx test tests/unit/sitePublish.spec.ts Expected: FAIL, module not found.

  • Step 3: Implement tools/site/publishPlan.ts with the signatures above. No I/O.

  • Step 4: Run the spec to see it pass

Run: chemx test tests/unit/sitePublish.spec.ts Expected: PASS, 7 tests.

  • Step 5: Implement tools/site/publish-doit.ts in this order, each as a small function taking (auth, …):
  1. main(): parse flags; --site required; read the env file (--env-file, else DOIT_WP_ENV_FILE, else ../Xophz-COMPASS/.env.local relative to the repo root); pickAuth. Stop with the message if a screenshot file is missing on disk.
  2. rest(auth, method, path, body?) → JSON; Authorization: Basic <base64 user:password>; a non-2xx throws Error("<status> <code>: <message>") built from the site's JSON. restForm for multipart (FormData with a Blob of the file, title, slug, alt_text).
  3. Auth check: GET /wp/v2/users/me?context=edit; print the display name and roles only.
  4. ensureMedia(auth, shot): GET /wp/v2/media?slug=doit-shot-<name>&per_page=1; reuse {id, source_url} when found, else upload.
  5. Render the page with renderMarkup; if findPlaceholders(html) is non-empty or checkMarkup(html) returns problems, stop before any write and print them (this check runs in --dry-run too, after the media lookups, which are reads).
  6. upsertPage(auth, "home", "Do It For The XP", html): GET /wp/v2/pages?slug=home&status=<statusQuery()>&per_page=1; found → POST /wp/v2/pages/<id> with {status:"publish", title, content, comment_status:"closed"} (a trashed page republishes the same way); none → POST /wp/v2/pages with {slug:"home", …}.
  7. upsertTemplate(auth): POST /wp/v2/templates/xophz-magic-hat//front-page with {content, title:"Front Page", status:"publish"} (WordPress makes the override on first write and updates it after); report its id.
  8. Settings: read GET /wp/v2/settings first (remember show_on_front, page_on_front), then POST /wp/v2/settings with {show_on_front:"page", page_on_front:<home id>, title:"Do It For The XP", description:"A family chore app that plays like a SNES-era RPG."}.
  9. planFrontPage(previous, homeId).draftPrevious → POST /wp/v2/pages/<prev> {status:"draft"}.
  10. checkLinks: HEAD (fall back to GET on 405) each of linksToCheck(collectHrefs(html), site) plus the page URL; collect {href, status}.
  11. Write the report; print the one-screen summary (created / updated / reused per item, the page URL, the links with a status other than 200); exit 1 if any link is not 200.
  12. --dry-run: steps 1 to 5 and the settings read only; print what 6 to 10 would do; no POST.
  13. --rollback: read the report; POST /wp/v2/pages/<previousFrontPage> {status:"publish"} when not null; POST /wp/v2/settings with {show_on_front:<previousShowOnFront>, page_on_front:<previousFrontPage>}; print what changed; never touches the theme, media or the home page.

Guard the entry point with if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main() so the module can be imported.

  • Step 6: Add the report file to .gitignore (one line: tools/site/.doit-publish-report.json).

  • Step 7: Prove the placeholder guard and the auth guard without a site

Run: DOIT_WP_ENV_FILE=/dev/null node tools/site/publish-doit.ts --site https://doit.forthexp.com --dry-run Expected: exits 1 with Missing DOIT_WP_USER in env file, no request made. Run: node tools/site/publish-doit.ts --site http://mycompass --dry-run (env file lacks LOCAL_WP_* until Task 4) Expected: exits 1 with Missing LOCAL_WP_USER in env file.

  • Step 8: Typecheck and commit

Run: chemx typecheck Expected: 0 errors (tools/ is outside tsconfig include; the specs compile under ts-jest).

git add tools/site/publishPlan.ts tools/site/publish-doit.ts tests/unit/sitePublish.spec.ts .gitignore
git commit -- tools/site/publishPlan.ts tools/site/publish-doit.ts tests/unit/sitePublish.spec.ts .gitignore -m "feat(site): publish-doit script: media, page, template override, settings, report, rollback"

Task 4: Local rehearsal

Files:

  • Create: tests/e2e/specs/site.spec.js
  • Output: tests/e2e/screenshots/site/*.png (not committed unless already tracked)
  • Modify (outside the repo, gitignored): ../Xophz-COMPASS/.env.local gains LOCAL_WP_USER and LOCAL_WP_APP_PASSWORD

Interfaces:

  • Consumes: the script (Task 3).

  • Produces: the rehearsal report and screenshots for the owner's review; the Cypress spec reused in Task 5.

  • Step 1: Make Application Passwords work on the local site

Run: docker exec u-wordpress wp --allow-root config set WP_ENVIRONMENT_TYPE local --type=constant Run: docker exec u-wordpress wp --allow-root eval 'echo wp_is_application_passwords_available() ? "y" : "n";' Expected: y. (If config set is refused, add wp-content/mu-plugins/local-application-passwords.php in the COMPASS repo returning true from the wp_is_application_passwords_available filter only when parse_url(home_url(), PHP_URL_HOST) is mycompass, localhost or 127.0.0.1, and say so in the task report.)

  • Step 2: Mint a local Application Password without echoing it
pw=$(docker exec u-wordpress wp --allow-root user application-password create admin "Claude rehearsal" --porcelain) \
 && printf 'LOCAL_WP_USER=admin\nLOCAL_WP_APP_PASSWORD=%s\n' "$pw" >> ../Xophz-COMPASS/.env.local && unset pw && echo "stored"

Expected: stored.

  • Step 3: Dry run, then publish to the local site

Run: node tools/site/publish-doit.ts --site http://mycompass --dry-run Expected: the auth line, eight media rows marked "would upload", the page, template and settings rows marked "would create/update", exit 0. Run: node tools/site/publish-doit.ts --site http://mycompass Expected: exit 0, report written, page URL http://mycompass/.

  • Step 4: Write the Cypress spec
describe("doit front page", () => {
  const sizes = [["phone", 390, 844], ["desktop", 1280, 800]];
  sizes.forEach(([name, w, h]) => {
    it(`reads at ${name} width`, () => {
      cy.viewport(w, h);
      cy.visit("/");
      cy.get("h1").first().should("contain", "Do It For The XP");
      cy.contains("a", "Play now").should("have.attr", "href", "https://app.doit.forthexp.com");
      cy.contains("a", "How it works").should("have.attr", "href", "#how-it-works");
      cy.get("#how-it-works").should("exist");
      cy.get("iframe.mh-phone-iframe").first().should("have.attr", "src", "https://app.doit.forthexp.com");
      cy.get("img").each(($img) => expect($img.attr("alt"), $img.attr("src")).to.not.be.empty);
      cy.document().then((d) => expect(d.documentElement.scrollWidth).to.be.at.most(d.documentElement.clientWidth));
      cy.screenshot(`site/front-page-${name}`, { capture: "fullPage" });
    });
  });
  it("stays legible in the dark scheme", () => {
    cy.viewport(390, 844);
    cy.wrap(Cypress.automation("remote:debugger:protocol", {
      command: "Emulation.setEmulatedMedia", params: { features: [{ name: "prefers-color-scheme", value: "dark" }] } }));
    cy.visit("/");
    cy.get("h1").first().should("be.visible");
    cy.screenshot("site/front-page-phone-dark", { capture: "fullPage" });
  });
});
  • Step 5: Run it against the local site

Run: pnpm vue-cli-service test:e2e --headless --browser chrome --url http://mycompass --spec tests/e2e/specs/site.spec.js Expected: 3 passing; tests/e2e/screenshots/site/front-page-phone.png, front-page-desktop.png and front-page-phone-dark.png written.

  • Step 6: Look at both screenshots (Read the PNGs) and fix the markup in tools/site/doit-front-page.html if the hero needs scrolling on the phone, a button sits under a fold, or any section looks wrong; republish and rerun Step 5 until clean. Keep the Task 2 spec green (chemx test tests/unit/siteFrontPage.spec.ts).

  • Step 7: Prove idempotency

Run: node tools/site/publish-doit.ts --site http://mycompass Expected: every row says "reused" or "updated", none "created"; GET http://mycompass/wp-json/wp/v2/pages?slug=home&status=any has exactly one item; the summary shows no draft step because the front page is already home.

  • Step 8: Commit and hand the screenshots to the owner
git add tests/e2e/specs/site.spec.js
git commit -- tests/e2e/specs/site.spec.js -m "test(site): Cypress check of the published front page"

Report: the two screenshot paths and the publish summary. Stop here until the owner approves.


Task 5: Live publish, theme switch, docs

Files:

  • Modify: CLAUDE.md (one line under Commands for node tools/site/publish-doit.ts --site <url>)

  • Modify: docs/current_context.md (a short session note), docs/Roadmap.md (an "Account" style line under the site section: the front page is published from tools/site)

  • Step 1: Dry run against live

Run: node tools/site/publish-doit.ts --site https://doit.forthexp.com --dry-run Expected: auth line for the administrator, previousFrontPage 1141369 would be drafted, exit 0.

  • Step 2: Publish

Run: node tools/site/publish-doit.ts --site https://doit.forthexp.com Expected: exit 0, report lists the eight media, the page, the template, settings; every link 200.

  • Step 3: Owner activates Magic Hat on doit.forthexp.com (Appearance → Themes → Xophz Magic Hat → Activate). Wait for the owner's word.

  • Step 4: Verify live

Run: pnpm vue-cli-service test:e2e --headless --browser chrome --url https://doit.forthexp.com --spec tests/e2e/specs/site.spec.js Expected: 3 passing; screenshots overwritten with the live page. Read them.

  • Step 5: Docs and commit

Add the CLAUDE.md line, the current_context note (date, what was published, how to roll back) and the roadmap line; commit the app repo paths with git commit -- CLAUDE.md, and the docs submodule with git -C docs commit -- current_context.md Roadmap.md.

  • Step 6: Report the live URL, the rollback command, and the open follow-ups from spec section 7.
⚠️ **GitHub.com Fallback** ⚠️