tech-stack-setup
Guide a user to a coherent technology stack — language, frameworks, database, host, and providers that work well together — then generate a repository conforming to STACKSPEC, the organization's techn
Files36
View on GitHubSKILL.mdclaude/plugins/project-setup/skills/tech-stack-setup/SKILL.md
GitHubTech stack setup — a guided wizard, then a generator
This skill does two things:
- Guides the user to a coherent stack. It is a filtered interview, not a menu. Every option offered is compatible with everything already chosen, and every consequence is stated at the moment of the choice.
- Generates a repository conforming to
STACKSPEC8.1.0, the binding invariants specification atreferences/invariants.md.
The two halves, and why they are separate
STACKSPEC 8.1.0 contains no technology names. It specifies properties:
contract-first APIs, atomic enqueue, adapter boundaries, retention and erasure,
consent-gated analytics, three-tier promotion. These hold whether the product is
TypeScript on Vercel or Rust in a container.
The technology knowledge lives in assets/catalog/, which is data, not
requirements. Nothing in the catalog is graded. STACKSPEC §0.2 defines a
fourth requirement level, CHOICE, for this purpose. A CHOICE is reported
as recorded or unrecorded, never as conforming or violating.
Read the spec before acting on it. It has about 1,050 lines. Read the sections a step needs. Do not work from this file's summary, and do not generate against a clause you have not read.
Scope — what this skill does not do
It does not build authentication. AUTHSPEC is a separate binding spec owned
by the sibling auth-setup skill. This split exists to stop running both in one
pass. A user who asked to "set up the stack" got a full auth stack that they had
not asked for and could not review in one sitting.
AUTHSPEC §2.1 resolves the stack by finding a declared stack document in the
project, and the Stack Profile this skill writes is that document. So the
order is not a preference: auth-setup has nothing to resolve against until this
skill has finished.
Phase 9 asks whether to run it. Never start it automatically, not even if the user said "set up the whole thing" at the beginning.
The distribution rule — do not violate it
A product MUST NOT contain a copy of the spec (§2.1, §16). The spec lives
here, versioned with this plugin. A product carries a Stack Profile instead:
assets/stack-profile.md, deployed to docs/architecture/stack-profile.md (STACKSPEC §18; a repo that still has it at docs/specs/stack-profile.md is read there until it moves), naming the
spec version it conforms to plus its selections, capability gaps and deviations.
A vendored copy becomes a second, competing authority. No CI check can compare it against the original. The profile's version reference is the only thing that can go stale in a way that can be detected.
Mode 1 — The wizard
Setup pattern — how this wizard talks to you
This setup follows section 13 of docs/features/delivery/shared-self-hosted-runners.md.
- Plan first. After the consequence review (Phase 5) and before Phase 7 changes anything, show one plan: the folders, files, packages and CI files it will generate (with paths), the installs it will run (
pnpm install, the hooks), what it changes outside the repository (new cloud projects and buckets from the provisioning ledger), what needs the owner (a yes, a sign-in, a production secret) and about how long it takes. Ask for one yes. Phase 6 questions stay. - Do the heavy lifting. After the yes, run every install and configuration yourself: the generation steps of Phase 7,
pnpm install, the git hooks, dev provisioning where a CLI exists, and the checks of Phase 8. Leave to the owner only what you cannot do: production secrets (run with the!prefix), a web-only setting, a sign-in. Give each an exact step and check it afterwards. - One line per step. While it runs, print one line per step: what you are doing, then done, or failed with the reason and the fix.
- Finish with three parts (Phase 8): Done (what was generated, with paths), Left for you (numbered, exact steps: deferred ledger rows, profile fields to fill, production secrets), What you can do now (the commands and features now available, each with a one-line example:
pnpm devstarts the apps,pnpm testruns the tests,python3 scripts/delivery/branch_gate.py --no-publishruns the branch gate,/luismc-project-setup:auth-setupbuilds authentication,/luismc-project-setup:tech-stack-setupin Mode 2 checks conformance). - Safe to run again. A second run says what is already set and changes only what is missing. It never generates over existing code (Mode 2 applies).
STACKSPEC is prescriptive enough to generate from directly. There is no
template repository.
Phase 0 — Establish the target, then announce the foundation
Confirm the working directory, that it is a git repo (git rev-parse --show-toplevel; offer git init if not), and that it is empty or nearly so.
If it already holds a product, switch to Mode 2. Do not generate over existing
code.
Then show the fixed foundation and ask nothing about it. From
assets/catalog/roles.yaml, the fixed: block:
| Layer | Fixed |
|---|---|
| Repo | Git, monorepo apps/ + packages/, docs/specs + docs/notes |
| CI | GitHub Actions, path-filtered, twelve required gates (§13.1), gated E2E (§11.2), a default-branch run that skips a tree already tested |
| Delivery | a branch gate with a verifiable result, a pre-push guard hook, a merge script and a CI watcher (§13.5, §13.6) |
| Promotion | preview per PR, staging on merge, production on release (§13.2) |
| Security | gitleaks pre-commit + CI over full history, dependency scanning |
| Contract | One machine-readable contract, authored first, all clients generated from it (§4.1) — the format is chosen in Phase 3 |
| Secrets | three tiers, one validated config schema, value-free example env |
Everything else is chosen. Say that explicitly. It sets the expectation for the next eight phases.
Phase 1 — Profile the product
Six questions, none about technology, from roles.yaml's profile: block:
team expertise · cost posture · ops appetite · compliance and residency ·
lock-in tolerance · does mobile ship.
Mobile has the largest cascade of the six. It forces generated Swift and Kotlin clients, push credentials and store declarations, and it makes hypermedia web approaches cost double. Ask it even when the answer seems obvious.
Two answers are worth reacting to immediately:
- Compliance is anything but
none→ the free bundle is eliminated outright. Free tiers rarely offer a DPA and often pin no region. Say so now, not later. - Lock-in tolerance is "want a clean exit" → single-vendor bundles are the highest-lock-in options in the catalog. Surface the contradiction. Do not let both answers stand.
Phase 2 — Entry fork
Three ways in, not two:
| Path | What it does |
|---|---|
| Recommend me a stack | Derive 2–3 complete candidates from Phase 1 and present them side by side. Most users should take this. |
| Infrastructure first | Host constrains language. |
| Language first | Language constrains host. |
The two ordered paths are the same solver with a different binding order —
roles.yaml's entry_paths: holds both orders.
Phase 3 — Bind the roles, filtering after every answer
Work the chosen order. For each role:
- Filter candidates against everything already chosen, using the
conflicts,requiresandhostingscores in the catalog files. - Compare the survivors on the axes that decide it: cost band, ops burden, lock-in, and fit with what is already chosen. Use a table, not prose.
- Recommend one, with a reason referencing the Phase 1 profile.
- Say why anything was eliminated. "ElastiCache is VPC-only and you said no VPC" is the guidance. Silently omitting it is not.
The cross-constraint that matters most
Server language largely determines which hosting is viable. From
tech/hosting.yaml:
- Picking Vercel or Cloudflare first eliminates Go, Rust and .NET for the API.
- Picking Go, Rust or .NET first eliminates them as API hosts, but not
as web hosts. Offer split hosting (web on an edge platform, API in
containers). Do not treat "not all one vendor" as a failure.
§4.1's generated client is the seam, and it already has to exist.
Vendor roles are open, and that is the point
Every provider (database engine and host, cache, storage, mail, error tracking)
is a CHOICE. None produces a deviation. Say out loud why this is safe:
§3.3 confines each vendor SDK to exactly one adapter module. That turns
"we picked the wrong provider" into a change to one file. That rule is what
allows the freedom. Mention it when the user hesitates over a vendor.
No role is closed, but four carry a property the candidate must satisfy. The
spec names no technology anywhere (§20 makes doing so a defect). Present these
as a bar to clear, not a list to pick from:
| Role | The property (§) |
What falls out, and why |
|---|---|---|
| Contract format | Maintained generators for every client language (§4.1) |
Anything without a Swift or Kotlin generator, when mobile ships |
Authenticated bounce/complaint feedback into a suppression list (§8.1) |
Bare SMTP with no event channel — SMTP plus a feedback source conforms | |
| Push | Device tokens never disclosed outside the §10.4 register (§8.3) |
Nothing. A push vendor is priced, not banned — it adds a register entry |
| Analytics | Non-essential events gated on server-side consent at an ingestion point you control (§10.3) |
Any SDK reporting to a backend you don't control |
Users argue with the analytics property. It excludes Vercel Web Analytics, Cloudflare Web Analytics, CloudWatch RUM, Pinpoint and GA, but on architecture, not brand. An SDK that reports to a vendor backend cannot be gated on consent state that the backend never sees. A vendor that offers consent-gated ingestion under your control passes the clause. Say it that way: "not allowed" invites a fight, and "here is the bar" does not.
State the consequence too. With first-party collection, §14's field-vitals
budgets need web-vitals reporting to your own endpoint, which is real work.
Phase 4 — Announce the derived decisions
Do not ask these. Compute and state them:
| Derived | From | Source |
|---|---|---|
| Queue substrate | db_engine |
tech/data.yaml → queue_derivation |
| Data access + migrations | language + db_engine |
tech/data.yaml → engine data_access |
| Package manager, task runner, lint, test, boundary check | language |
tech/languages.yaml → derives |
Explain the queue derivation, do not just state it. If the engine supports
transactional enqueue, the queue is a table in the product's own datastore and
enqueue happens inside the business transaction. If it does not, an outbox with a
relay is mandatory, and that is a §18 item 14 capability gap.
Say what is counter-intuitive: an external queue is not an upgrade. Where the engine supports transactional enqueue, moving the queue out adds a sub-processor and brings back the dual-write problem that the outbox then has to solve.
Phase 5 — Consequence review
Show the complete stack on one screen. Then run the validator:
python3 assets/check_stack.py --selection <selections.yaml>
It reports errors (combinations that cannot be built) and warnings
(capability gaps that STACKSPEC requires to be recorded). Do not skip it
because the reasoning "already checked". A wizard that reasons its way to "Go on
Vercel" in a long session is the failure the script exists to make
impossible.
For each warning, capture a one-line reason and the compensating control. These
become §18 item 14. Pushback rules, from roles.yaml:
| Situation | Action |
|---|---|
| Hard conflict | Block. Offer the nearest working alternative. |
| Weakens an invariant | Explain, capture a reason, record it |
| Ordinary tradeoff | State it once, proceed. No ceremony. |
Phase 6 — Provisioning ledger
For every external service in the stack, ask three things: do you have an account? → do you have the dev credential? → should I provision it now?
Three outcomes per row: have it, provision now, defer with a placeholder and a TODO.
Dev is automatic, production is manual. This split is deliberate:
- Dev / preview — provision freely where a CLI exists (
neonctl projects create,wrangler r2 bucket create,supabase projects create,fly secrets set, …). Write values to a git-ignored.env.local. - Production — generate the exact commands and have the user run them via
the
!prefix, so production secrets never enter the transcript. If a value is read into a tool call it lands in conversation history and possibly in logs.
Four rules for this phase:
- Never write a production secret to the repo, and never echo one into the
transcript.
.env.examplegets key names and descriptions only (§15). - Verify, don't assume. After each credential, smoke-test it: a trivial query, a cache ping, an upload-then-delete, a send to a sink. A wrong-but- present credential fails at 2am instead of now.
- Three tiers, so it is a matrix (
§15). Dev, staging, production, with separate credentials. A single list invites sharing one. - Resumable. Write the ledger to the Stack Profile (
docs/architecture/stack-profile.md, else the olddocs/specs/stack-profile.md) item 6 with statuses only, never values, so a later run picks up the deferred rows.
Generate .env.example, the config-validation schema (§15's "one schema,
validated at startup") and the CI secret list from the ledger, so the three
cannot drift.
Phase 7 — Generate
Build in dependency order. Each step is a precondition of the next: Print one line for each step as it runs.
| Step | Owner |
|---|---|
1. Repository skeleton, workspace, CI, hooks, secret scanning, branch gate and delivery scripts — copy the hooks, linters and guards from assets/tooling/shared/ and assets/tooling/<track>/ (see below); the CI and delivery files come from luismc-delivery's installer (see below) |
this skill and luismc-delivery |
2. core, then the data module with initial schema and migration runner |
this skill |
3. contract — the OpenAPI document and generators |
this skill |
| 4. (auth) | auth-setup — skip entirely |
5. apps/api — skeleton, health route, error envelope, route-coverage test harness, non-auth routes only |
split |
6. jobs + apps/worker with §7.4's required job set |
this skill |
7. Adapters: mail, storage, notify, observability |
this skill |
8. tokens and the UI module |
this skill |
9. apps/web, apps/admin |
this skill |
10. apps/ios, apps/android — whenever wanted |
this skill |
| 11. Stack Profile, runbooks, conformance run | this skill |
Step 1 in detail — what is copied where
Paths and owners are fixed by CONTRACT.md in luismc-delivery (templates/shared/scripts/delivery/CONTRACT.md).
This skill carries no CI logic. It picks technologies and copies the hooks, linters and guards below. At the end of step 1 it calls the delivery installer, which writes the delivery scripts, the workflow files and the per-track gate.json and smoke.json:
python3 <luismc-delivery>/scripts/delivery_ci_install.py install --track <track> --root .
Do it in the first commit, so the first branch is already gated:
| Into the repository | From assets/tooling/ |
Clause |
|---|---|---|
scripts/delivery/*, .github/workflows/*, scripts/delivery/gate.json, smoke.json |
the delivery installer (luismc-delivery, templates/). Afterwards set default_branch in gate.json to the repo's own and ui_paths to the apps that exist. CONTRACT.md is not copied. |
§13.5, §13.6, §13.1, §11.2 |
e2e/quarantine.json (TypeScript track only) |
ts/e2e/quarantine.json |
§11.2 |
.githooks/pre-push |
shared/.githooks/pre-push, beside pre-commit |
§13.5 |
.githooks/commit-msg |
shared/.githooks/commit-msg, beside pre-commit; does nothing unless .delivery/config.json sets no_attribution: true (DL-74). pre-commit also refuses staged conflict markers (DL-24) |
DELIVERYSPEC |
.gitattributes |
shared/.gitattributes — LF for shell scripts, hooks and the delivery scripts, so a CRLF checkout cannot break them |
§13.5 |
| the guard-test starter, TypeScript | ts/guards/*.guard.test.ts and ts/vitest.config.ts (its guard project) |
§13.5 item 4 |
| the guard-test starter, Go | go/guards/ (TestGuard*) |
§13.5 item 4 |
| the guard-test starter, Rust | rust/crates/guards/ — a member crate (guard_* tests); add crates/guards to the workspace members in the root Cargo.toml |
§13.5 item 4 |
| the guard-test starter, .NET | dotnet/tests/Guards/ (Category=Guard); add Guards.csproj to the solution and match its TargetFramework to global.json |
§13.5 item 4 |
a .gitignore line .delivery/ |
no asset: append it to the generated .gitignore |
§13.5 |
Turn on the repository setting that deletes a merged pull request's head
branch, so the platform does its part of the §13.6 cleanup:
gh api -X PATCH repos/{owner}/{repo} -F delete_branch_on_merge=true. It is a
repository setting the user owns: ask before changing it on an existing
repository.
If the product adds a workflow that uploads a build artifact (an installer such
as an MSI, a bundle, a report), give each kind a stable name, with no version,
run number or commit in it. Add the workflow to artifact-prune.yml's
workflows: list (§13.1).
CI option, per OS (DL15). Ask the owner where CI runs, once for each OS the product tests on (linux, windows, macos). There is no default and no assumed answer. The choices are (a) GitHub self-hosted runners, (b) GitHub-hosted runners, (c) Claude cloud sessions. Record each answer with python3 <luismc-delivery>/scripts/delivery_ci.py set --os <os> --option a|b|c, then run delivery_doctor.py and fix what its ci check reports. The owner changes the choice later with the same command. Everything about the options (shard table, runners, cloud sessions, statuses) is in luismc-delivery.
Runners (§13.1). For option (a) or (b): ask whether CI runs on hosted or self-hosted runners.
Every workflow template reads its runner labels from the repository variable
CI_RUNS_ON (a JSON string or list; unset means ubuntu-latest). For
self-hosted or shared runners, set it and turn on one-at-a-time ready-PR CI
(§13.6): gh variable set CI_RUNS_ON --body '["self-hosted","linux"]' and
gh variable set CI_SERIAL --body true. These are repository settings the user
owns: ask before changing them on an existing repository.
Warn against requiring a label that only one runner carries (gh api repos/{owner}/{repo}/actions/runners lists them when the user has admin rights; else ask):
if that runner is offline, every job waits. Suggest a second runner with the label, or a
fallback job on hosted labels. Warn; never block. join.py and ci_watch.py warn at
run time when no online runner carries a job's labels (§13.6).
Splitting the dominant CI job (§13.1, SHOULD). The ts template already runs the
browser suite as two Playwright shards behind the unchanged web E2E (§11.2) check (one
shard: list sets the count). For any track, once CI has run, read the slowest-job summary
that ci_watch.py prints. If one job or test step takes most of the run, give it its own job
or shards. Leave small steps together, because every job repeats setup.
End-to-end selection (§13.5). When the product has more than a handful of
end-to-end specs, add an e2e_map to gate.json (specs to source paths, plus
full_on for shared configuration and the API contract), so a branch gate runs
only the affected specs. Without a map the gate runs them all.
Change notes (§13.6). Create docs/specs/changes/ with a short README.
Branches add a note per spec change instead of editing a spec's version line
or change log. scripts/delivery/change_notes.py fold applies them on the
release branch. The change-notes gate step enforces it. If the product's
specs number their change-log headings, set changelog_headings in
gate.json.
Install the pre-push hook so a project's existing check survives:
- Find the existing pre-push hook: in
.githooks/, in the directorycore.hooksPathpoints to, or.git/hooks/pre-pushwhencore.hooksPathis unset. - If it differs from the template, move it to
pre-push.localin that hooks directory before copying the template; never overwrite it. - The template runs
pre-push.localafter its own guards pass, with the same arguments and stdin, and fails the push if it fails. - If
core.hooksPathalready points at another directory (for example.husky), install both hooks into that directory; do not changecore.hooksPath. - If
core.hooksPathis unset, copy into.githooks/and set it:git config core.hooksPath .githooks; say so in the repo's setup notes.
Mark
agent_setup.sh and every hook (pre-commit, pre-push, commit-msg) executable. Run
chmod +x on each file first (git ignores a hook whose file is not executable, and
that includes the secret scan in pre-commit), then git update-index --chmod=+x
once they are staged. Otherwise a checkout on another machine cannot run them. Copy .gitattributes before the
first commit, so those files are never stored with CRLF. If the repo already has
one, append the four lines. Do not replace it.
scripts/delivery/agent_setup.sh is the first step for any cloud agent. It
installs Python 3.11+ if missing, the scanners pinned in tools.json (the version
CI installs, so the gate runs the scanner CI runs) and gate.json's setup
commands. It is idempotent. Put one line in the repo's agent instructions:
run it before the first gate.
Do not write the delivery scripts yourself. Do not edit them in the
generated repo to fit a product. They belong to the plugin and are versioned
with STACKSPEC. Product differences belong in gate.json.
Hosting. A change that touches only docs/ has nothing to deploy, so
configure the host to skip it, using whatever the host offers for deciding
whether to build (an ignored-build-step command, a path filter). For example, a
host that runs a command and skips the build on exit 0 can be given
git diff --quiet HEAD^ HEAD -- . ':!docs'. This is a hosting setting, not a
repository file; record it in the Stack Profile's environment inventory.
Delivery log (§13.6). The deploy job, after its production smoke test,
runs python3 scripts/delivery/delivery_log.py live --pr <N> (the merged PR
number) with contents: write, so the train's release notes record when each
change went live. A failure there warns and never fails the deploy.
Packaged programs (§13.7, a MUST). A product that ships a packaged
program (single executable, installer, app bundle, container image) declares
its start command and every worker's readiness line in
scripts/delivery/smoke.json; the track's package.yml builds the packaged
form and runs scripts/delivery/smoke_packaged.py on it. An empty start means
the product ships no packaged program.
The seam is at steps 4 and 5 — do not paper over it
- Step 4 is entirely
auth-setup's. Do not create the auth module, not even an empty one with a README. An empty auth package makesauth-setup's preflight see a requirement as met when it is not. - Step 5 splits. Create the API app, its bootstrap, its health route, its
error envelope and the
§11.1route-coverage test harness. Leave the auth endpoints out. The coverage test over zero auth routes is correct here. It starts to fail, usefully, as soon asauth-setupadds routes without declarations. - Step 7's mail adapter is a dependency of auth, not of it. Build it here;
auth-setupwires notifications into it. - Step 6's job set is
§7.4's.AUTHSPEC's pruners belong toauth-setup, which adds them to the same jobs module.
Do not create modules speculatively. §2.1: a shared module MUST NOT exist
before it has a second consumer or a clear adapter role. Generating eleven
packages for a product with one app is a defect.
Phase 8 — Verify and report
Run Mode 2 against what you generated. A generation that has not been checked is
not finished. Expect and ignore auth-shaped absences. They are the declared gap,
not findings. Also run the branch gate once (python3 scripts/delivery/branch_gate.py --no-publish) so the first result is a real one. Fix gate.json if a step
cannot run.
Report: what was generated · what was deferred and why · every profile field the user still has to fill · every open ledger row · the Mode 2 findings.
Give the report as three parts, in this order, with these headings: Done (what was generated and deferred, with paths), Left for you (every profile field and open ledger row, as numbered steps with the exact command, each checked afterwards), What you can do now (see "Setup pattern").
Phase 9 — Ask about authentication
Authentication is absent by design. Say so plainly, then ask:
The stack is set up and authentication is not part of it.
auth-setupbuildspackages/authagainstAUTHSPEC, a separate binding spec. Want to run it now, or later?
Do not start it without an answer, and do not treat an earlier "set up the whole thing" as that answer. Two binding specs in one unreviewable pass is the failure this split exists to prevent. If the user says later, say what to run and stop.
Mode 2 — Check conformance
STACKSPEC §19 is the checklist. Until a script implements it, this mode is that
check, performed by reading.
Find the declared standard. Read
docs/architecture/stack-profile.md, or the olddocs/specs/stack-profile.mdwhen the repo has not moved it (say so in the audit: moving it is a finding). Note theSTACKSPECversion it claims. If it is behind the version inreferences/, say so first. Every finding is relative to the version the product claims. The difference between versions is itself a finding.A profile claiming 5.x (5.2.0 and earlier) predates
STACKSPEC6.0.0's four newMUSTs, listed in the spec's§0.16.0.0 entry: the§11.2E2E gate, the§13.1default-branch skip, the§13.5branch gate and pre-push hook, and the§13.6integration flow. Report the product as conforming to its claimed version and list each of the four as an upgrade item. Upgrading is the repair pass in step 7 plus a profile edit: change the claimed version to 6.0.0 only after the re-check passes, never before.A profile claiming 7.x predates
STACKSPEC8.0.0's one newMUST,§13.7: a product that ships a packaged program has CI start the packaged form and exercise every worker it starts (smoke.jsonplus apackage.ymljob). List it as an upgrade item only for such a product. A product that ships none conforms to 8.0.0 as it stands. Offer the 8.0.0 improvements as suggestions:inert_pathsandlockfile_scopeingate.json, the light join check, the train freeze in pre-push, runner-health warnings, path-filtered build-artifact workflows and the delivery log in release notes.A profile claiming 7.0.x conforms to 7.1.0 as it stands (7.1.0 adds no
MUST). Offer the 7.1.0 improvements as suggestions, not findings: the current delivery scripts (the gate fetches its base, pre-push compares with the fork point, pinned scanner checksums, the default-branch skip for ignored paths), runner labels as a setting, ane2e_map, change notes, and one-at-a-time ready-PR CI on shared runners.A profile claiming 6.x predates
STACKSPEC7.0.0's newMUSTs, listed in the spec's§0.17.0.0 entry:§13.1keeps only the newest build artifact of each name, the§13.5gate installs from the lockfile in frozen mode first, stops at the first failure and has a time limit, and§13.6removes a merged train automatically. Report each as an upgrade item, the same way; the repair is copying the current delivery scripts, the two new workflows and the setting above.A profile claiming 4.x or earlier predates the technology-neutral split. Report that its technology clauses no longer exist as requirements, and that its recorded vendor deviations are now
CHOICEs needing recording, not approval. That conversion is the main finding for such a product.Check the profile's
AUTHSPECversion too, but do not audit auth here. Report only whether the product claims one.If there is no profile, that is finding number one.
§16and§18make it mandatory. Without it there is no declared engine, no RPO/RTO, no pinning decision, no capability-gap record and no deviation record. So most clauses cannot be evaluated. Report it and continue with what is checkable.Work
§19's checklist, section by section: Repository and layering · Contract · Data · Jobs and integrations · Caching · Clients · Observability · Testing and CI · Configuration and compliance. For each item resolve one of: conforms, violates, absent, not applicable (with the reason). Cite file and line for every violation. A finding without a location is not actionable.Check the structural gates specifically. These fail silently when absent:
- layer-boundary check (
§3.2) and the adapter rule (§3.3) — every vendor SDK confined to one module - route-coverage test exists and runs (
§11.1) — authorization and rate-limit declarations. Whether the auth routes declare authorization isauth-setup's finding, not this skill's - contract drift: all generated clients regenerated and committed (
§4.1) - a test suite finding zero tests fails rather than passes (
§11.3) - secret scan covers full history (
§12) - CI cost (
SHOULD,§13.1's CI-cost clause) — drift findings atSHOULDseverity, so each needs a recorded reason or it is a violation (step 5): a separate secret-scan job where a step in another job would do; the gitleaks step withoutpull-requests: read(it 403s on a PR before scanning);cancel-in-progress: truethat also applies to the default branch; no draft-PR skip; no task-runner result cache - field classification, retention declaration, export inclusion (
§5.7) - data-collection inventory vs. generated artifacts (
§9.7) - E2E gate (
§11.2) — the web E2E job runs on ready pull requests and is not advisory (nocontinue-on-error, and it is a required check); a committed quarantine file exists (e2e/quarantine.jsonon the TS track), each entry with an owner, a date and a reason; quarantined specs run in a separate non-blocking step; nothing retries a spec until green - Default-branch skip (
§13.1) — adecidejob runs first on a push to the default branch, every other job needs it and runs unless it says skip,ci_skip_check.pyis present, and a skipped or faileddecidestill runs the suite. Default-branch runs are never cancelled - Branch gate (
§13.5) —scripts/delivery/holds the gate scripts;gate.jsonparses and its steps cover typecheck, lint, unit, guards (scopealways), secret scan and E2E (scopeui-changed), withwiden_onnaming the lockfile, the task-runner config and the CI workflows; a guard-test suite exists and is selected by the track's convention;.githooks/pre-pushexists in the hooks directorycore.hooksPathpoints at (.githooksunless the project already used another), and any earlier pre-push hook it replaced is kept aspre-push.localbeside it;agent_setup.shinstalls the scanner at the version intools.json, and CI installs that same version;.delivery/is git-ignored;.gitattributesforces LF on*.sh,.githooks/*andscripts/delivery/*.py, so the hooks run on every OS (a CRLF shebang line fails on Linux and macOS). The guard starter is registered: the Rustcrates/guardsis in the workspacemembers, the .NETtests/Guardsproject is in the solution.gate.json'ssetupinstalls in frozen or locked mode, and itstimeout_minutes, when set, is a positive integer. A branch result that is not tied to the current head and base is not a result - Build artifacts (
§13.1) —artifact-prune.ymlis present and itsworkflows:list names every workflow that uploads an artifact; everyupload-artifactname is stable for its kind (no version, run number or commit in it) - Train cleanup (
§13.6) —train-cleanup.ymlandscripts/delivery/cleanup.pyare present; the repository settingdelete_branch_on_mergeis on (gh api repos/{owner}/{repo} --jq .delete_branch_on_merge) - Integration flow (
§13.6) — advisory, because it is process: read the repo's rules and agent instructions for task-ID branches, draft pull requests into an integration branch, the merge script, one ready pull request, and CI watched by commit with no auto-merge. Report documented or not documented; do not infer a violation from the absence of a file
- layer-boundary check (
Classify divergence correctly. This is where Mode 2 most often produces false positives, and 5.x changed the rules:
Kind Test Verdict MUSTviolatedNo waiver in the profile violation MUSTwaivedReason + named approver recorded conforming SHOULDnot followedReason + named approver recorded conforming SHOULDnot followedNot recorded violation CHOICERecorded in profile item 2 conforming — always CHOICENot recorded unrecorded, not a violation Capability gap Compensating control in item 14 conforming Capability gap No control recorded violation A vendor is never a violation. Postgres vs. MySQL, Sentry vs. Bugsnag, Neon vs. RDS, Drizzle vs. Prisma, Vercel vs. Fly — all
CHOICEs. The only question is whether the profile records them. Do not import 4.x's gradedSHOULDs for Neon, Upstash or Vercel Blob; they no longer exist.The tools are also never a violation.
§13.1requires twelve gates to run and fail the build. It names no tool. A repo on Biome instead of ESLint, orgo testinstead of Vitest, conforms perfectly.Report severity-ranked findings, grouped by section. Edit nothing while surveying. A check that repairs as it goes cannot show you what the repo looked like.
Then offer to fix the mechanical findings — opt-in, never automatic. Some violations are decisions (a missing RPO, a layering breach needing code moved). Others are absent files this plugin carries. Offer the second kind, listed explicitly, as one confirmation:
.gitleaks.toml, pre-commit hook missing →§12secret scanning- CI workflow missing or short of
§13.1's twelve gates, or drifting from its CI-cost clause (theSHOULDfindings from step 4) - boundary-check config missing →
§3.2has no enforcement, so the layer rules decay by default - lint, format, test and task-runner configs missing
scripts/delivery/,gate.json,tools.jsonor.githooks/pre-pushmissing →§13.5; the.delivery/line missing from.gitignore;.gitattributesmissing or not forcing LF on the hooks and scriptsci.ymlwithout thedecidejob or its job conditions →§13.1; E2E advisory ore2e/quarantine.jsonmissing →§11.2artifact-prune.ymlmissing, or an upload with a per-run name →§13.1;train-cleanup.ymlorcleanup.pymissing →§13.6
Copy the hooks and guards from
assets/tooling/, and the delivery assets withdelivery_ci_install.py update(paths perCONTRACT.mdinluismc-delivery), keep the repo's owndefault_branchingate.json, and leave the§13.6documentation to the user: offer to draft the text for the repo's rules file, as a diff.Rules for the repair pass:
- Diff before replacing. If the file exists but differs, show the diff and
ask. An existing CI workflow may carry project-specific jobs that a blind
overwrite destroys. Generation and repair MUST NOT touch those jobs, only
the gates and CI-cost settings the template owns. An existing
.gitleaks.tomlmay carry allowlist entries added for real false positives. - Never touch a conforming file, even if it differs from the template. The template is one way to satisfy a clause, not the only one.
- Re-run the check afterwards. A repair that was not re-verified is a claim, not a fix.
- If the user declines, leave everything untouched and say which findings remain open.
The catalog
assets/catalog/ is the compatibility model. It is data, not requirements.
Nothing in it is graded.
| File | Holds |
|---|---|
roles.yaml |
The slots, which are asked vs. derived vs. fixed, the Phase 1 profile questions, the two entry orders, the pushback rules |
tech/languages.yaml |
The four tracks, their per-track derived foundation, hosting scores |
tech/frameworks.yaml |
Web and API frameworks by track; the rich vs. server-rendered distinction |
tech/hosting.yaml |
Compute platforms, language scores, VPC and cold-start implications, spec exclusions |
tech/data.yaml |
Engines, hosts, the capability matrix, and the queue derivation |
tech/services.yaml |
Cache, object storage, mail, error tracking, uptime |
bundles.yaml |
Pre-pinned coherent stacks, plus the anti-combinations to block or warn on |
Two habits when using it:
say_out_loudblocks are not optional. They exist because the consequence is invisible at selection time and expensive later. Read them to the user.- Run
check_stack.py --catalogafter editing any of it. It verifies that the files agree with each other: framework tracks name real languages, hosts name real engines, every engine answers every declared capability, and bundle selections reference technologies that exist.
Keeping the catalog honest
The catalog carries claims about the world: Workers have no raw TCP, D1 lacks interactive transactions, PlanetScale discouraged foreign keys. These go stale. When a recommendation depends on a claim and you are not current on it, say so. Do not assert it. Verify it before blocking a user's choice on it.
The tooling assets
assets/tooling/<track>/ holds working implementations of §13.1's twelve
gates, one set per track. Copy in Phase 7 step 1, before any module exists, so
the first commit is already gated. luismc-delivery's templates/shared/scripts/delivery/
holds the track-independent delivery scripts (branch gate, verifier, pre-push
guards, merge script, CI watcher, default-branch skip check, agent setup), and
its CONTRACT.md is the one place their paths, exit codes and file formats are
defined.
Three properties every track's templates must preserve. They exist so that an absent check cannot pass for a passing one:
- A suite that finds no tests fails (
§11.3). A mis-globbed suite must not report green over zero tests. - The codegen drift check regenerates then diffs, so a stale committed artifact cannot satisfy it.
- The secret scan checks out full history. At the default clone depth
gitleaks silently examines one commit and reports clean. It is a step in the
verifyjob, not a job of its own: a few-second job bills a full minute.
The tool names are this generator's choice, not the spec's
STACKSPEC §13.1 requires the twelve gates to run and fail the build. It names
no tool, and it must not start to. §20 makes a new MUST a major version, and
defines naming a vendor anywhere in the spec as a defect.
So record the tools in docs/architecture/stack-profile.md, not here. In Mode 2 that
distinction is the difference between a finding and a false positive.
Editing STACKSPEC itself
Not part of using this skill. The spec is maintained in the dev-standards
repository, which gates every edit with tools/spec_check.py and documents the
process in its README. A product never edits its copy, because it never has one.
Two governance rules worth knowing before proposing a change (§20):
- A new
MUSTis a major version. - Promoting a
CHOICEto aSHOULDorMUSTis a major version, and naming a vendor anywhere in the spec is a defect, not an edit. Technology belongs in the catalog.