m3l-groundwork is a command-line tool that sets up (or reports on) a TypeScript project's toolchain and Claude Code setup, then keeps that setup current with live upstream guidance.
Read this project's rendered docs site at groundwork.monte3l.com.
- Someone starting a new TypeScript project who wants a strict toolchain and a working Claude Code setup from the first commit, instead of assembling both by hand.
- A team with an existing TypeScript project who wants an honest, read-only report on how it compares to that same baseline, with no risk of an automated tool rewriting their repo.
- Anyone who wants their project's Claude Code configuration and TypeScript toolchain to stay aligned with current official guidance, rather than frozen at whatever was true the day it was set up.
- Claude Code documentation -- what it is and how to install it.
docs/glossary.md-- plain-English definitions of the terms used throughout this repo's docs (harness, skill, pack, gate, and so on).- TypeScript Handbook -- if TypeScript itself is new to you.
Install Node 24+, pnpm
and Claude Code first
(see "Requirements" below for exact versions). Nothing is
installed globally: npx and pnpm dlx fetch the CLI, run it once and leave
only a download cache behind.
A new project. Point the CLI at an empty or missing directory:
# with npx
npx @monte3l/groundwork my-app
# or with pnpm
pnpm dlx @monte3l/groundwork my-appIt writes the baseline, runs git init and pnpm install, and installs the
/customize skill. Add --pack <name> to install an optional
pack at the same time (--list-packs shows them).
An existing project. Run the CLI from the project's root, or pass its path. It detects that the directory already looks like a project and switches to adopt mode, which surveys it read-only and never overwrites a project file:
cd path/to/your-project
# with npx
npx @monte3l/groundwork .
# or with pnpm
pnpm dlx @monte3l/groundwork .It writes a report and staged copies of what it would add under
.groundwork/, plus one guarded copy of the /customize skill.
Then, in either case:
cd my-app # or stay in your existing project
claude # open the project in Claude CodeRun /customize inside Claude Code. For a new project it interviews you and
tailors the baseline. For an existing one it first reconciles the survey
against your repo and confirms each change with you before editing anything.
Note
Earlier releases shipped on other dist-tags.
If you installed a 0.x release from next or a 1.0.0-rc release from
rc, switch to the default latest tag: a prerelease version range never
crosses to 1.0.0 on its own. See
"Versioning policy" below for what counts as the
CLI's public API, frozen since 1.0.
A two-phase bootstrapper. Point it at a directory and it leaves you with a strict TypeScript toolchain, a Claude Code harness, and CI wired to the same gates you run locally.
Phase A (packages/cli) is deterministic: an offline Node CLI with no
runtime dependencies that writes the baseline. It makes no network call
beyond the final package install, and the same input always produces the
same output.
Phase B (packages/plugin) is adaptive: the /customize
skill, which interviews you (or, for an existing project, reconciles the
CLI's survey against the real repo) and then sweeps current official
TypeScript and Anthropic sources, so the result reflects upstream guidance
today rather than whenever this repo last shipped.
They stay two separate phases on purpose, rather than one -- see
docs/architecture.md for why.
The mode is auto-detected from the target directory; --fresh and --adopt
override it.
| Mode | When | What it does |
|---|---|---|
| Fresh | The target is empty or missing | Writes the full baseline, installs any --pack, installs the /customize skill, runs git init, then pnpm install. |
| Adopt | The target already looks like a project | Surveys it read-only and writes .groundwork/: an inventory, a report, inert .staged copies of the baseline files it would add (.groundwork/baseline/) and of the packs (.groundwork/packs/). It writes only under .groundwork/, plus one guarded copy of the /customize skill, and never overwrites a project file. |
That copy lands at .claude/skills/customize/, or at .groundwork/customize/
when something already there blocks it. It is where the real work happens: it reads the survey, confirms
each change with you, and only then edits your project.
A pack is an optional bundle of extra files -- an
agent, a hook, a gate -- installed on top of the baseline for projects that
want more than the capped default. A pack never edits YAML or JavaScript;
it only adds files and extends three JSON files the baseline already
reads. See templates/packs/README.md for
the wiring contract.
| Pack | Contents |
|---|---|
harness-extras |
The compaction-handoff hook pair and a read-only Bash guard. |
statusline |
A five-row Claude Code status line plus a per-subagent row renderer, width-fit to the terminal. Sets the statusLine and subagentStatusLine keys. |
quality |
A per-file size ratchet gate over src/ and tests/, and a read-only type-design-analyzer review agent. Adopt-capable, recommended for every project kind. |
frontend |
A read-only ui-reviewer accessibility agent (static WCAG 2.2 review) and a new-component skill that mirrors a sibling component. Recommended for the frontend kind only. |
service |
A read-only performance-analyzer agent (static review for blocking calls and unbounded work; profiling is reported as NOT CHECKED) and an api-doc skill that writes an OpenAPI description from the routes the code defines. Recommended for the service kind only. |
claude-github |
Anthropic's official Claude Code GitHub Action as two workflows: @claude mention-mode and an automated PR-review comment on every pull request -- no hooks, no gate. Needs an auth secret the pack cannot create, and the review spends API credits per PR. |
github |
Three gh-CLI skills (Dependabot PR triage, scan-alert triage, PR-check watching) -- no hooks, no gate, no workflows. |
publishing |
A release pipeline (changesets version-PR / staged, provenance-attested npm publish via trusted publishing). Fresh mode only. |
licensing |
An SPDX/REUSE license-header gate (check-license-headers.mjs, a lint-group verify step) and a REUSE.toml template. Fresh mode only, because the one-time backfill fails an established project's pnpm verify. |
supply-chain |
Secret scanning (gitleaks.yml) and an OpenSSF Scorecard run (scorecard.yml) for any GitHub project, published or not. Two workflows (read-only apart from the Scorecard job's security-events and id-token write), adopt-capable, recommended for every project kind. |
worktrees |
Enforces that all src/tests development happens inside an isolated git worktree, on any branch, for any caller: a working-in-worktrees skill (start/status/sync/finish/fan-out), a dependency-install backstop hook, a guard stricter than the baseline's own branch/hub guards, and a .worktreeinclude copying .env/.env.local/.env.*.local into every worktree Claude Code creates. Changes the day-to-day workflow -- opt-in, not recommended by default. |
In fresh mode --pack installs a pack directly. In adopt mode the CLI only
surveys which packs apply and stages them under .groundwork/packs/ (and
the baseline files it would add under .groundwork/baseline/);
/customize installs from there after you confirm.
| Flag | Effect |
|---|---|
--name <project-name> |
Override the project name (default: the target directory's basename). |
--skip-install |
Skip the final pnpm install (fresh mode only). |
--force |
Overwrite a non-empty target directory (fresh mode only). |
--adopt |
Force adopt mode, even if the target looks empty. |
--fresh |
Force fresh mode, even if the target looks like an existing project. |
--pack <name> |
Install an opt-in pack from templates/packs/ (fresh mode only; repeatable). |
--list-packs |
Print every available pack and exit. |
--help, --version |
Print usage, or the CLI's version. |
An unexpected failure (anything that is not a bad invocation) prints its
error name before the message, for example TypeError: .... Set M3L_DEBUG
to any non-empty value to also print its stack trace.
- Node 24+.
.node-versionis the authority. - pnpm. Fresh mode ends with a
pnpm installin the new project (skip it with--skip-install), and this repo pins 12.8.1 throughpackageManager. - git.
- Claude Code, for Phase B only. The CLI itself does not need it.
The Quickstart above covers the common case. Here are two other ways to run this project.
To run it from a checkout instead (or to work on it), build it and call the built entry point directly:
git clone https://github.com/monte3l/m3l-groundwork.git
cd m3l-groundwork
pnpm install
pnpm build
node packages/cli/bin/m3l-groundwork.mjs ../my-new-projectThe /customize skill is also available as a Claude Code plugin, for use in
a project that was not bootstrapped by this CLI:
claude plugin marketplace add monte3l/m3l-plugins
claude plugin install m3l-groundwork-customize@monte3l/plugin marketplace add monte3l/m3l-plugins
/plugin install m3l-groundwork-customize@monte3l
The first pair runs in your shell, the second inside a Claude Code session. Settings and management are in the plugin README.
npx leaves nothing installed globally -- it only populates its own
download cache (npm cache clean clears it, if you want that back too).
my-new-project/
├── src/, tests/ # placeholder entry point and test
├── tsconfig*.json # strict flags, split into base / tooling / build
├── eslint.config.js # typescript-eslint, flat config
├── vitest.config.ts # v8 coverage with a per-file gate
├── lefthook.yml # pre-commit, commit-msg and pre-push hooks
├── bin/verify.mjs # runs the same five gate groups CI runs
├── .github/workflows/ # ci, dependency-review, security-audit
├── .claude/ # agents, skills, hooks, rules, settings.json
├── CLAUDE.md # the agent-facing project guide
└── docs/research/ # living trackers for the guidance sweeps
pnpm verify runs the same five groups
(format, lint, typecheck, build, test) that the pre-push hook and
CI run, all read from one list in bin/lib/verify-steps.mjs. The baseline
is also graded by two offline checks, one for the harness and one for the
toolchain, that run as part of pnpm verify.
The baseline is deliberately capped: at most 5 agents, 8 skills, 10 hooks, 3 CI workflows and 12 root scripts. Anything useful beyond that ships as a pack instead.
The skill lands at .claude/skills/customize/ in the target project. Open it
in Claude Code and run /customize.
In adopt mode the CLI never removes or overwrites anything the project owns
there. A .claude/skills/customize/ that holds only files with none of the
skill's names is used as is, and the skill is added beside them. If the
project already owns an entry under one of the skill's names, the CLI writes
the skill to .groundwork/customize/ instead and says so; Claude Code does
not load that copy, so run the plugin's /customize or replace the project's
copy yourself. If .claude, .claude/skills or .claude/skills/customize is
a symlink or not a directory, no project copy exists: fix that path, copy the
staged skill into place, or run the plugin's /customize. Fresh mode with
--force does replace an existing skill copy. Fresh mode also checks, before
it writes anything, that no directory component of a destination below the target directory and no existing
file the baseline or a --pack would write is a symlink (or a non-directory
where a directory is needed); the same check covers the /customize skill copy's destination. If one fails, it refuses and writes nothing. The
target directory itself and its ancestors are not checked, and neither are
the .git directory git init creates and the node_modules the install step
fills: a symlink there is followed.
- Fresh project: a short interview (project kind, runtime target, test strictness, CI depth) tailors the baseline, then a guidance sweep checks the result against current upstream sources.
- Adopted project: Step 0 reconciles the survey against the real repo and confirms each change before anything is written, then the same guidance sweep runs.
pnpm verify runs every gate. The pre-push hook and CI run the same five
groups by name, so anything pnpm verify catches, CI catches too. CI adds
the end-to-end jobs on top (pnpm test:e2e on Linux and macOS), a Node-current
verify run and a DCO check, all rolled up by the verify job. pnpm test:e2e
is the real acceptance test: it
bootstraps throwaway projects with the built CLI and runs their own
pnpm verify. It is slow and touches the network, so it is not part of
pnpm verify; run it yourself after any change to packages/cli/src/,
templates/core/ or the /customize skill. pnpm eval makes paid model
calls and never runs in CI.
See CONTRIBUTING.md for the full setup and PR workflow.
See CONTRIBUTING.md for how
@monte3l/groundwork is released, published, and verified, and
CLAUDE.md for the full design.
The /customize plugin has no release of its own. The central
monte3l/m3l-plugins marketplace lists it pinned to a CLI release tag
(@monte3l/groundwork@X.Y.Z), so a plugin change reaches marketplace users
only after the next CLI release and a pin-bump PR in monte3l/m3l-plugins.
CLAUDE.md is the full reference: architecture, the command
table, conventions, and known gaps.
CONTRIBUTING.md-- how to contribute.GOVERNANCE.md-- decision-making, roles, and access continuity.ROADMAP.md-- what's planned, and what isn't.SECURITY.md-- reporting a vulnerability, and how to verify a release.docs/architecture.md-- a high-level map of the two phases.docs/assurance-case.md-- the threat model and security design argument.docs/glossary.md-- plain-English definitions of this repo's jargon.CODE_OF_CONDUCT.md.
Exit 2 is a bad invocation, raised before anything is written. Exit 1 is
everything else that goes wrong at run time.
| Situation | Exit | What happens / what to do |
|---|---|---|
Unrecognized flag, missing flag value, repeated --name, missing or extra target argument |
2 | Prints the error and the usage text. |
--adopt with --fresh |
2 | The two are mutually exclusive. |
--name is not a valid npm package name, or a --pack value is not a valid pack name |
2 | Prints the rule it broke and the usage text. |
Unknown --pack |
2 | Lists the available packs. |
--pack for a pack without fresh mode |
2 | Says the pack does not support fresh mode and lists its modes. |
Adopt mode with --adopt and a missing target directory, --force or --pack |
2 | Adopt never writes project files and surveys every pack itself; run /customize to install a pack. |
| Windows, fresh mode | 1 | Refuses before writing anything: Linux and macOS only. Adopt mode, --help, --version and --list-packs still run. |
Fresh target is not empty and --force is absent |
1 | Refuses; pass --force to overwrite, or --adopt to survey it. |
Fresh mode, /customize skill install fails |
1 | The project is already written and git init and pnpm install did not run. Re-run with --fresh --force plus your original --name, --pack and --skip-install; a plain re-run adopts it. |
Fresh mode, git init fails |
1 | The project is left written. Run git init (and pnpm install unless --skip-install) yourself. |
Fresh mode, pnpm install fails |
1 | The project is left written without dependencies. Run pnpm install there yourself. |
| Adopt mode fails after it starts deleting the previous run's files | 1 | The message names which of inventory.json, adoption-report.md and adoption-decisions.json were removed; fix the cause and re-run. A removed adoption-decisions.json is not recreated. |
| A pack's manifest fails to load | 1 | A broken install, not a bad invocation. |
A flag value that starts with - counts as missing, and --pack is the only value
flag you can repeat. --help, --version and --list-packs exit 0, but an
unrecognized or malformed flag anywhere on the command line still exits 2 first.
In adopt mode the signal that chose the mode is printed as
adopt mode: <signal> and recorded in the report's **Mode:** line. The
fresh-mode signals only explain the detection: nothing prints them.
| Signal | Mode | When |
|---|---|---|
<dir> does not exist yet |
Fresh | The target is missing. |
<dir> is empty |
Fresh | The target is an empty directory. |
<dir> exists but has no recognizable project markers |
Fresh | None of the adopt signals below match. |
found package.json |
Adopt | A package.json is in the target. |
found a .git directory |
Adopt | A .git directory is in the target. |
found a .git file (a worktree or submodule) |
Adopt | .git is a file. |
found <name> |
Adopt | A loose .ts, .tsx or .js file is in the target. |
--adopt forced |
Adopt | --adopt was passed. |
--fresh forced |
Fresh | --fresh was passed. |
.groundwork/adoption-report.md is written in this order. Headings are
exact strings.
| Heading | Present |
|---|---|
# Adoption report |
Always, followed by a Generated ... line and the **Mode:** line |
## Codebase shape |
Always |
## Toolchain enforcement in effect |
Always |
## Toolchain grade |
Always; says there is nothing to grade when no toolchain files exist. Has ### Toolchain wiring findings and ### Toolchain quality findings (advisory) when there is something to grade |
## Existing Claude Code harness |
Always; short when there is no .claude/ directory |
## Harness grade |
Always; says there is nothing to grade when neither .claude/ nor CLAUDE.md exists. Has ### Wiring findings, ### Quality findings (advisory) and ### Baseline harness files this project has changed when applicable |
## Human-facing docs & guidelines |
Always |
## Baseline caps after a merge (approximate) |
Always |
## Available packs |
Always, with one ### <pack name> per pack when any packs exist |
## What groundwork would change |
Always |
## Could not be determined |
Always |
## Next step |
Always, followed by a staged-files note (when anything was staged) and a note that .groundwork/ was not added to .gitignore |
The CLI never edits your .gitignore: the closing note says so and leaves
the choice to you.
@monte3l/groundwork follows Semantic Versioning.
Until 1.0.0, breaking changes ship in a minor release, as SemVer allows
for major version zero -- see the changelog for
examples. From 1.0.0, the public API is:
- CLI flags, modes, and exit codes --
--helplists every flag; exit0is success,1is a runtime failure,2is a bad invocation (see Exit codes and failure behaviour). The mode is announced on stdout asadopt mode: <signal>(adopt only) and in the report as**Mode:** adopt -- <signal>; the signals are listed under Mode signals. .groundwork/inventory.json-- aschemaVersionbump that/customizecan't read is a breaking change..groundwork/adoption-report.md's section headings, listed under Report headings..groundwork/packs/and.groundwork/baseline/staging layouts, and pack names. Baseline files are staged as inert copies with a.stagedsuffix appended to each name (so no project tool discovers them), listed with their original path and asha256ininventory.json'sstagedBaseline. Packs follow the same convention:.groundwork/packs/<name>/pack.json.stagedand.groundwork/packs/<name>/files/<path>.staged, listed with their original path and asha256ininventory.json'sstagedPacks. All packs are staged together: written to a temp sibling directory and swapped in by rename, never half-written.- The
engines.nodefloor -- narrowing it is breaking; widening it isn't. /customize's invocation name, and the promise that it reads every inventoryschemaVersionthe CLI has ever emitted since 1.0.
Not covered by this policy: the contents of the baseline the CLI
emits (templates/core/, templates/packs/). What a fresh bootstrap
writes into your project follows current upstream TypeScript and Anthropic
guidance and can change in a minor release -- that's the whole point of
/customize's live guidance sweep. Also not covered: internal modules
(anything under packages/*/src/ not listed above) and the exact prose of
adoption-report.md's body.
Deprecation: anything on this list gets at least one minor release with a visible warning before it's removed in a major release.
MIT. The license covers the bootstrapper itself, not what it writes: the code the CLI emits into your project is yours to use and relicense as you see fit.