loaded.bonsung.me 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: Build and publish loaded.bonsung.me, a portfolio page in Loaded's colours that shows its own build: the spec line, the checks and the merged change behind every section, from a record produced by the repository itself.

Architecture: A statically exported Next.js site. A record script reads the repository's own history (git) and its pull requests and check results (GitHub's API) at build time and writes one JSON file, from which the /built pages and the build notes are rendered. The spec and this plan are published from docs/, each block anchored by its line number. Vitest checks the built HTML, Playwright checks behaviour in a browser, and a history check proves main holds only Bonsung's merges.

Tech Stack: Node 22, Next.js 16 (App Router, output: "export"), React 19, Tailwind CSS 4, TypeScript, Vitest with jsdom, Playwright (Chromium), unified/remark for Markdown, unpdf for the CV's text, tsx for scripts, GitHub Actions, Vercel, Cloudflare DNS.

Spec: docs/spec.md. Read it before starting.

Global Constraints

Working rules (every task)

Start: git switch main && git pull, then git switch -c <the task's branch>. If the previous task's pull request is still open, branch from that branch instead. Every pull request targets main.

Finish:

  1. npm run lint && npm run typecheck && npm run build && npm test all exit 0, and npm run test:e2e from Task 4 on.
  2. Tick the task's boxes in docs/plan.md.
  3. Commit with a feat:, fix:, test:, docs: or chore: message ending in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
  4. git push -u origin <branch>, then gh pr create --base main --title "<title>" --body "<body>". The title and body are published on /built/changes: plain English, what changed and why, no dates or durations, the page's style rules. The body ends with 🤖 Generated with [Claude Code](https://claude.com/claude-code).
  5. gh pr checks <number> --watch until every check passes, then tell Bonsung the pull request is ready.
  6. Bonsung merges in GitHub with Create a merge commit. Agents never merge.

File Map

File Responsibility
AGENTS.md, CLAUDE.md Rules for coding agents. CLAUDE.md imports AGENTS.md
docs/spec.md, docs/plan.md The published spec and this plan
.claude/settings.json, .claude/hooks/guard.mjs The coding-agent hook
.githooks/pre-commit Refuses commits on main
.github/workflows/ci.yml The five checks: build, test, lint, typecheck, history
src/content/theme.ts Colour tokens and the contrast formula
src/content/cv.ts The CV's text, line by line
src/components/sections/*.tsx One file per page section
src/components/{Section,Inline,Footer,Strip,BuildSwitch,BuildNote}.tsx Shared pieces
src/record/*.ts History, GitHub, documents, tests, notes: everything the build record is made from
scripts/record.ts, scripts/check-history.ts Write the record, check main's history
src/app/built/{spec,plan,checks,changes}/page.tsx The build record pages
tests/*.test.ts, tests/hooks.test.mjs, e2e/*.spec.ts Checks

Task 1: Start the repository

Branch: none. This is the first commit on main, made before the hooks are switched on.

Files:

Interfaces:

import { test } from "node:test"
import assert from "node:assert/strict"
import { execFileSync } from "node:child_process"
import { mkdtempSync } from "node:fs"
import { tmpdir } from "node:os"
import { join, resolve } from "node:path"
import { blockReason } from "../.claude/hooks/guard.mjs"

const blocked = (command, branch = "feat/hero") => blockReason(command, branch) !== null

test("the agent guard stops skipping the git hooks", () => {
  for (const c of [
    "git commit --no-verify -m 'feat: x'",
    "git commit -n -m 'feat: x'",
    "git push --no-verify origin feat/hero",
    "git -c core.hooksPath=/dev/null commit -m x",
    "git config core.hooksPath .git/hooks",
  ]) assert.ok(blocked(c), c)
})

test("the agent guard stops pushes to main", () => {
  for (const c of [
    "git push origin main",
    "git push origin HEAD:main",
    "git push -f origin feat/hero:main",
    "git push origin +main",
    "git push origin refs/heads/main",
  ]) assert.ok(blocked(c), c)
  assert.ok(blocked("git push", "main"))
  assert.ok(blocked("git push -u origin HEAD", "main"))
})

test("the agent guard stops merging pull requests", () => {
  for (const c of [
    "gh pr merge 3",
    "gh pr merge 3 --merge --admin",
    "gh api -X PUT repos/b0nsun9/portfolio-for-loaded/pulls/3/merge",
  ]) assert.ok(blocked(c), c)
  assert.ok(blocked("git merge feat/hero", "main"))
})

test("the agent guard lets ordinary branch work through", () => {
  for (const c of [
    "git commit -m 'feat: add the hero'",
    "git push -u origin feat/hero",
    "git push origin feat/main-menu",
    "git push -n origin feat/hero",
    "gh pr create --base main --fill",
    "gh pr checks 3 --watch",
    "git merge main",
  ]) assert.equal(blockReason(c, "feat/hero"), null, c)
})

test("the pre-commit hook refuses commits on main", () => {
  const dir = mkdtempSync(join(tmpdir(), "hook-"))
  const git = (...args) => execFileSync("git", ["-C", dir, ...args], { stdio: "pipe" })
  git("init", "-b", "main")
  git("config", "core.hooksPath", resolve(".githooks"))
  git("config", "user.email", "test@example.com")
  git("config", "user.name", "Test")
  assert.throws(() => git("commit", "--allow-empty", "-m", "straight onto main"))
  git("switch", "-c", "feat/x")
  assert.doesNotThrow(() => git("commit", "--allow-empty", "-m", "on a branch"))
})

Run: node --test tests/hooks.test.mjs Expected: FAIL, cannot find .claude/hooks/guard.mjs.

blockReason is conservative: a command that merely mentions --no-verify (in a commit message, say) is blocked too. A push targets main when a refspec's destination is main or refs/heads/main (with or without +), or when the current branch is main and the push names no other branch. The pre-commit hook is a POSIX sh script that exits 1 with a message when git symbolic-ref --short HEAD is main, and must be executable. .claude/settings.json registers the guard as a PreToolUse hook on Bash, run as node "$CLAUDE_PROJECT_DIR/.claude/hooks/guard.mjs".

Run: node --test tests/hooks.test.mjs Expected: 5 tests pass.

git init -b main
git add -A
git commit -m "chore: start with the spec, the plan and the rules" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
gh repo create b0nsun9/portfolio-for-loaded --private --source . --push
gh api -X PATCH repos/b0nsun9/portfolio-for-loaded \
  -F allow_merge_commit=true -F allow_squash_merge=false -F allow_rebase_merge=false \
  -F delete_branch_on_merge=true -f merge_commit_title=MERGE_MESSAGE -f merge_commit_message=PR_TITLE
gh repo view b0nsun9/portfolio-for-loaded --json visibility,mergeCommitAllowed,squashMergeAllowed,rebaseMergeAllowed

Expected: "visibility":"PRIVATE", merge commits allowed, squash and rebase not.


Task 2: Scaffold, checks and CI

Branch: feat/scaffold

Files:

Interfaces:

Run npx create-next-app@16 into an empty temporary folder with TypeScript, ESLint, Tailwind, the src/ folder, the App Router and the @/* alias, without git. Copy everything except .git, README.md and .gitignore into the repository. Delete public/*.svg and src/app/favicon.ico: they are Next.js and Vercel artwork. Set output: "export" and images: { unoptimized: true } in next.config.ts. Add vitest and jsdom as dev dependencies, with vitest.config.ts including only tests/**/*.test.ts and aliasing @ to src. Run npm install, which also switches the git hooks on.

import { expect, it } from "vitest"
import { loadPage } from "./helpers"

it("the site builds to a static page", () => {
  const doc = loadPage()
  expect(doc.documentElement.lang).toBe("en-NZ")
  expect(doc.title).toBe("Bonsung Koo, Senior Software Developer")
})

Run: npm run build && npx vitest run tests/site.test.ts Expected: FAIL, the scaffold's lang is en and its title differs.

Run: npm run build && npm test Expected: PASS, and the hook tests still pass.

.github/workflows/ci.yml runs on pull_request and on push to main, with permissions: { contents: read, pull-requests: read, actions: read }. Jobs, named exactly: build (checkout with fetch-depth: 0, Node 22, npm ci, npm run build with GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}, upload out as the artifact site), test (needs build, checkout with fetch-depth: 0, npm ci, download site into out, npm test), lint and typecheck. Use the current major version of each action.


Task 3: main holds merges only

Branch: feat/history-check

Files:

Interfaces:

import { expect, it } from "vitest"
import { historyViolations, type MainCommit } from "@/record/history"
import type { PullInfo } from "@/record/github"

const root: MainCommit = { sha: "a1", parents: [], subject: "chore: start with the spec, the plan and the rules", committerEmail: "developer@example.com" }
const merge = (sha: string, n: number, over: Partial<MainCommit> = {}): MainCommit =>
  ({ sha, parents: ["p", "q"], subject: `Merge pull request #${n} from b0nsun9/feat-${n}`, committerEmail: "noreply@github.com", ...over })
const pull = (n: number, mergeSha: string, mergedBy = "b0nsun9"): [number, PullInfo] =>
  [n, { number: n, title: "", body: "", mergedBy, mergeSha }]

it("main may hold the first commit and pull requests merged by Bonsung", () => {
  const pulls = new Map([pull(1, "m1"), pull(2, "m2")])
  expect(historyViolations([root, merge("m1", 1), merge("m2", 2)], pulls, "b0nsun9")).toEqual([])
})

it("a commit pushed straight to main fails the history check", () => {
  const direct = { sha: "x9", parents: ["a1"], subject: "feat: straight in", committerEmail: "developer@example.com" }
  const found = historyViolations([root, direct], new Map(), "b0nsun9")
  expect(found).toHaveLength(1)
  expect(found[0]).toContain("x9")
})

it("a pull request merged by anyone else fails the history check", () => {
  const found = historyViolations([root, merge("m1", 1)], new Map([pull(1, "m1", "someone-else")]), "b0nsun9")
  expect(found.join()).toContain("#1")
})

it("a merge made outside GitHub fails the history check", () => {
  const local = merge("m1", 1, { committerEmail: "developer@example.com" })
  expect(historyViolations([root, local], new Map([pull(1, "m1")]), "b0nsun9")).toHaveLength(1)
})

it("a squash or rebase merge fails the history check", () => {
  const squash = { sha: "s1", parents: ["a1"], subject: "Add the hero (#1)", committerEmail: "noreply@github.com" }
  expect(historyViolations([root, squash], new Map([pull(1, "s1")]), "b0nsun9")).toHaveLength(1)
})

Run: npx vitest run tests/history.test.ts Expected: FAIL, @/record/history not found.

Rules: the first commit has no parents. Every later commit has two parents, a subject matching ^Merge pull request #(\d+) from , committer noreply@github.com, and a pull request whose mergedBy is the merger and whose mergeSha is that commit. Each violation names the commit's short sha and, where there is one, the pull request number. The script reads git rev-list --first-parent --reverse of its argument (default origin/main) with the subject and committer email, fetches each named pull request, prints violations and exits 1 if there are any. It needs a token (resolveToken) and exits 1 without one.

Run: npx vitest run tests/history.test.ts Expected: 5 tests pass. Then GITHUB_TOKEN=$(gh auth token) npm run check-history exits 0.


Task 4: The look and the hero

Branch: feat/hero

Files:

Interfaces:

// tests/theme.test.ts
it("the contrast formula matches known values", () => {
  expect(contrast("#000000", "#ffffff")).toBeCloseTo(21, 1)
  expect(contrast("#ffffff", "#3b73fa")).toBeCloseTo(4.2, 1)
})
it("every text colour pair meets WCAG AA", () => {
  for (const [fg, bg, use] of TEXT_PAIRS) expect(contrast(fg, bg), use).toBeGreaterThanOrEqual(4.5)
})
it("the stylesheet uses the theme's colours", () => {
  const css = readFileSync("src/app/globals.css", "utf8")
  for (const value of Object.values(THEME)) expect(css).toContain(value)
})

// tests/page.test.ts
it("the hero reads Kia ora, I'm Bonsung. Loaded.", () => {
  const hero = loadPage().querySelector("[data-section=hero]")!
  expect(squash(hero.querySelector("h1")!.textContent!)).toBe(squash("Bonsung. Loaded."))
  for (const line of ["Kia ora, I’m", "Helping the platform that helps hospitality grow.", "Senior Software Developer"])
    expect(squash(hero.textContent!)).toContain(squash(line))
})
it("the footer says the page is a job application, not Loaded's", () => {
  const footer = loadPage().querySelector("footer")!
  expect(squash(footer.textContent!)).toContain(squash("A job application by Bonsung Koo. Not made or endorsed by Loaded."))
  expect(footer.querySelector("a[href='mailto:loaded@bonsung.me']")).not.toBeNull()
  expect(footer.querySelector("a[href='https://github.com/b0nsun9']")).not.toBeNull()
})

// tests/repo.test.ts
const OWN_MEDIA = ["src/app/icon.svg"]
it("no font, image, logo or icon file from another company is in the repository", () => {
  const files = execFileSync("git", ["ls-files"], { encoding: "utf8" }).split("\n")
  const media = files.filter((f) => /\.(woff2?|ttf|otf|eot|png|jpe?g|gif|webp|avif|svg|ico)$/i.test(f))
  expect(media.sort()).toEqual([...OWN_MEDIA].sort())
})
// e2e/hero.spec.ts
test("with reduced motion the bar is full and Loaded. shows at once", async ({ page }) => {
  await page.emulateMedia({ reducedMotion: "reduce" })
  await page.goto("/")
  const word = page.getByTestId("loaded-word")
  expect(await word.evaluate((el) => getComputedStyle(el).animationName)).toBe("none")
  expect(await word.evaluate((el) => getComputedStyle(el).opacity)).toBe("1")
  const fill = await page.getByTestId("bar-fill").boundingBox()
  const track = await page.getByTestId("bar-track").boundingBox()
  expect(fill!.width).toBeCloseTo(track!.width, 0)
})
test("the bar fills before Loaded. appears", async ({ page }) => {
  await page.goto("/")
  await expect(page.getByTestId("loaded-word")).toHaveCSS("opacity", "1", { timeout: 5000 })
  const fill = await page.getByTestId("bar-fill").boundingBox()
  const track = await page.getByTestId("bar-track").boundingBox()
  expect(fill!.width).toBeCloseTo(track!.width, 0)
})

Run: npm run build && npm test Expected: FAIL, @/content/theme not found, no hero.

The colours go into Tailwind's @theme in globals.css as --color-champagne and so on. The hero sits on champagne in off-black: the greeting, the h1, then the bar (coffee track, blue fill), then the line and the role. CSS only, no script: the fill animates its width from 0 to 100%, then Loaded. animates its opacity from 0 to 1, delayed by the fill's duration, with animation-fill-mode: both. Under prefers-reduced-motion: reduce both have animation: none, the fill is 100% and the word is opaque. The footer is dark blue with coffee text: the name, the GitHub link, the email link and the footer line. icon.svg is drawn here: a short blue bar on champagne, nothing of Loaded's.

playwright.config.ts: testDir: "e2e", Chromium at 1280 by 800, baseURL: "http://localhost:4173", webServer: { command: "npx serve out -l 4173", port: 4173, reuseExistingServer: !process.env.CI }. Script test:e2e is playwright test. In CI, the test job runs npx playwright install --with-deps chromium and then npm run test:e2e after npm test.

Run: npm run build && npm test && npm run test:e2e Expected: PASS.


Task 5: The CV on the page

Branch: feat/cv

Files:

Interfaces:

// tests/cv.test.ts
const PDF = "public/cv/Bonsung_Koo_CV_Senior_Software_Developer.pdf"
const sha = (p: string) => createHash("sha256").update(readFileSync(p)).digest("hex")
const lines = (section: string, selector = "[data-cv-line]") =>
  [...loadPage().querySelectorAll(`[data-section=${section}] ${selector}`)].map((e) => e.textContent ?? "")
async function expectCvLines(section: string, bullets: number | null) {
  const pdf = squash(await pdfText())
  const found = lines(section)
  expect(found.length).toBeGreaterThan(0)
  for (const line of found) expect(pdf, line).toContain(squash(line))
  if (bullets !== null) expect(lines(section, "li[data-cv-line]")).toHaveLength(bullets)
}

it("the CV download is the submitted PDF", () => {
  expect(sha(PDF)).toBe("18fbcab91a7f18f9edc94602ce2005c46778aeb41a0a82695b1a1d4bf4f3eaa3")
  expect(sha("out/cv/Bonsung_Koo_CV_Senior_Software_Developer.pdf")).toBe(sha(PDF))
  expect(loadPage().querySelector("[data-section=download-cv] a[href='/cv/Bonsung_Koo_CV_Senior_Software_Developer.pdf']")).not.toBeNull()
})
it("the introduction is the CV summary", async () => { await expectCvLines("introduction", null) })
it("every key strength is the CV's text", async () => { await expectCvLines("key-strengths", 5) })
it("every experience line is the CV's text", async () => { await expectCvLines("experience", 14) })
it("every project line is the CV's text", async () => { await expectCvLines("projects", 3) })
it("every education line is the CV's text", async () => { await expectCvLines("education", null) })

// tests/page.test.ts
it("airie is always lowercase", () => {
  expect(loadPage().body.textContent).not.toMatch(/Airie|AIRIE/)
})
it("every external link is on the allowed list", () => {
  const ALLOWED = [
    "https://www.airie.world",
    "https://apps.apple.com/nz/app/shopl-for-frontline-workers/id1284466280",
    "https://apps.apple.com/us/app/detail-stories-in-image/id1468063860",
    "https://jpeg.org/jpegsystems/index.html",
    "https://usesentinel.tools",
    "https://npmjs.com/package/sentinel-mcp",
    "https://github.com/b0nsun9",
    "mailto:loaded@bonsung.me",
  ]
  for (const file of globSync("out/**/*.html")) for (const a of loadPage(file.slice(4)).querySelectorAll("a[href]")) {
    const href = a.getAttribute("href")!
    if (!href.startsWith("/") && !href.startsWith("#")) expect(ALLOWED, `${file}: ${href}`).toContain(href)
  }
})
it("the page shows no phone number", () => {
  expect(loadPage().body.textContent).not.toMatch(/\b0\d{1,2}[\s-]?\d{3}[\s-]?\d{3,4}\b/)
})
it("text written for the page follows the style rules", () => {
  const doc = loadPage()
  doc.querySelectorAll("[data-cv-line], script, style").forEach((e) => e.remove())
  const text = doc.body.textContent!
  expect(text).not.toMatch(/[—–;]/)
  expect(text).not.toMatch(/\b\d+\s*(hours?|hrs?|days?|weeks?)\b/i)
})

Run: npm run build && npx vitest run tests/cv.test.ts tests/page.test.ts Expected: FAIL, no sections and no pdfText.

Read the text with pdftotext -layout and copy each line exactly, headings and separators included. Where the layout splits a heading line (the PDF puts Sentinel's links on its first line, for one), follow the PDF: the test compares against the PDF's text. The Introduction is the CV's summary paragraph. Key Strengths shows each strength's bold label and text as one line. Experience shows, per job, the company line, the role line and the bullets (7, 6 and 1). Projects shows Sentinel's lines and its 3 bullets, with its two links. Education shows each degree and its place line. Download CV links to the PDF. Section headings: Introduction, Key Strengths & Achievements, Work Experience, Selected Projects, Education, Download CV. Sections alternate light coffee and white.

Run: npm run build && npm test && npm run test:e2e Expected: PASS.


Task 6: The spec and the plan as pages

Branch: feat/built-docs

Files:

Interfaces:

// tests/docs.test.ts
const md = "# Title\n\nFirst paragraph\nwraps here.\n\n- one item\n- two **bold** item\n\n| a | b |\n|---|---|\n| c | d |\n"

it("each block of a document keeps its first line number", () => {
  expect(parseDoc(md).map((b) => b.line)).toEqual([1, 3, 6, 7, 9])
})
it("a spec quote is found across line breaks and markdown", () => {
  expect(findBlock(parseDoc(md), "paragraph wraps here").line).toBe(3)
  expect(findBlock(parseDoc(md), "two **bold** item").line).toBe(7)
})
it("a quote that is missing or appears twice is an error", () => {
  expect(() => findBlock(parseDoc(md), "not there")).toThrow(/not found/)
  expect(() => findBlock(parseDoc(md), "item")).toThrow(/more than one/)
})
it("a rendered document gives every block an anchor", () => {
  const html = renderDoc(md)
  for (const id of ["L1", "L3", "L6", "L7", "L9"]) expect(html).toContain(`id="${id}"`)
})

// tests/built.test.ts
it("the spec and the plan are published with every block addressable", () => {
  for (const [page, name] of [["built/spec.html", "spec"], ["built/plan.html", "plan"]] as const) {
    const doc = loadPage(page)
    for (const b of parseDoc(readDoc(name))) expect(doc.getElementById(`L${b.line}`), `${page} L${b.line}`).not.toBeNull()
    expect(squash(doc.body.textContent!)).toContain(squash("The repository is private. Access on request."))
  }
})

Run: npx vitest run tests/docs.test.ts Expected: FAIL, @/record/docs not found.

Run: npm run build && npm test && npm run test:e2e Expected: PASS.


Task 7: The change record, the checks page and the strip

Branch: feat/built-record

Files:

Interfaces:

export type CheckName = "build" | "test" | "lint" | "typecheck" | "history"
export type Conclusion = "success" | "failure" | "cancelled" | "skipped"
export type Change = {
  id: string                 // "start" for the first commit, otherwise "pr-<number>"
  number: number | null
  title: string
  body: string
  mergedBy: string | null    // GitHub login, null for the first commit
  agentWritten: boolean      // a commit in it carries a "Co-Authored-By: Claude" line
  files: string[]
  checks: Partial<Record<CheckName, Conclusion>> | null   // null when unknown, and for the first commit
}
export type TestInfo = { name: string; file: string; slug: string }
export type BuildRecord = { changes: Change[]; tests: TestInfo[] }   // changes newest first

tests/git.test.ts builds a throwaway repository in a temporary folder, with its own test user configured: a first commit adding docs/spec.md, then branch a adding src/a.ts in a commit ending Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>, merged into main with git merge --no-ff -m "Merge pull request #1 from b0nsun9/a" -m "Add a", then branch b adding src/b.ts with no co-author line, merged the same way as #2 with the body Add b.

it("the change record reads merged pull requests from main's history", () => {
  const changes = gitChanges(repo)
  expect(changes.map((c) => c.id)).toEqual(["pr-2", "pr-1", "start"])
  expect(changes[0]).toMatchObject({ number: 2, title: "Add b", files: ["src/b.ts"], agentWritten: false })
  expect(changes[1]).toMatchObject({ number: 1, title: "Add a", files: ["src/a.ts"], agentWritten: true })
  expect(changes[2]).toMatchObject({ number: null, files: ["docs/spec.md"] })
})
it("commits on an unmerged branch are not in the change record", () => {
  // on a new branch from main, commit src/c.ts, then:
  const changes = gitChanges(repo)
  expect(changes).toHaveLength(3)
  expect(changes.flatMap((c) => c.files)).not.toContain("src/c.ts")
})
// tests/record.test.ts
it("the checks page finds every test by name", () => {
  const found = listTests([{ path: "tests/x.test.ts", source: `it("one's test", () => {})\ntest('two', () => {})\ntest.describe("group", () => {})` }])
  expect(found.map((t) => t.name)).toEqual(["one's test", "two"])
  expect(found[0].slug).toBe("one-s-test")
})
it("test names are unique across the repository", () => {
  const names = listTests(readTestFiles(".")).map((t) => t.name)
  expect(new Set(names).size).toBe(names.length)
})
it("a production build without a GitHub token stops instead of publishing an incomplete record", () => {
  expect(() => recordOptions({ VERCEL_ENV: "production" }, false)).toThrow(/token/)
})
it("a build from a shallow clone stops instead of publishing a short history", () => {
  expect(() => recordOptions({ GITHUB_TOKEN: "t" }, true)).toThrow(/shallow/)
})
it("a local build without a token makes a record without check results", () => {
  expect(recordOptions({}, false)).toEqual({ token: null })
})
it("every merged change passed every check", () => {
  for (const c of readRecord().changes) if (c.id !== "start" && c.checks !== null) {
    for (const name of ["build", "test", "lint", "typecheck"] as const) expect(c.checks[name], `${c.id} ${name}`).toBeDefined()
    for (const [name, result] of Object.entries(c.checks)) expect(result, `${c.id} ${name}`).toBe("success")
  }
})

history is not required on every change because the first pull request merged before that check existed. Its result is still held to success wherever it ran.

// tests/built.test.ts
it("the changes page lists every change, newest first, with who merged it", () => {
  const record = readRecord()
  const doc = loadPage("built/changes.html")
  const items = [...doc.querySelectorAll("[data-change]")]
  expect(items.map((e) => e.id)).toEqual(record.changes.map((c) => `change-${c.id}`))
  for (const c of record.changes) if (c.mergedBy) expect(doc.getElementById(`change-${c.id}`)!.textContent).toContain(c.mergedBy)
})
it("the checks page names every check and every test", () => {
  const doc = loadPage("built/checks.html")
  for (const name of ["build", "test", "lint", "typecheck", "history"]) expect(doc.getElementById(`check-${name}`), name).not.toBeNull()
  for (const t of readRecord().tests) expect(doc.getElementById(`test-${t.slug}`), t.name).not.toBeNull()
})

// tests/page.test.ts
it("the strip shows the five steps", () => {
  const STEPS = [
    ["Spec", "The page was agreed with the agent in a written spec before any code.", "/built/spec"],
    ["Plan", "The spec became a step-by-step plan.", "/built/plan"],
    ["Agents build", "Coding agents wrote the code, a change at a time.", "/built/changes"],
    ["Checks", "Every change passed the tests, the build and lint, which give the same answer every time.", "/built/checks"],
    ["I approve", "Nothing reached main without my merge.", "/built/changes"],
  ]
  const strip = loadPage().querySelector("[data-section=strip]")!
  const items = [...strip.querySelectorAll("li")]
  expect(items).toHaveLength(5)
  STEPS.forEach(([title, text, href], i) => {
    expect(squash(items[i].textContent!)).toContain(squash(title))
    expect(squash(items[i].textContent!)).toContain(squash(text))
    expect(items[i].querySelector(`a[href='${href}']`)).not.toBeNull()
  })
  expect(squash(strip.textContent!)).toContain(squash("Turn on “Show how this was built” to see it on every section."))
})

Run: npx vitest run tests/git.test.ts tests/record.test.ts Expected: FAIL, modules not found.

/built/changes: the merged changes, newest first, each an element with data-change and id="change-<id>": its number and title, its body rendered as Markdown, Written by coding agents where agentWritten, its check results, and Merged by Bonsung (b0nsun9) (or the login, for anyone else). The first commit comes last, under its own heading: it added the spec, the plan and the rules, and went straight onto main to start the repository. /built/checks: one block per check with id="check-<name>" and what it guards in one plain sentence, the tests under test each with id="test-<slug>", and a table of each merged change's results. No dates or times anywhere. The strip goes at the end of the home page, before the footer, as <section data-section="strip"> titled How this page was built.

Run: npm run build && npm test && npm run test:e2e Expected: PASS. src/record/generated/record.json lists the merged changes with their check results.


Task 8: The build notes and the switch

Branch: feat/build-notes

Files:

Interfaces:

id specQuote tests paths
hero Hero copy, a play on Loaded's name and line. the hero reads Kia ora, I'm Bonsung. Loaded., with reduced motion the bar is full and Loaded. shows at once, the bar fills before Loaded. appears src/components/sections/Hero.tsx, src/components/sections/Hero.css
introduction the CV summary, in his own first-person words the introduction is the CV summary src/components/sections/Introduction.tsx, src/content/cv.ts
key-strengths the CV's five strengths, which hold the claims about agent-driven work every key strength is the CV's text src/components/sections/KeyStrengths.tsx, src/content/cv.ts
experience airie, Shopl and Company, PJ FACTORY, with the CV's job titles and dates every experience line is the CV's text, airie is always lowercase src/components/sections/Experience.tsx, src/content/cv.ts
projects Sentinel, with its two public links every project line is the CV's text, every external link is on the allowed list src/components/sections/Projects.tsx, src/content/cv.ts
education The text of sections 2 to 6 is the CV's text. every education line is the CV's text src/components/sections/Education.tsx, src/content/cv.ts
download-cv the PDF he submitted to Loaded, served byte for byte the CV download is the submitted PDF src/components/sections/DownloadCv.tsx, public/cv/Bonsung_Koo_CV_Senior_Software_Developer.pdf
// tests/notes.test.ts
const change = (n: number, files: string[]): Change =>
  ({ id: `pr-${n}`, number: n, title: `Change ${n}`, body: "", mergedBy: "b0nsun9", agentWritten: true, files, checks: { test: "success" } })

it("a note names the merged change that first shipped its section", () => {
  const changes = [change(3, ["src/a.ts", "src/b.ts"]), change(2, ["src/a.ts"]), change(1, ["src/b.ts"])]
  expect(firstChangeTouching(changes, ["src/a.ts"])?.id).toBe("pr-2")
})
it("a section no merged change has touched yet says so", () => {
  expect(firstChangeTouching([change(1, ["src/b.ts"])], ["src/a.ts"])).toBeNull()
  const record: BuildRecord = { changes: [change(1, ["src/b.ts"])], tests: listTests(readTestFiles(".")) }
  expect(buildNote(SECTIONS[0], record, parseDoc(readDoc("spec"))).shipped).toBeNull()
})
it("every section's spec quote and tests exist", () => {
  const spec = parseDoc(readDoc("spec"))
  const tests = new Set(listTests(readTestFiles(".")).map((t) => t.name))
  expect(SECTIONS.map((s) => s.id)).toEqual(["hero", "introduction", "key-strengths", "experience", "projects", "education", "download-cv"])
  for (const s of SECTIONS) {
    expect(() => findBlock(spec, s.specQuote), s.id).not.toThrow()
    for (const t of s.tests) expect(tests.has(t), t).toBe(true)
  }
})

// tests/page.test.ts
it("every build note points at a spec line, a test and a change that exist", () => {
  const notes = [...loadPage().querySelectorAll("[data-note]")]
  expect(notes).toHaveLength(7)
  for (const a of notes.flatMap((n) => [...n.querySelectorAll("a")])) {
    const [path, id] = a.getAttribute("href")!.split("#")
    expect(loadPage(`${path.slice(1)}.html`).getElementById(id), a.getAttribute("href")!).not.toBeNull()
  }
})
// e2e/switch.spec.ts
const toggle = (page: Page) => page.getByRole("switch", { name: "Show how this was built" })

test("the switch shows and hides the build notes", async ({ page }) => {
  await page.goto("/")
  await expect(page.locator("[data-note]").first()).toBeHidden()
  await toggle(page).click()
  await expect(toggle(page)).toHaveAttribute("aria-checked", "true")
  await expect(page.locator("[data-note]").first()).toBeVisible()
  await expect(page).toHaveURL(/\?build=on$/)
  await toggle(page).click()
  await expect(page.locator("[data-note]").first()).toBeHidden()
  await expect(page).not.toHaveURL(/build=on/)
})
test("a link with ?build=on opens with the notes showing", async ({ page }) => {
  await page.goto("/?build=on")
  await expect(toggle(page)).toHaveAttribute("aria-checked", "true")
  await expect(page.locator("[data-note]").first()).toBeVisible()
})
test("on a phone the notes sit below their sections and nothing scrolls sideways", async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 })
  await page.goto("/?build=on")
  expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375)
  const heading = await page.locator("[data-section=introduction] h2").boundingBox()
  const note = await page.locator("[data-section=introduction] [data-note]").boundingBox()
  expect(note!.y).toBeGreaterThan(heading!.y)
  const h1 = await page.locator("h1").boundingBox()
  const sw = await toggle(page).boundingBox()
  expect(sw!.y + sw!.height <= h1!.y || sw!.x >= h1!.x + h1!.width).toBe(true)
})

Run: npx vitest run tests/notes.test.ts Expected: FAIL, @/record/sections not found.

The home page builds the seven notes at build time from readRecord() and readDoc("spec") and passes each to its section. Notes are in the static HTML, so their links are checkable without a browser.

Run: npm run build && npm test && npm run test:e2e Expected: PASS.


Task 9: Share card and page details

Branch: feat/share-card

Files:

it("the page has its description and share card", () => {
  const meta = (p: string) => loadPage().querySelector(`meta[property='${p}'], meta[name='${p}']`)?.getAttribute("content")
  expect(meta("description")).toBe("Senior Software Developer. Helping the platform that helps hospitality grow.")
  expect(meta("og:url")).toBe("https://loaded.bonsung.me")
  expect(meta("og:image")).toMatch(/^https:\/\/loaded\.bonsung\.me\/opengraph-image/)
})

Run: npm run build && npx vitest run tests/page.test.ts Expected: FAIL, no description.

metadataBase is https://loaded.bonsung.me. opengraph-image.tsx uses ImageResponse from next/og with dynamic = "force-static", 1200 by 630: the hero's words on champagne with the full blue bar, nothing of Loaded's. The README gets the commands (npm run dev, build, test, test:e2e, check-history), the two environment variables (RECORD_GITHUB_TOKEN, VERCEL_DEEP_CLONE) and how a change reaches main.

Run: npm run build && npm test && npm run test:e2e Expected: PASS, and the repository test still lists only src/app/icon.svg.


Task 10: Publish and verify

Replaced by Task 14 after the revision below. The build no longer needs a token or a full clone.

Files: none in the repository.

Each step that sets something up outside the repository is Bonsung's or needs his go-ahead.


Review Focus

  1. A shared link with ?build=on must open with the switch on and the notes showing. Pinned by a link with ?build=on opens with the notes showing (Task 8).
  2. A reader with reduced motion must see the full bar and Loaded. at once, with nothing animating. Pinned by with reduced motion the bar is full and Loaded. shows at once (Task 4).
  3. A phone at 375 px with the notes on must not scroll sideways, and the fixed switch must not cover the name. Pinned by on a phone the notes sit below their sections and nothing scrolls sideways (Task 8).
  4. A production build without the token or with a shallow clone must fail rather than publish a record missing changes or results. Pinned by the two recordOptions tests (Task 7).
  5. A section no merged change has touched yet (on a preview of an open pull request) must say Not merged yet, not name the wrong change. Pinned by a section no merged change has touched yet says so (Task 8).

Revision: the workflow showcase

The spec changed before the page went live. A list of every commit said little about how the work was done, so a short showcase of the workflow replaces the published build record, the site stops offering the CV as a PDF, and the footer loses its disclaimer line. Tasks 1 to 9 stay above as they were built. Task 10 is replaced by Task 14.

Goal: Replace the build record with the eight-step showcase the spec describes, and publish the site.

Architecture: The site stays a static Next.js export, but the build no longer reads git or GitHub. The showcase's copy lives in one content file. Tests check each piece of evidence against the file it comes from (AGENTS.md, the agent hook, the CI workflow, the plan, the end-to-end tests and the lesson in docs/solutions/). The history check stays in CI.

Revision constraints

These replace the matching lines of the Global Constraints above. Every other line there still holds.

Task 11: Take out the build record, the CV download and the footer line

Branch: chore/showcase-groundwork.

Files:

Interfaces:

In tests/page.test.ts, replace the footer test and the strip and build-note tests with:

it("the footer has his name, GitHub and email, and nothing else", () => {
  const footer = loadPage().querySelector("footer")!
  expect(squash(footer.textContent!)).toBe(squash("Bonsung Koo GitHub loaded@bonsung.me"))
  expect(footer.querySelector("a[href='mailto:loaded@bonsung.me']")).not.toBeNull()
  expect(footer.querySelector("a[href='https://github.com/b0nsun9']")).not.toBeNull()
})
it("every link inside the site opens a page that exists", () => {
  for (const file of globSync("out/**/*.html")) for (const a of loadPage(file.slice(4)).querySelectorAll("a[href^='/']")) {
    const [path, id] = a.getAttribute("href")!.split("#")
    const page = path === "/" ? "index.html" : `${path.slice(1)}.html`
    expect(existsSync(`out/${page}`), `${file}: ${path}`).toBe(true)
    if (id) expect(loadPage(page).getElementById(id), `${file}: ${a.getAttribute("href")}`).not.toBeNull()
  }
})
it("the page sections are in order", () => {
  const ids = [...loadPage().querySelectorAll("[data-section]")].map((e) => e.getAttribute("data-section"))
  expect(ids).toEqual(["hero", "introduction", "key-strengths", "experience", "projects", "education"])
})

In tests/cv.test.ts, replace the download test with the one below, and make the headings test read section:not([data-section=showcase]) h2 and expect ["Introduction", "Key Strengths & Achievements", "Work Experience", "Selected Projects", "Education"].

it("the site serves no CV file", () => {
  expect(existsSync("out/cv")).toBe(false)
  for (const file of globSync("out/**/*.html")) expect(loadPage(file.slice(4)).querySelector("a[href$='.pdf']"), file).toBeNull()
})

In tests/built.test.ts, delete the changes and checks tests and make the date test expect builtPages().sort() to equal ["built/plan.html", "built/spec.html"]. In tests/docs.test.ts, delete the change body test and the two spec quote tests. In tests/site.test.ts, add:

it("the build reads nothing from git or GitHub", () => {
  expect(JSON.parse(readFileSync("package.json", "utf8")).scripts.build).toBe("next build")
  for (const file of globSync("src/{app,components,content}/**/*.{ts,tsx}"))
    expect(readFileSync(file, "utf8"), file).not.toMatch(/child_process|api\.github\.com/)
})

Run: npm run build && npx vitest run Expected: FAIL on the footer, link, section order, CV file, headings, /built pages and build script tests.

Delete the imports of the removed modules with them, then run npx tsc --noEmit until nothing refers to a deleted file.

Run: npm run lint && npm run typecheck && npm run build && npm test && npm run test:e2e Expected: all pass, and ls out shows no cv folder and out/built holds only plan.html and spec.html.

Task 12: Write down the lesson from the hero's review

The session runs this task itself, not a fresh agent, because the compound step asks Bonsung before it edits AGENTS.md.

Branch: docs/hero-test-lesson.

Files:

Interfaces:

Task 13: The showcase

Branch: feat/showcase.

Files:

Interfaces:

The copy, exactly. LESSON_PATH is Task 12's file, and the last step's evidence is that file's title. The plan link opens the hero's task: PLAN_HERO_LINE is the line of the first block of parseDoc(readDoc("plan")) whose source starts with ### Task 4:.

export const SHOWCASE_LEAD = "How I work with coding agents, shown on the page you are reading."
export const SHOWCASE_CLOSE = "The repository is private. Access on request."
export const SHOWCASE_STEPS: ShowcaseStep[] = [
  { title: "Agree the spec first.", says: "I talk each change through with the agent until we agree a written spec.", label: "From AGENTS.md", evidence: "“A change to what the page does starts as a change to `docs/spec.md`.”", link: { text: "Read the spec", href: "/built/spec" } },
  { title: "Plan small tasks, with their tests.", says: "Each task names its tests before any code exists, and Context7 keeps the plan on current library documentation.", label: "From the plan", evidence: "“the bar fills before Loaded. appears”", link: { text: "Read the plan", href: `/built/plan#L${PLAN_HERO_LINE}` } },
  { title: "Keep the rules in the repository.", says: "Hooks stop the agents committing or pushing to `main`, and merging.", label: "The agent hook", evidence: "“Agents never merge pull requests. Bonsung merges.”" },
  { title: "One agent builds, another reviews.", says: "A fresh agent checks each task against the spec.", label: "From a review", evidence: "The loading bar’s test could not fail if the order were wrong. A check of the order was added." },
  { title: "Write down judgement calls.", says: "Each one records what was decided, why, and what it costs if wrong.", label: "From the decision log", evidence: "The starter’s Geist font was removed, because the rules allow no other company’s fonts." },
  { title: "Checks decide, and I merge.", says: "Every pull request passes checks that give the same answer every time, and only I merge into `main`.", label: "On every pull request", evidence: "Build, test, lint, typecheck, history" },
  { title: "Review the whole at the end.", says: "A more capable model reviews the finished site at once.", label: "From the final review", evidence: "It found a real email address in the published plan’s sample data. It now uses an example address." },
  { title: "Write down what was learnt.", says: "Compound Engineering writes the lessons from solved problems into `docs/solutions/`, where the agents look when they plan.", label: "From docs/solutions", evidence: LESSON_TITLE },
]

Markup: a dark blue band like the other sections, <section id="showcase" data-section="showcase"> with the h2 How this page was built, the lead, an <ol> of eight <li data-step>, each with an h3 title, the sentence, and a <figure> holding a <figcaption> (the label) and the evidence, then the link if there is one, then the closing line.

const plain = (s: string) => s.replace(/`/g, "")
const read = (p: string) => readFileSync(p, "utf8")
const steps = () => [...loadPage().querySelectorAll("[data-section=showcase] li[data-step]")]

it("the showcase shows the eight steps as written", () => {
  const section = loadPage().querySelector("[data-section=showcase]")!
  expect(squash(section.textContent!)).toContain(squash(SHOWCASE_LEAD))
  expect(squash(section.textContent!)).toContain(squash(SHOWCASE_CLOSE))
  expect(steps()).toHaveLength(8)
  SHOWCASE_STEPS.forEach((s, i) => {
    for (const text of [s.title, s.says, s.label, s.evidence]) expect(squash(steps()[i].textContent!)).toContain(squash(plain(text)))
  })
})
it("each step's evidence sits in its own figure, under its label", () => {
  steps().forEach((li, i) => {
    expect(li.querySelectorAll("figure")).toHaveLength(1)
    expect(li.querySelector("figure figcaption")!.textContent).toBe(SHOWCASE_STEPS[i].label)
    expect(squash(li.querySelector("figure")!.textContent!)).toContain(squash(plain(SHOWCASE_STEPS[i].evidence)))
  })
})
it("the two links open the spec and the hero's task in the plan", () => {
  const links = steps().flatMap((li) => [...li.querySelectorAll("a")]).map((a) => a.getAttribute("href"))
  const line = parseDoc(readDoc("plan")).find((b) => b.source.startsWith("### Task 4:"))!.line
  expect(links).toEqual(["/built/spec", `/built/plan#L${line}`])
})
it("every quotation in the showcase is in the file it comes from", () => {
  const quote = (i: number) => SHOWCASE_STEPS[i].evidence.replace(/[“”]/g, "")
  const plan = read("docs/plan.md")
  const [from, to] = [plan.indexOf("\n### Task 4:"), plan.indexOf("\n### Task 5:")]
  expect(from).not.toBe(-1)
  expect(to).not.toBe(-1)
  const task4 = plan.slice(from, to)
  expect(read("AGENTS.md")).toContain(quote(0))
  for (const text of [task4, read("e2e/hero.spec.ts")]) expect(text).toContain(quote(1))
  expect(blockReason("gh pr merge 1", "feat/showcase")).toBe(quote(2))
})
it("what each story points to is in the repository", () => {
  const hero = read("e2e/hero.spec.ts").split('test("the bar fills before Loaded. appears"')[1].split("\ntest(")[0]
  expect(hero).toContain("expect(seconds(wordTiming.delay)).toBeGreaterThanOrEqual(seconds(fillTiming.delay) + seconds(fillTiming.duration))")
  const tracked = execFileSync("git", ["ls-files", "src"], { encoding: "utf8" }).split("\n").filter(Boolean)
  expect(tracked.filter((f) => /geist/i.test(f))).toEqual([])
  for (const file of ["package.json", "package-lock.json", ...tracked.filter((f) => f !== "src/content/showcase.ts")]) {
    if (!/\.(woff2?|ttf|otf|png|jpe?g|svg|ico|webp|avif)$/i.test(file)) expect(read(file), file).not.toMatch(/geist/i)
  }
  expect(read("AGENTS.md")).toContain("## No third-party files")
  const ci = read(".github/workflows/ci.yml")
  expect(ci.split("\njobs:\n")[0]).toMatch(/^on:\n(?:.*\n)*?  pull_request:/m)
  const jobs = [...ci.split("\njobs:\n")[1].matchAll(/^  ([\w-]+):$/gm)].map((m) => m[1])
  expect(jobs).toEqual(SHOWCASE_STEPS[5].evidence.toLowerCase().split(", "))
  const emails = read("docs/plan.md").match(/[\w.+-]+@[\w-]+(\.[\w-]+)+/g)!
  expect(emails.some((e) => e.endsWith("@example.com"))).toBe(true)
  for (const e of emails) expect(["loaded@bonsung.me", "noreply@anthropic.com", "noreply@github.com"].includes(e) || e.endsWith("@example.com"), e).toBe(true)
  const title = /^title:\s*["']?(.+?)["']?\s*$/m.exec(read(LESSON_PATH))![1]
  expect(SHOWCASE_STEPS[7].evidence).toBe(title)
})

src/content/showcase.ts reads LESSON_TITLE from the title: line of LESSON_PATH's frontmatter at build time, so the test above parses the file on its own.

Run: npm run build && npx vitest run tests/showcase.test.ts Expected: FAIL, src/content/showcase.ts does not exist.

Task 14: Publish and verify

Files: none in the repository. Each step that sets something up outside the repository is Bonsung's or needs his go-ahead.

Task 15: Loaded's favicon

The author chose Loaded's own favicon for the tab, unchanged (spec Decision 9). It is the one file of another company's that the repository holds, so the rule against third-party files names it, and step 5 of the showcase now says the rules allow no other company's fonts, which stays true.

Branch: feat/loaded-favicon.

Files:

const THIRD_PARTY = { "src/app/icon.png": "4e90fbe6f1e52b1ca927484a49b56bfdb196ca1b4bb9bf29bbb81122753371c8" }
it("no font, image, logo or icon file from another company is in the repository", () => {
  const files = execFileSync("git", ["ls-files"], { encoding: "utf8" }).split("\n")
  const media = files.filter((f) => /\.(woff2?|ttf|otf|eot|png|jpe?g|gif|webp|avif|svg|ico)$/i.test(f))
  expect(media.sort()).toEqual(Object.keys(THIRD_PARTY).sort())
})
it("the favicon is Loaded's own, unchanged", () => {
  for (const [file, sha] of Object.entries(THIRD_PARTY)) expect(createHash("sha256").update(readFileSync(file)).digest("hex"), file).toBe(sha)
  expect(loadPage().querySelector("link[rel='icon']")?.getAttribute("href")).toMatch(/^\/icon\.png/)
})

Task 16: No line about the repository

The author will not open the repository, so the site stops offering it (spec Decision 4). The showcase loses its closing line and the /built pages lose theirs. The earlier tasks above still mention that line, because they describe what was built then.

Branch: chore/no-repository-line.

Files:

it("no page offers access to the repository", () => {
  for (const file of globSync("out/**/*.html")) {
    const doc = loadPage(file.slice(4))
    doc.querySelectorAll("script, style, article.doc").forEach((e) => e.remove())
    expect(doc.body.textContent, file).not.toMatch(/repository is private|access on request/i)
  }
})

The published spec and plan are left out of this check (article.doc), since the plan's earlier tasks quote the old line as history.

Revision Review Focus

  1. A link inside the site to a removed page (the /built menu, an old anchor) must not land on a missing page. Pinned by every link inside the site opens a page that exists (Task 11).
  2. A Vercel build with no token and a shallow clone must succeed. Pinned by the build reads nothing from git or GitHub (Task 11), and the CI build job checks out shallow with no token.
  3. An edit to AGENTS.md, the hook, CI or the plan that changes a quoted line must fail the tests, not leave the page quoting something false. Pinned by every quotation in the showcase is in the file it comes from and what each story points to is in the repository (Task 13).
  4. A phone at 375 px must not scroll sideways once the showcase's longer lines and code are on the page. Pinned by nothing overflows sideways at phone width (Task 4), which now covers the showcase.
  5. A screen reader must hear the steps in order, each with its own evidence. Pinned by each step's evidence sits in its own figure, under its label (Task 13).