The shared stack for Featherweight projects (Concert Crawl, The 99 and whatever comes next): the configs every project extends, the CI gate every project calls, and the version baseline every project is held to.
The design and the reasoning behind it are in the audit and design doc.
| Path | What it is |
|---|---|
packages/tsconfig |
base, strictest, nextjs and node TypeScript configs |
packages/eslint-config |
base, node and next ESLint flat configs |
packages/prettier-config |
The shared Prettier config |
packages/commitlint-config |
Conventional commits with the shared type list |
.github/workflows/node-ci.yml |
Reusable CI gate: install, then typecheck, lint, test and build; opt-in selected tests with a nightly full run |
.github/workflows/baseline.yml |
Reusable check of a project's versions against baseline.json; warns by default |
baseline.json |
The majors every project is held to (Node, pnpm, Next, React, Zod, Vitest, Sentry, Drizzle …), plus a Node security minimum |
plugins/fwd |
Claude Code plugin: the shared agent skills and their guard hooks |
tests/ |
Proves each config loads and enforces what it claims |
Planned, in order: the stack generator with opt-in capabilities, and a cross-project cost check.
The packages are not on npm yet. Until they are, install them from this repo by path:
pnpm add -D "@fwd-stack/eslint-config@github:featherweight-design/fwd-stack#path:/packages/eslint-config"TypeScript:
{ "extends": "@fwd-stack/tsconfig/nextjs" }ESLint (a project's own rules go after the shared ones):
import next from '@fwd-stack/eslint-config/next';
export default [...next, { rules: {/* project guards */} }];Prettier, in package.json: "prettier": "@fwd-stack/prettier-config".
CI, in the project's .github/workflows/ci.yml:
jobs:
gate:
uses: featherweight-design/fwd-stack/.github/workflows/node-ci.yml@main
with:
scripts: typecheck lint test buildA second reusable workflow compares the project against baseline.json. Add it beside gate:
jobs:
baseline:
uses: featherweight-design/fwd-stack/.github/workflows/baseline.yml@main
with:
mode: warnIt reads every tracked package.json (fixtures skipped) and checks:
- the declared major of each package
baseline.jsonlists, or major.minor for a0.xpin such asdrizzle-orm. A package the project does not use is skipped; - the Node pin (
.nvmrcor.node-version): the baseline major, and at leastnodeMinimum; packageManagerin the rootpackage.json: pnpm at the baseline major.
Each mismatch is one annotation on the run. mode: warn (the default) passes; mode: fail fails
the job. kit-ref (default main) picks the fwd-stack ref the baseline and the check are read
from. A documented exception goes in package.json:
{ "fwdBaseline": { "ignore": ["zod"] } }In the root package.json the list covers every package.json in the repo; in a nested one, only
that file. "node" or "pnpm" in the root list skips that check.
This is optional, and off by default. A project whose suite is slow can run only the tests a change needs on pull requests and pushes, and the whole suite on a schedule. Name a package.json script that runs the selected tests:
on:
pull_request:
push:
branches: [main]
# The full suite on main, nightly: the net under anything the selection missed.
schedule:
- cron: '17 9 * * *'
workflow_dispatch:
jobs:
gate:
uses: featherweight-design/fwd-stack/.github/workflows/node-ci.yml@main
with:
scripts: typecheck lint test build
selected-test-script: test:selectedWith selected-test-script set, the step that would run test works like this:
- Pull request. Runs
test:selectedwithCI_BASE_REFset toorigin/<base branch>. - Push. Runs
test:selectedwithCI_BASE_REFset to the commit before the push. A new branch, or a forced push whose old head is gone, has no base to compare against, so it runstestin full. - Anything else (the
schedule, aworkflow_dispatch). Runstestin full.
The checkout is then made with full history, so the base commit is there to compare against. The
selected script must exist: it runs without --if-present, so a misspelt name fails the job instead
of passing without running a test. With the input empty, the workflow runs exactly as it always has.
How a project selects is its own business. The script reads CI_BASE_REF. The simplest form is
Vitest's own. A change that touches no test, such as a README edit, selects none, and Vitest exits 1
on an empty run unless it is told not to:
{
"scripts": {
"test:selected": "vitest run --changed \"$CI_BASE_REF\" --passWithNoTests"
}
}An import graph cannot see a test that reads files instead of importing them, such as a guard that scans the source tree. A project with tests like that should add them to every selection, or have its selector run the full suite when a shared config changes. The nightly run catches what the selection missed, but only the next day.
A red nightly run has no pull request to block, so nobody sees it unless something reports it. GitHub emails whoever last edited the schedule, and an orchestrator session never reads that email. Two ways to put it where the orchestrator looks:
-
Check it on every status pass. This prints the last scheduled run's result:
gh run list --workflow ci.yml --event schedule --limit 1 \ --json conclusion,url,createdAt
A
failurethere is reported like any other red CI, and a lane is briefed to fix it before anything else merges. -
Open an issue from the run. Add this job beside
gatein the caller. It opens one issue, labelled so the backlog shows it, and adds a comment for each later red night while it stays open:nightly-red: needs: gate if: ${{ failure() && github.event_name == 'schedule' }} runs-on: ubuntu-latest permissions: issues: write steps: - env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | open=$(gh issue list --label ci:nightly-red --state open \ --json number --jq '.[0].number // empty') if [ -n "$open" ]; then gh issue comment "$open" --body "Failed again: $RUN_URL" else gh label create ci:nightly-red --color B60205 --force gh issue create --label ci:nightly-red \ --title "Nightly full suite failed on $(date -u +%F)" \ --body "The scheduled full test run failed: $RUN_URL" fi
The orchestrator then finds it with
gh issue list --label ci:nightly-red. Close the issue in the PR that fixes the failure.
This repo is also a Claude Code plugin marketplace. The fwd plugin carries the skills and hooks
every project's agents share, so each repo runs the same review gate instead of its own copy.
/plugin install fwd --marketplace featherweight-design/fwd-stackThat one-step form needs Claude Code 2.1.275 or later. On an earlier version, add the marketplace first:
/plugin marketplace add featherweight-design/fwd-stack
/plugin install fwd@fwd-stack| Skill | What it does |
|---|---|
pr-review |
The Copilot review cycle and merge bar: the skill, its gate (pr-gate.mjs), watcher and hook |
feature-acceptance |
Proves a UI feature landed on every route and viewport it claimed |
design-review |
Compares an implementation against its design handoff, property by property |
design-status |
A fixed-shape readout of the design pipeline's state |
css-layout-triage |
Measurement-first triage for layout bugs that resist fixing |
complete-and-ship |
Finishes a plan's remaining tasks unattended, then opens and reviews the PR |
epic-run |
Runs a multi-PR feature epic end to end with per-PR and whole-vertical gates |
feature-run-report |
Emails the run report (progress, ready to test, waiting on you) at the end of a run, or when it blocks |
progress-chart |
Status as a chart: one bar per named feature line from merged PRs, detail per group on request |
usage-report |
Splits recorded Claude Code usage (weighted tokens) by project, model and session |
copy-review |
Writing and reviewing customer-facing copy against the project's voice and locked vocabulary |
infra-setup |
Hosting, database and monitoring defaults, staging footprint and a names-only secrets inventory |
backlog-audit |
Audits, triages and prioritises the GitHub Issues backlog, and governs intake |
ui-audit |
Runs the project's UI sweep, triages captures for layout breakage, files on approval |
prose-pass |
Rewrites long-form in-app prose in the project's voice, with a fact check (own apps only) |
run-project |
The standard operating model: one orchestrator, a capped set of lanes for PR work, subagents for bounded reads |
orchestrator |
Coordinates lane sessions: assigns work, watches stalls and context, rotates lanes, reports status |
lane |
Works under an orchestrator: stays inside its brief, waits so the wait notifies, writes its handoff |
Each skill first reads .claude/fwd/<skill>.md in the project, if present: the project's own
tools and rules for that skill, which win where they conflict. Settings the skills read (for
example uiSweep, ownApp) live in .claude/fwd.json.
State the skills keep, such as the pre-open pass ledger, lives in the plugin's data folder
(~/.claude/plugins/data/fwd-fwd-stack/), never in this repo.
machine/switch.sh is the one-time install: it installs the plugin and makes the machine
consistent. It works on a fresh machine and on one that ran these skills from ~/.claude/skills.
Updates after that do not need it (see Updating). Backups are kept in
~/.claude/backups/fwd-switchover-<time>/:
machine/switch.sh --dry-run # print every step, change nothing
machine/switch.sh # back up, install, verify; rolls back on any failed check
machine/rollback.sh [backup] # undo the last run, or the run whose backup folder you nameIt does five things:
- Installs
fwd@fwd-stack. - Retires user-scope copies of the plugin's skills and hooks.
- Carries the pre-open ledger into the data folder.
- Links
~/.claude/fwdto the plugin, so commands and docs outside a hook have a stable path, such asnode ~/.claude/fwd/skills/pr-review/pr-gate.mjs <pr>ornode ~/.claude/fwd/hooks/autonomous-guard.mjs arm …. - Points
~/.claude/CLAUDE.mdat that path.
machine/verify.sh checks that the guards answer as they should: the Neon guard, the merge
gate, the autonomous-run guard and the secret-print guard (which denies Bash commands that would
print a secret, such as an environment dump or cat .env). Run the switch when no session is mid-review or mid-merge,
then restart sessions so they load the plugin's hooks.
Claude Code fetches a new copy only when plugins/fwd/.claude-plugin/plugin.json's version
changes, so a merged change to the plugin does nothing on a machine until that number is raised.
Raise it in the change itself, or in a follow-up PR.
Automatic (recommended). Turn on auto-update for the fwd-stack marketplace once per machine:
/plugin → Marketplaces → fwd-stack → Enable auto-update, or set it in
~/.claude/settings.json:
"extraKnownMarketplaces": {
"fwd-stack": {
"source": { "source": "github", "repo": "featherweight-design/fwd-stack" },
"autoUpdate": true
}
}Claude Code then fetches a new version in the background a few minutes into a session and shows
Plugin updated: fwd · Run /reload-plugins to apply. Run /reload-plugins in each open session,
or just start new ones.
By hand. machine/update.sh updates the marketplace and the plugin, then re-links. It is safe
while sessions run. Then /reload-plugins in each open session.
Either way ~/.claude/fwd follows on its own: the plugin's fwd-link hook re-points it at the
installed version at the next session start or prompt. It only re-points a symlink into the fwd
plugin cache, and only to a newer version of the user-scope install; it leaves anything else alone.
pnpm install
pnpm format:check && pnpm typecheck && pnpm lintThat is the whole local gate before a push. Tests run in CI only (the pr-review skill, §5).
Raising a version: change baseline.json first, then each project. A config change that would
fail an existing project lands with that project's fix PR linked in the description.