feat(workspace): scaffold monorepo per workspace plan#9
Conversation
Via `asdf set nodejs latest:22` per the workspace plan. asdf-managed so contributors get the same version on `asdf install`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root package.json declares `apps/*` and `packages/*` workspaces with
`type: module` and the npm-run scripts (dev, build, type-check, lint)
the plan calls for.
tsconfig.base.json carries `strict: true` plus the noUnchecked /
noImplicit pillars and bundler-style module resolution, per
specs/architecture.md ("TypeScript everywhere. strict: true.").
.gitignore covers node_modules, dist, *.local.*, .env*, and the dev
private-storage directory (see specs/behaviors/private-storage.md).
.editorconfig pins LF, 2-space, UTF-8 across editors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty workspace shell with name (@cfp/api), npm scripts (dev/build/ type-check/start), and a tsconfig that extends the base with NodeNext module resolution and a node types reference. No deps and no source yet; those land in the next two commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by:
npm install -w apps/api fastify
npm install -w apps/api -D typescript tsx @types/node pino-pretty
Fastify 5.x is the runtime per specs/architecture.md. tsx runs TS
directly in dev (watch mode) and ships builds get compiled via tsc.
pino-pretty is the dev transport for Fastify's built-in pino logger.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Boots Fastify on ${PORT:-3001} with pino-pretty in non-prod, JSON
logs otherwise. Registers GET /api/health returning {"status":"ok"}.
Intentionally minimal — under 30 lines per the workspace plan.
Response envelope plumbing (specs/api/conventions.md) lands in the
api-skeleton plan; /api/health here is a boot-validation route, not
a typed endpoint.
Verified locally:
$ curl -sS http://localhost:3001/api/health
{"status":"ok"}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty workspace shell with name (@cfp/web), npm scripts (dev/build/ preview/type-check), and a tsconfig with react-jsx, DOM libs, and vite/client types. Deps and source land in the next two commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by:
npm install -w apps/web react react-dom
npm install -w apps/web -D vite @vitejs/plugin-react \
typescript @types/react @types/react-dom
React 19 + Vite per specs/architecture.md. shadcn/ui + Tailwind v4
land in the web-shell plan, not here — this is the bare boot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier `npm install -w apps/web react react-dom` was followed
immediately by `npm install -w apps/web -D vite ...` which rewrote
the workspace manifest and dropped the runtime entries (lockfile kept
them but the manifest didn't). Re-ran:
npm install -w apps/web react@latest react-dom@latest
so apps/web/package.json carries the runtime deps explicitly and
`npm ci` will reproduce a working tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vite.config.ts: react plugin, dev proxy of /api → :3001, and `resolve.dedupe: ['react', 'react-dom']` so Vite's dep scanner finds the npm-workspace-hoisted react package (without dedupe it fails on `react/jsx-dev-runtime` resolution). index.html mounts #root and loads /src/main.tsx as a module. src/main.tsx creates a React 19 root in StrictMode; src/App.tsx renders "Hello, Code for Philly". Real screens and shadcn/Tailwind setup land in the public-screens / web-shell plans. Verified locally: `npm run -w apps/web dev` serves both `/` and the transformed `/src/main.tsx` at HTTP 200 with React fast-refresh wired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cfp/shared exports nothing useful yet — Zod schemas + shared types land here once the storage-foundation plan starts (see specs/data-model.md). For now it's a typed-module stub so the workspaces resolver wires it up and the root type-check covers it. `npm install` here updates package-lock.json with the new workspace binding only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by:
npm install -D concurrently
`npm run dev` at the root runs api + web in parallel via concurrently,
prefixed (api blue, web magenta) so the combined output is readable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by:
npm install -D eslint typescript-eslint @eslint/js \
eslint-plugin-react-hooks globals
Flat-config setup; the config itself lands in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single root config covers all workspaces (per the plan). Layered:
* @eslint/js recommended baseline
* typescript-eslint recommended
* eslint-plugin-react-hooks recommended for apps/web
* node globals for apps/api and *.config.{js,ts}
* browser globals for apps/web
Ignores dist/build/node_modules/.vite/coverage.
`npm run lint` at the root is the single entrypoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single `build` job on ubuntu-latest: 1. actions/checkout@v6 2. asdf-vm/actions/install@v4 — picks up .tool-versions (Node 22.22.3) 3. npm ci 4. npm run type-check 5. npm run lint 6. npm run build Triggers on push to main + every PR. Concurrency group cancels in-progress runs for the same ref to keep queue shallow. Test step lands once the test-harness plan introduces Vitest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ReviewCI green; the scaffold matches What's solid
One thing worth fixing
The runtime # Dev private storage (filesystem backend; see specs/behaviors/private-storage.md)
private-storage/
-fixtures/private-storage-seeded/Not strictly blocking — the seeded fixtures don't exist yet (they'll land with Nits (optional)
VerdictLGTM with the |
The fixture directory at fixtures/private-storage-seeded/ is meant to **ship** in the code repo per specs/behaviors/private-storage.md: > Contributors either start empty (sign up via GitHub OAuth during > dev) or load a fixture-seeded directory shipped at > fixtures/private-storage-seeded/ Ignoring it would cause `git add fixtures/private-storage-seeded/` to silently no-op once those fixtures land with storage-foundation. Only the runtime ./private-storage/ working directory should be ignored. Comment expanded so the distinction is explicit. Flagged in PR #9 review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`exactOptionalPropertyTypes` is opt-in; it is NOT enabled by `strict`, so explicitly setting it to `false` was a no-op that read like a deliberate choice without a documented reason. If we ever decide to enable it (the upside is real but the ecosystem-compat cost can be high), that becomes a separate intentional change with its own justification. Flagged in PR #9 review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the review! Addressed both fixes; skipping the README/ Applied
Deliberately skipped
Type-check still clean; pushing now should re-trigger CI. |
Claude Code recognizes a project-scoped CLAUDE.md at either
`<project>/CLAUDE.md` or `<project>/.claude/CLAUDE.md`. Moving it
under .claude/ collocates it with the project's agents/ and
commands/ — everything Claude-Code-specific lives in one directory.
Internal relative paths inside the file rewritten:
](specs/ → ](../specs/
](plans/ → ](../plans/
Cross-file references updated:
plans/README.md — workflow doc link
plans/workspace.md — spec list bullet
specs/architecture.md — repo-layout tree diagram (also adds
the README.md entry at the same level)
Prose mentions of "CLAUDE.md" without a link were left alone — the
filename still reads correctly regardless of path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A short, pointer-focused landing page for a contributor arriving cold — explains the spec-driven posture, lists the canonical references (specs/, plans/, .claude/CLAUDE.md), and documents the four root npm scripts so `npm run dev` isn't a discovery quest. Recommended in the review on PR #9 and authorized here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both the canonical workflow text (.claude/CLAUDE.md) and the
plans/README.md summary were ambiguous about *when* and *how* a
plan transitions to `done`. "Link the merged PR" implied a
post-merge update with no clear owner; in practice that meant the
update never happened.
Tighten the convention to: the last commit on the implementation
branch (before merge) does five things together —
1. Frontmatter: status → done, add `pr: <n>`
2. Validation checklist: flip each [ ] to [x] for verified
criteria. Unverifiable ones stay [ ] with a Notes entry
explaining where they'll close out. Never silently rewrite a
criterion to match what shipped — that's a separate plan
amendment commit.
3. Notes section: decisions, gotchas, surprises worth carrying
4. plans/README.md status table row: 📋 → ✅, link the PR
5. Commit message: `chore(plans): mark <slug> done (PR #<n>)`
Also softens step 2 (move to in-progress) — first commit on the
branch for non-tiny plans, skippable for small ones going straight
to done.
After merge: plan is frozen. Historical record.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontmatter: status → done, pr: 9.
Validation: all eight criteria re-verified against branch HEAD
just before commit:
1. fresh-install dev — evidenced by CI's npm-ci + the running
local stack
2. curl /api/health → {"status":"ok"} — HTTP 200 confirmed
3. web at :5173 serves / and /src/main.tsx — both HTTP 200
4. npm run type-check exits 0
5. npm run build produces apps/{api,web}/dist
6. CI green on all three pushes (25955278298, 25964750066,
25964864529)
7. package-lock.json tracked at root
8. no .js files in apps/api/src or apps/web/src
Notes populated with six load-bearing carry-forwards:
* Vite-dedupe workaround for hoisted React
* npm-install chain-overwrite gotcha that bit react/react-dom
mid-plan
* exactOptionalPropertyTypes is not part of strict
* Pinned dep / action versions current at cutover
* CLAUDE.md location decision
* .env.example deferred to api-skeleton
plans/README.md status table updated: 📋 → ✅, name links to PR.
First plan to use the done-state plan-update convention
introduced in the preceding commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-plan frontmatter (`status:`, `depends:`) is the source of truth
for both lifecycle state and graph shape. The status table and ASCII
DAG in plans/README.md duplicated both — rot-prone by construction
and never the place a curious reader should look anyway.
Removed:
- `## Status legend` (only consumer was the table)
- `## Initial DAG to ship spec-complete` (ASCII redraw of depends:)
- `## Status table` (parallel copy of frontmatter status + pr)
Replaced with a short pointer block on how to derive each from the
plan files: `grep '^status: in-progress' plans/*.md`, etc.
Closeout convention extended with a new **Follow-ups** section,
distinct from Notes:
- Notes = non-actionable carry-forwards (decisions, gotchas,
learnings) for *understanding*
- Follow-ups = actionable items that didn't ship with this plan
(issue links, downstream-plan pointers, or
"None.") for *triage*
Entry shapes documented; "None." is explicit so a future reader sees
the section was considered, not just absent.
The "update plans/README.md status table" step from the convention
introduced in d2d32b0 is removed (the table no longer exists).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the `.env.example` deferral out of Notes (where it didn't belong — Notes is for non-actionable carry-forwards) into the new Follow-ups section per the convention introduced in 221805b. Using the "Deferred to [`<plan>`](...)" entry shape since the work is already owned by api-skeleton. No other follow-ups identified: every other Notes entry is a learning / decision worth carrying forward, not an actionable unshipped item. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Deferred to <plan>" in a closeout Follow-ups section was a pointer
that didn't oblige anyone to do anything — the downstream plan
could ship without ever absorbing the deferred work, and the
deferral would rot in place. Caught when reviewing the workspace
plan's .env.example follow-up.
Tighten:
* "Deferred to <plan>" is only valid when the downstream plan is
still `status: planned`
* The same closeout commit must edit that downstream plan to
absorb the deferral — typically a new bullet under Approach and
a new criterion under Validation
* If the downstream plan is `in-progress` or `done`, use the
Issue shape instead — never modify a plan that's actively being
implemented or already frozen
Applied immediately to plans/api-skeleton.md to demonstrate the
protocol and actually close the workspace deferral:
* Approach.Env validation gains a paragraph requiring
.env.example to ship with one entry per EnvSchema field
* Validation gains a checkbox confirming it exists, cross-linked
back to the workspace plan that deferred it
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fixture directory at fixtures/private-storage-seeded/ is meant to **ship** in the code repo per specs/behaviors/private-storage.md: > Contributors either start empty (sign up via GitHub OAuth during > dev) or load a fixture-seeded directory shipped at > fixtures/private-storage-seeded/ Ignoring it would cause `git add fixtures/private-storage-seeded/` to silently no-op once those fixtures land with storage-foundation. Only the runtime ./private-storage/ working directory should be ignored. Comment expanded so the distinction is explicit. Flagged in PR #9 review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`exactOptionalPropertyTypes` is opt-in; it is NOT enabled by `strict`, so explicitly setting it to `false` was a no-op that read like a deliberate choice without a documented reason. If we ever decide to enable it (the upside is real but the ecosystem-compat cost can be high), that becomes a separate intentional change with its own justification. Flagged in PR #9 review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A short, pointer-focused landing page for a contributor arriving cold — explains the spec-driven posture, lists the canonical references (specs/, plans/, .claude/CLAUDE.md), and documents the four root npm scripts so `npm run dev` isn't a discovery quest. Recommended in the review on PR #9 and authorized here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontmatter: status → done, pr: 9.
Validation: all eight criteria re-verified against branch HEAD
just before commit:
1. fresh-install dev — evidenced by CI's npm-ci + the running
local stack
2. curl /api/health → {"status":"ok"} — HTTP 200 confirmed
3. web at :5173 serves / and /src/main.tsx — both HTTP 200
4. npm run type-check exits 0
5. npm run build produces apps/{api,web}/dist
6. CI green on all three pushes (25955278298, 25964750066,
25964864529)
7. package-lock.json tracked at root
8. no .js files in apps/api/src or apps/web/src
Notes populated with six load-bearing carry-forwards:
* Vite-dedupe workaround for hoisted React
* npm-install chain-overwrite gotcha that bit react/react-dom
mid-plan
* exactOptionalPropertyTypes is not part of strict
* Pinned dep / action versions current at cutover
* CLAUDE.md location decision
* .env.example deferred to api-skeleton
plans/README.md status table updated: 📋 → ✅, name links to PR.
First plan to use the done-state plan-update convention
introduced in the preceding commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements
plans/workspace.md— the foundational plan everything else in the DAG depends on. The PR grew well past its original scope: review-driven fixes, a CLAUDE.md relocation, a root README, and the first cut of a plan-closeout convention all landed here.Original workspace scaffold
apps/*,packages/*),type: module, scripts fordev,build,type-check,lint. Node pinned to 22.22.3 via.tool-versions;engines.nodebelt + suspenders.apps/api— Fastify 5.8 boots on${PORT:-3001}with a single/api/healthroute returning{"status":"ok"}. Pino-pretty in dev, JSON logs in prod. Under 20 lines per the plan.apps/web— Vite 8 + React 19 placeholder rendering "Hello, Code for Philly". Dev server proxies/api/*to:3001.resolve.dedupe: ['react', 'react-dom']is the workaround for Vite 8 + npm-workspace-hoisted React (react/jsx-dev-runtimewon't resolve without it).packages/shared— typed-module stub; Zod schemas land here whenstorage-foundationbegins.tsconfig.base.json(strict +noUncheckedIndexedAccess), ESLint flat config (@eslint/js+typescript-eslint+react-hooks),.gitignore,.editorconfig..github/workflows/ci.ymlrunsactions/checkout@v6→asdf-vm/actions/install@v4(picks up.tool-versions) →npm ci→type-check→lint→build. Test step deferred totest-harness.Review-driven fixes (PR #9 first review)
.gitignorestopped ignoringfixtures/private-storage-seeded/— that directory ships in the code repo perspecs/behaviors/private-storage.md. Only the runtime./private-storage/is ignored.exactOptionalPropertyTypes: falseremoved fromtsconfig.base.json— it's not part ofstrict, so the explicitfalsewas a no-op that implied intent without explanation.Documentation and structural housekeeping
CLAUDE.mdmoved to.claude/CLAUDE.md. Both locations are valid Claude Code project-scoped configs; collocating with.claude/agents/and.claude/commands/keeps Claude-specific config in one directory. Internal relative paths rewritten; three cross-file references (plans/README.md,plans/workspace.md,specs/architecture.md) updated.README.mdadded — short, pointer-focused landing page (recommended in the review).Plan-closeout convention established
This PR is the first to ship using the new convention, and the convention itself was hammered out during the PR:
status: done+pr: <n>, validation checkboxes ticked for verified criteria, Notes populated with non-actionable carry-forwards (learnings, decisions, gotchas), Follow-ups populated with actionable items not shipped.Issue [#N](link)for actionable + un-owned work,Deferred to <plan>for actionable + already-owned-by-a-downstream-plan,Tracked as: <pointer>for free-form, orNone.explicitly.Deferred to <plan>entry is only valid when the downstream plan is stillplanned, AND the same closeout commit must edit that downstream plan to actually pick up the work (typically a new Approach bullet + Validation criterion). If the downstream plan isin-progressordone, file an issue instead. Demonstrated by the.env.exampledeferral from this plan being absorbed intoplans/api-skeleton.md.plans/README.mdslimmed down: the status table and ASCII DAG were both deleted as redundant with per-plan frontmatter (status:anddepends:) and rot-prone. The README now points readers at the per-plan files and providesgreprecipes.Validation against
plans/workspace.mdAll eight criteria re-verified against the final branch HEAD just before the closeout commit (
ffbffe2):git clone … && npm install && npm run devworks on a fresh machine with only asdf preinstalled — verified locally and via three successful CI runscurl localhost:3001/api/healthreturns{"status":"ok"}http://localhost:5173/— verified via chrome-devtools-axi; rendered DOM shows<main><h1>Hello, Code for Philly</h1>…</main>npm run type-checkexits 0npm run buildproducesapps/api/dist/andapps/web/dist/.github/workflows/ci.ymlpasses on a clean push (three successful runs across the three pushes)package-lock.jsonis committed at root.jsfiles inapps/api/src/orapps/web/src/Commit map
23 atomic commits, roughly grouped:
648a280…6b5fdfd— scaffold (Node, root, api, web, shared, eslint, CI)1c39c04 293f799— first-review fixes (.gitignore, tsconfig)0d3ac74 73c4f09— CLAUDE.md relocation + root READMEd2d32b0 ffbffe2— plan-closeout convention v1 + workspace done-flip221805b 94e57ff 23d496d— convention refinement (drop status table + DAG, add Follow-ups, require absorption of deferrals)Test plan
npm ci && npm run devboots both servers on a clean clonecurl localhost:3001/api/healthreturns expected JSONhttp://localhost:5173/renders the placeholder🤖 Generated with Claude Code