2026 10 19 doit front page - xopherdeep/do-it-for-the-xp GitHub Wiki
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
- Copy and structure come from spec section 1 verbatim; no invented numbers, no prices, no exclamation marks, no crypto.
- Every
<img>has non-emptyalt; everyhrefis 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 filetools/site/.doit-publish-report.jsonis 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>andchemx typecheck. Never barepnpm test. - Shared checkout: stage only your own paths (
git add <paths>), commit withgit commit -- <paths>, nevergit add -A, nevergit stash. Commit messages end withCo-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.
- 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. - A leftover
{{placeholder}}must stop the run before any write. Pinned in Task 1 (renderMarkupthrows) and Task 3 step 7. - A
homepage that exists as a draft or in the trash must be reused and republished, never duplicated ashome-2. Pinned in Task 3 (statusQuery,upsertPage). - Wrong or missing credentials must fail at the first request with the site's error code and write nothing. Pinned in Task 3 (
loadAuth, theusers/mecheck). - 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).
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); throwsError("Unknown placeholder: {{x}}")for a key not invars. -
findPlaceholders(html: string): string[]— the distinct{{…}}keys still present, in order of appearance. -
collectHrefs(html: string): string[]— everyhref="…"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 byusers,achievementsortasks(case-insensitive);relative href: <href>for an href that is not^https?://,^/or^#;img without alt: <src>for an<img>whosealtis 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"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 nameshometown,battle,victory,temple-map,beast-log,boss,small-key,my-home.front-page-template.htmlis 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.htmlas 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"Files:
- Create:
tools/site/publishPlan.ts - Create:
tools/site/publish-doit.ts - Modify:
.gitignore(addtools/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 }— hostmycompass,localhostor127.0.0.1→LOCAL_WP_USER/LOCAL_WP_APP_PASSWORD, elseDOIT_WP_USER/DOIT_WP_APP_PASSWORD; throwsError("Missing <KEY> in env file")naming the first missing key. -
parseEnvFile(text: string): Record<string, string>—KEY=valuelines,#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 }—nullwhen equal orpreviousId <= 0. -
linksToCheck(hrefs: string[], site: string): string[]— drops#…, resolves/…againstsite, keeps absolute, distinct. -
SCREENSHOTS: Array<{ name: string; file: string; alt: string }>— the eight names of Task 2,file=.github/readme/<name>.jpg,altas in Task 2; media slug isdoit-shot-<name>.
-
-
Produces (in
publish-doit.ts): CLInode 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. Writestools/site/.doit-publish-report.jsonas 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.tswith 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.tsin this order, each as a small function taking(auth, …):
-
main(): parse flags;--siterequired; read the env file (--env-file, elseDOIT_WP_ENV_FILE, else../Xophz-COMPASS/.env.localrelative to the repo root);pickAuth. Stop with the message if a screenshot file is missing on disk. -
rest(auth, method, path, body?)→ JSON;Authorization: Basic <base64 user:password>; a non-2xx throwsError("<status> <code>: <message>")built from the site's JSON.restFormfor multipart (FormDatawith aBlobof the file,title,slug,alt_text). - Auth check:
GET /wp/v2/users/me?context=edit; print the display name and roles only. -
ensureMedia(auth, shot):GET /wp/v2/media?slug=doit-shot-<name>&per_page=1; reuse{id, source_url}when found, else upload. - Render the page with
renderMarkup; iffindPlaceholders(html)is non-empty orcheckMarkup(html)returns problems, stop before any write and print them (this check runs in--dry-runtoo, after the media lookups, which are reads). -
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/pageswith{slug:"home", …}. -
upsertTemplate(auth):POST /wp/v2/templates/xophz-magic-hat//front-pagewith{content, title:"Front Page", status:"publish"}(WordPress makes the override on first write and updates it after); report itsid. - Settings: read
GET /wp/v2/settingsfirst (remembershow_on_front,page_on_front), thenPOST /wp/v2/settingswith{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."}. -
planFrontPage(previous, homeId).draftPrevious→POST /wp/v2/pages/<prev>{status:"draft"}. -
checkLinks:HEAD(fall back toGETon 405) each oflinksToCheck(collectHrefs(html), site)plus the page URL; collect{href, status}. - 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.
-
--dry-run: steps 1 to 5 and the settings read only; print what 6 to 10 would do; noPOST. -
--rollback: read the report;POST /wp/v2/pages/<previousFrontPage>{status:"publish"}when not null;POST /wp/v2/settingswith{show_on_front:<previousShowOnFront>, page_on_front:<previousFrontPage>}; print what changed; never touches the theme, media or thehomepage.
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"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.localgainsLOCAL_WP_USERandLOCAL_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.htmlif 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.
Files:
-
Modify:
CLAUDE.md(one line under Commands fornode 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 fromtools/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.