npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

backend-skeleton

v1.9.0

Published

Deterministic gate layer for AI-assisted backend changes -- blocks brownfield collisions and contract/handle drift via disk-hash checks before code ships. Scans Spring, Rails, FastAPI, and Express; scaffolding codegen included for selected stacks.

Readme

backend-skeleton

npm version npm license node GitHub release

bskel is a deterministic gate layer for AI-assisted (and human) backend work: before a change is allowed to count as done, bskel checks it against disk — not against what an agent or a person claims. Is this branch actually based on the real default branch? Does this "new" module collide with one that already exists elsewhere in the codebase? Does the emitted contract still match the source it claims to describe? Did a hand-finished file just get silently overwritten? Every one of those is a gate backed by a content-hash on disk, not a prompt instruction a future session could ignore, forget, or talk itself past.

Scaffolding codegen — Java/Spring Boot, Python/FastAPI, and TypeScript/JavaScript Express repos, feature_id-scoped machine-readable contracts, UUID-addressable field handles, and stack-choice (e.g. ngrok) wiring — rides on top of that same gate machinery. It's useful on its own, but the reason the gates exist first is what makes the codegen safe to trust in a brownfield repo, instead of just another thing to double-check by hand.

bskel exists because a previous ad-hoc agent-driven scaffolding attempt branched a worktree 658 commits behind the real default branch and never noticed. Every gate in this tool is a regression check for a specific failure mode found the same way — see DECISIONS.md for the full record.

bskel run against a real fixture repo: preflight, a brownfield collision scan, and a feature status gate table

A real terminal, a real bskel binary, a real fixture repo — not a scripted transcript. Source: docs/demo.tape, regenerated with docs/record-demo.sh.

Contents

Status: 1.0.0

As of 1.0.0, this project makes an explicit API-stability promise: bskel's CLI surface -- command/flag names and their meaning, every --json output shape (all schemas under schemas/), gate names and pass/fail semantics, and exit codes -- is stable. A change that breaks any of those requires a major version bump, never a patch or minor release; a new command, a new optional flag, or a new additive field on an existing JSON shape is always minor-version-safe (every schema under schemas/ is additionalProperties: false specifically so a genuinely new field shows up as a real, visible schema change rather than something an existing consumer could silently miss). See D-stable-api-contract in DECISIONS.md for the full policy and what's explicitly excluded from it.

That promise is about the interface, not a claim that every subsystem has been proven in production -- which still splits the same way it always has:

  • scan, contract (including export), and new are the most exercised paths — real, measured verification against a real production Spring Boot repo (see DECISIONS.md), plus a synthetic fixture corpus for every adapter, run in CI on every change.
  • handles (the UUID-addressable resolver/codec/router codegen) is functionally complete and tested the same way, but has never been deployed to a real production repo. Two gaps named in earlier betas have since been partially closed: O3 (opt-in --enforce-registry, checked fetch/patch/recover against HandleRegistry, revocation-aware) and O5 (fetch/patch now derive independently correct roles instead of silently sharing one) are both implemented -- see DECISIONS.md's D-handle-registry-enforcement/D-resolver-authorization-action-aware. Real gaps remain and are explicitly still open, not closed: registry enforcement is opt-in, off by default; authorization inference now recognizes both @PreAuthorize(hasRole(...)) and hasAuthority(...) (see DECISIONS.md's D-resolver-authorization-action-aware) via an explicit per-action policy contract (D-resolver-policy-contract) -- a companion annotation this scanner cannot safely evaluate (@PostAuthorize, @Secured, @RolesAllowed, ownership/tenant checks, hasAnyRole/hasAnyAuthority) now refuses auto-materialization and generates an AuthorizationPolicy interface that blocks the target app from starting until a human implements it -- ships default-on, handles emit exits non-zero (23) until acknowledged with --force --reason or the policy is implemented. hasAnyRole/hasAnyAuthority list-shapes remain unaddressed (deliberately, see D-resolver-policy-contract's own EXIT). All three providers (Java/Python/TypeScript) now generate a real sbf_handle/ sbf_handle_snapshot schema and support --enforce-registry/recover() -- TypeScript's own registration mechanism is a higher-order wrapper function, not a decorator (no Java-AOP or Python-decorator equivalent exists in this ecosystem the templates could safely rely on; see D-typescript-express-registry-parity in DECISIONS.md). Treat handles emit's output as a scaffold to finish by hand, not a production-ready subsystem, until a real deployment happens.

Quickstart

Try it in 10 seconds

npm install -g backend-skeleton   # or: npx backend-skeleton <command>
cd <any-existing-repo>            # must be a git repository -- that's the only requirement
bskel scan                        # zero flags: every module/controller/entity/enum this repo's
                                   #   adapter can see, unscored -- no preflight, no feature, no
                                   #   files written, no gate touched

Rails projects are scanned statically by default. On a trusted Rails checkout, add --runtime-routes to boot the application and use bin/rails routes --expanded as the route source; initializers and application boot code will run.

That's a read-only look, not the gated workflow — for real feature work (collision-checked against a specific idea, contract-gated, codegen), see below.

The gated workflow

npm install -g backend-skeleton   # or: npx backend-skeleton <command>
cd <target-repo>                  # must be a git repository

bskel doctor                      # what's on PATH, which scanner adapter detects this repo, and why
bskel preflight                   # confirms HEAD is actually based on the real default branch,
                                   #   not a stale/abandoned one -- required before anything else

bskel feature init --slug organization-management
bskel scan --feature 001-organization-management --terms organization
                                   # brownfield-collision scan; refuses to proceed silently if this
                                   #   module already exists elsewhere in the codebase
bskel scan disposition --feature 001-organization-management --mode reuse --note "..."
                                   # required once scan finds a collision/adjacent match

bskel contract emit --feature 001-organization-management
                                   # feature_id-scoped JSON Schema contract, from real source annotations
bskel scan cross-feature-check --feature 001-organization-management
                                   # refuses to proceed if this feature's resourceType/table/operationId
                                   #   collides with another feature -- required before handles emit
bskel handles plan --feature 001-organization-management
bskel handles emit --feature 001-organization-management
                                   # UUID-addressable field handles + generated resolver code

bskel verify --feature 001-organization-management --build
                                   # aggregates every gate's current status; --build also runs the
                                   #   target repo's own build wrapper (gradlew/mvnw/npm), if present

Pull-request gate checks

bskel ci check is the read-only CI counterpart to verify: it computes the merge-base with a PR base ref, selects changed active features, and runs the same gate/artifact/build calculation and bskel next remediation. It never refreshes preflight, passes a gate, or runs remediation.

bskel ci check --base origin/main --build \
  --summary-file "$GITHUB_STEP_SUMMARY" \
  --sarif-file bskel.sarif

--feature 001-a,002-b overrides automatic selection. Without it, changes confined to one or more specs/<feature-id>/ directories select those active features; a non-document change outside feature specs checks all active features; documentation-only diffs are an explicit successful no-op. --json writes exactly one report document even when checks fail. SARIF generation only creates a file; uploading it is your workflow's policy.

The repository root is also a composite action:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0 # ci check needs the PR base and merge-base locally
- uses: popixoxipop-collab/backend-skeleton@main
  id: bskel
  with:
    build: 'true'

# Optional: the action output is only a local file. Uploading requires this explicit policy.
- uses: github/codeql-action/upload-sarif@v3
  if: always() && steps.bskel.outputs.sarif-file != ''
  with:
    sarif_file: ${{ steps.bskel.outputs.sarif-file }}

Use this action only on GitHub-hosted or otherwise trusted runners for pull requests; it executes the checked-out repository's configured build command when build: 'true' is selected.

bskel status/bskel next are what you actually run over and over — real output, captured against a fixture repo partway through the flow above, not written by hand:

$ bskel status --feature 001-organization-management
# Status: 001-organization-management

## Gates
- [PASS] preflight
- [PASS] scan
- [(not_run)] cross_feature (required-when-present, feature-scoped)
- [BLOCKING] contract
- [(not_run)] dependencies (required-when-present, feature-scoped)
- [(not_run)] handles (required-when-present, feature-scoped)
- [(not_run)] stack (required-when-present, repo-scoped)
- [(not_run)] patch_transactions (required-when-present, feature-scoped)
- [(not_run)] conformance (required-when-present, feature-scoped)

## Artifacts
- [OK] contract: specs/001-organization-management/contracts/001-organization-management.schema.json

## Next
- bskel contract waive --feature 001-organization-management --code <CODE> (--subject "..."|--all) --reason "..."   # or: bskel gate force contract --feature 001-organization-management --reason "..." if intentional   # contract gate is awaiting disposition

## Optional, not yet run: handles, stack

Every gate line is a real, disk-verified check — the Next line is always the exact command to unblock whatever's currently BLOCKING, so there's no separate doc to cross-reference mid-workflow.

Starting from nothing (greenfield)

Every command above assumes an existing Spring Boot or FastAPI repo. If you don't have one yet:

bskel new --stack spring --slug my-service    # calls start.spring.io (network required), or:
bskel new --stack fastapi --slug my-service   # a local starter template, no network call

cd my-service
# create a remote you own and push to it (e.g. `gh repo create --private --source=. --push`),
# then: git remote set-head origin --auto
bskel preflight                               # now resolvable -- picks up from the Quickstart above

bskel new deliberately never creates a remote itself and never auto-chains into preflight -- preflight requires a real origin remote with a resolvable default branch, which a brand-new local-only repo doesn't have yet. See D-greenfield-bootstrap in DECISIONS.md.

Both stacks accept --name, --description and --project-version (the generated project's own version -- --version is a global flag that prints bskel's). Beyond that the parameters differ, because the two ecosystems do:

bskel new --stack spring --slug my-service \
  --group-id com.acme --artifact-id billing --package-name com.acme.billing \
  --java-version 21 --packaging war --add-dependencies actuator,postgresql

bskel new --stack fastapi --slug my-service \
  --python-version 3.12 --port 9000 --license MIT --database postgres
  • --add-dependencies extends the baseline (web, data-jpa, security, validation, lombok). --dependencies REPLACES it -- and if the result drops web, data-jpa or validation, you get a specific stderr warning naming what stops working downstream, then it scaffolds anyway.
  • --group-id/--package-name/--artifact-id are validated locally against the Java package grammar. That isn't belt-and-braces: start.spring.io accepts groupId=com.new (a reserved word) and groupId=has space with HTTP 200 and hands back a project that cannot compile.
  • --java-version is checked against start.spring.io's own live metadata, fetched on demand only when you pass a non-default value, never cached to disk -- for the same reason: javaVersion=99 returns HTTP 200 and writes JavaLanguageVersion.of(99) straight into build.gradle.
  • --database pins a driver and nothing else -- no engine, session or connection code is generated, because that would be bskel inventing your domain.
  • --type, --language and --boot-version are deliberately refused with a specific reason each (Maven/Kotlin scaffolds break this tool's own scanner and codegen assumptions; a bad bootVersion gets an unusable HTTP 500). Run bskel new --stack spring --type maven-project to see the actual explanation.

The full parameter list, the measured API-validation matrix behind that split, and the warning behaviour are in D-greenfield-parameters in DECISIONS.md.

Remembering your own conventions across projects (optional)

If you start several projects with the same conventions, bskel new can record them into a database you own -- never bskel's own state, never a shared store:

export MY_PATTERNS=postgres://localhost/my_patterns    # once: run patterns/schema.sql against it

bskel new --stack spring --slug billing \
  --java-version 21 --group-id com.acme --dependencies web,data-jpa,validation,flyway \
  --record-pattern --pattern-database-url-env MY_PATTERNS

bskel pattern suggest --stack spring --pattern-database-url-env MY_PATTERNS

pattern suggest prints what you've recorded, with per-value frequency, and a ready-to-paste command line at the bottom -- it never runs bskel new for you and bskel new has no flag that would accept a suggestion as a default. Every value in a generated project is still one you typed in that invocation. Omitting --record-pattern/--pattern-database-url-env leaves bskel new exactly as it is today. See D-pattern-accrual in DECISIONS.md.

Publishing a feature's contract as OpenAPI (optional)

bskel contract export --feature 001-organization-management --out openapi/organization.json

Renders an already-emitted, gate-passing contract as a standalone OpenAPI 3.1 document — the inverse of contract emit --openapi-file. Useful for a Swagger UI page scoped to one feature instead of the whole repo, a client generator that can't follow $ref (an exported document has none), or a mock server for one feature's operations.

It is a deliberately lossy, narrow projection, and it says so — every omission is disclosed both in prose (info.description) and machine-readably (info.x-bskel-omitted). Nothing is invented to fill a gap: an operation whose body shape the contract doesn't know gets a JSON media-type entry with no schema rather than a fabricated one, and an operation with no per-status source data still collapses its 2xx/4xx/5xx bodies into two unions rather than guessing a status code.

Query/header/cookie parameters, security (plus the referenced security schemes), summary, tags, per-status responses, and non-JSON request media types (e.g. multipart/form-data) are emitted — but only when a real --openapi-file source document said something for that exact operation, copied byte-for-byte, never reconstructed. security: [] is emitted when the source document itself said [] (a genuine claim that no authentication is required); it is never invented as a default. Where no source document was given, or it said nothing for an operation (or a particular field of one), the key is simply omitted, meaning "unspecified." Operation-level description is copied too, but opt-in only (contract emit --descriptions) — measured too expensive to copy by default (real average 2,442.7 bytes/operation, larger than every other field this projection copies combined). The same flag also copies a schema FIELD's own description/ example (a property's own annotation, not the operation's) one level deeper into request-body/ response/error/parameter/per-status/path-param schemas — title, plural examples, externalDocs, xml, and deprecated stay unconditionally dropped either way (0 real occurrences measured against the Team-IZ-Backend oracle).

Export refuses a zero-operation contract, refuses when the scan found a global path prefix the contract's paths don't reflect (--allow-unprefixed overrides), and stamps every document with an x-bskel-generated marker that contract emit --openapi-file then refuses to read back in — reconciling a contract against its own export would make it confirm itself. See D-openapi-export in DECISIONS.md.

A CSV table of a feature's contract (optional)

bskel contract export-csv --feature 001-organization-management --out organization.csv

One row per operation, opened by someone who will never read a JSON Schema — a PM reviewing scope, a lead deciding whether a partial contract is good enough to waive. Fourteen columns, always, in the same order: operation_id, verb, path, path_params, path_params_unverified, body, request_body_required, request_body_fields, response_fields, error_fields, provenance, summary, tags, security.

Every column is always present, even when every row leaves it blank — a scan-only contract (no --openapi-file) never states summary/tags/security, and the blank columns are that finding, not something to hide: export-csv prints exactly which columns are empty for every operation, and why, on stderr. Dropping an empty column would make the file's own shape depend on its content, so it never does.

Unlike contract export, this command is deliberately UNGATED — it works even when the contract gate hasn't passed yet, because its single most valuable moment is reviewing a partial contract to decide whether to waive it. An unreflected global path-prefix signal (the thing contract export hard-refuses on) downgrades to a stderr warning here instead. --bom prepends a UTF-8 byte-order mark for Excel-on-Windows, which otherwise mangles non-ASCII text; omit it for pandas/csv.DictReader/diff, which don't want one. See D-contract-csv in DECISIONS.md.

Database schema (optional)

bskel scan --db additionally scans Flyway/Liquibase migration files (local only, no network). Add --database-url-env <NAME> (naming an environment variable you've already exported, never read from .env) for live, read-only Postgres introspection (information_schema/pg_catalog, inside a BEGIN TRANSACTION READ ONLY) and a source-vs-live drift report. Both are informational additions to the scan report — neither blocks any gate. See D-db-schema-plane in DECISIONS.md.

The same --db/--database-url-env flags, passed to bskel scan cross-feature-check, add a 4th collision signal: a real live (or migration-file-derived) Postgres foreign-key edge whose two tables are declared by two different features surfaces as a db_foreign_key finding, direction- and confidence-scored the same way the existing NAME-identity signals are. Every fk_check in the report also carries generated_at — when the underlying data was actually captured, so a persisted/migrations-mode correlation (reused from an earlier scan, not a fresh connection) can be judged for staleness rather than trusted blindly. See D-cross-feature-fk-inference in DECISIONS.md.

An ERD of your database schema (optional)

bskel db erd --database-url-env BSKEL_DB_URL --schema public --out schema.mmd

A Mermaid erDiagram of the database — paste it straight into a GitHub/GitLab/Notion/Obsidian markdown file (fence it in ```mermaid) or mermaid.live and it renders with no extra tooling. Works two ways:

  • With --database-url-env: a real, live Postgres introspection — full column types, nullability, and primary/foreign keys.
  • Without it: falls back to scanning Flyway/Liquibase .sql migration files (no network, no credentials needed) — a real but degraded diagram, clearly marked as such in the file itself: every column types as unknown, no PK badge appears anywhere, and a header block spells out exactly what's missing. Useful for evaluating the tool on a repo you don't have DB credentials for yet.

Two things this diagram deliberately does not guess:

  • Composite foreign keys. Postgres's own information_schema doesn't retain which source column pairs with which target column once a foreign key spans more than one column — the raw data is a cross product that can include pairs that were never declared. Rather than draw a wrong relationship line, a composite FK collapses to one line labeled with all its source columns joined by +, with the ambiguity spelled out in a %% comment above it. (Composite primary keys have no such problem and render fully.)
  • 1:1 vs 1:N. The child side of every relationship is drawn as "zero or more," never "exactly one" — telling those apart needs a UNIQUE constraint check this tool doesn't perform. The header says so.

Whole-schema only in this version (no --feature/--tables filtering yet) — for a very large schema, db erd prints a note above 40 entities rather than silently producing an unreadable diagram. See D-db-erd in DECISIONS.md.

Applying DDL to a live database (optional)

bskel patch propose --kind ddl-apply extends the same propose/approve/apply/rollback lifecycle config_apply uses to a second kind: hand-authored CREATE/ALTER/DROP TABLE/INDEX/SCHEMA statements, run inside a real Postgres transaction and only COMMITted once the introspected schema actually matches the declared postcondition — anything else ROLLBACKs automatically, never partially applies:

bskel patch propose --feature 001-organization-management --kind ddl-apply \
  --database-url-env BSKEL_DB_URL --sql-file migrations/add_tax_rate.sql --schema public
bskel patch approve --feature 001-organization-management --transaction <id> --reason "..."
bskel patch apply --feature 001-organization-management --transaction <id>
# a transaction that DROPs one or more tables requires retyping the sorted, comma-joined table
# name(s) as --confirm instead of the transaction id -- the same "type the resource name to
# delete" pattern GitHub uses for its own irreversible actions

Rollback of an applied ddl-apply transaction is refused outright by design — the only path back is a new, forward transaction with hand-written reverse DDL, never an automated revert. The allowlist structurally excludes anything that can't run inside a transaction block (e.g. CREATE INDEX CONCURRENTLY) and anything outside TABLE/INDEX/SCHEMA DDL. bskel serve --database-url-env <NAME> [--sign-key <path>] [--require-sign-key] exposes the same lifecycle through the browser UI's own propose/approve/apply routes; --require-sign-key refuses to start the DDL surface at all unless a signing key was also given. See D-ddl-apply in DECISIONS.md for the full design and every explicitly-deferred boundary (non-Postgres databases, connection pooling, production safety rails).

Splicing real Java source (optional, java-spring only)

bskel patch propose --kind java-source-splice extends the same lifecycle to a third kind: a closed, four-operation vocabulary for editing REAL, hand-written .java source -- replace-method-body, insert-method-body-prologue, replace-field-initializer, add-import. A member is located by its language-guaranteed identity (a type's fully-qualified name plus, for a method, its erased parameter types — the same rule javac itself uses to forbid two overloads sharing one erased signature), resolved via a real JavaParser+Symbol Solver pass and cross-checked by an independent, non-AST falsifier before anything is written. The postcondition is a real ./gradlew compileJava; a failure automatically restores the original file, never leaves a broken one in place:

cat > splice.json <<'EOF'
{
  "schema": "sbf.java-source-splice/1",
  "file": "src/main/java/com/example/demo/domain/widget/application/WidgetServiceImpl.java",
  "edits": [{
    "op": "replace-method-body",
    "locator": {
      "type_fqn": "com.example.demo.domain.widget.application.WidgetServiceImpl",
      "member_kind": "method",
      "member_name": "updateWidget",
      "erased_param_types": ["java.util.UUID", "com.example.demo.domain.widget.presentation.dto.UpdateWidgetRequest"]
    },
    "replacement": "{\n\t\treturn widgetRepository.save(findWidget(widgetId));\n\t}"
  }]
}
EOF
bskel patch propose --feature 001-widget-management --kind java-source-splice --splice-file splice.json
bskel patch approve --feature 001-widget-management --transaction <id> --reason "..."
bskel patch apply   --feature 001-widget-management --transaction <id> --confirm WidgetServiceImpl#updateWidget

Every edit outside the four ops refuses outright (no member add/remove/rename, no signature/ annotation edits, no nested/inner/anonymous/local types, no multi-top-level-type files, no apply-diff) — there is no general-purpose patch/diff applier here, only grammar-delimited, independently-verified regions. Requires the bundled AST helper (bskel doctor reports readiness under "java-source-splice prerequisites"). Python/TypeScript source splicing is not supported — neither has an equivalent AST helper or whole-project compile check in this repo. See D-java-source-splice in DECISIONS.md for the full three-mechanism node-identity design and every explicitly-deferred boundary.

Declaring field-to-field dependencies (optional)

When one feature's data actually depends on another feature's (e.g. a WidgetDto.name that's populated from OrganizationDto.taxRate), bskel dependency declare records that link and passes the dependencies gate for it. Declaring a dependency also warns the source feature next time someone re-runs bskel status/bskel next there, so a downstream consumer doesn't silently break:

bskel dependency declare --feature 001-widget-management --resource WidgetDto --field name \
  --source-feature 002-organization-management --source-resource OrganizationDto --source-field taxRate \
  --reason "widget display name mirrors the owning org's rate tier" [--memo "..."]
bskel dependency list --feature 001-widget-management --json
bskel dependency remove --feature 001-widget-management --resource WidgetDto --field name \
  --source-feature 002-organization-management --source-resource OrganizationDto --source-field taxRate \
  --reason "no longer coupled"

Cross-feature impact graph: field-level change detection with a mandatory disposition (optional)

bskel dependency declare above records that an edge EXISTS; it does not, by itself, tell the source feature when its own contract/resource shape actually changes in a way that breaks the downstream side. bskel impact check/bskel impact accept close that gap -- an impact gate, complementing dependencies, that blocks the feature that CHANGED (not just the one depending on it) until every downstream impact has an explicit compatible/migrate/waive disposition:

bskel impact accept --feature 002-organization-management            # capture the first baseline
# ... later, OrganizationDto.taxRate's backing file changes ...
bskel impact check --feature 002-organization-management --json      # names the exact change_key + downstream feature
bskel impact disposition --feature 002-organization-management \
  --change <change_key> --downstream 001-widget-management \
  --mode compatible --reason "purely additive"                       # or --mode migrate --tracked-by "ISSUE-42"
                                                                       #    or --mode waive --expires-days 14
bskel impact accept --feature 002-organization-management            # unblocked; advances the baseline
bskel impact check --all --json                                      # the recommended CI invocation -- sweeps every feature

A migrate disposition creates a real two-sided handshake: it records an INBOUND obligation on the downstream feature (001-widget-management here), whose own impact gate stays blocked until it runs bskel impact ack --feature 001-widget-management --from 002-organization-management --change <change_key> --reason "...". Every disposition key embeds a hash of the NEW change, so a disposition recorded for one shape never silently covers a later, different change to the same field -- there is no wildcard. Only proven (exact-identity) impacts block; heuristic ones (a guessed table name, a lower-confidence collision) are always reported, never blocking on their own. Not a handles emit prerequisite (gated only at bskel verify) -- this repo's own cross_feature-gate CI incident is why. See D-cross-feature-impact-graph in DECISIONS.md.

bskel impact export --format graphify|json|mermaid [--out <path>] [--focus <node-id>] [--rings N] is the one LLM-free seam to an exploration layer: --format graphify writes the locally-installed graphify skill's own native extraction file shape directly (no install/detect/extract steps, no LLM call, no network) -- a consumer runs graphify.build.build_from_json() + cluster() + to_obsidian() on it to get a browsable Obsidian vault of the whole cross-feature graph. --focus/--rings write a Focus+Context data projection (ring/detail, inspired by Lamping/ Rao/Pirolli's 1995 hyperbolic-tree technique) into the export only -- never read back by any gate.

bskel serve [--port N] [--host <addr>] starts a small local HTTP server (loopback-only by default, matching this project's "safe default, explicit override" convention) that serves a read-only browser UI at / for the whole repo's dependency graph, backed by GET /api/graph. The UI's own POST/DELETE calls to /api/features/:id/dependencies go through the exact same declareDependency/removeDependency functions the CLI above uses — nothing is duplicated between the two — and, like every other mutating command in this project, both take a JSON body, not query parameters. GET/HEAD responses carry Access-Control-Allow-Origin: *; the mutating routes never do, so only same-origin requests (the bundled UI itself) can write.

bskel serve's dependency-graph UI, showing a real declared field-to-field dependency resolved as synced

The page's own header describes it honestly: "Not a redesign of the original Fieldwire mockup -- this exists to prove the API actually works, nothing more." It's a minimal read-only check page, not a polished dashboard — every table on it comes straight from GET /api/graph.

Patching a config file (optional)

bskel stack apply's config_check sometimes reports needs-manual-patch — a target file exists but isn't wired up the way the chosen stack (e.g. ngrok) needs. bskel patch propose/approve/ apply/rollback closes that gap for the catalog entries that declare a machine-applicable fix, using a comment-preserving edit with a content-addressed preimage check (refuses to apply if the target changed since you approved it) and a real rollback:

bskel patch propose --feature 001-organization-management --choice ngrok \
  --target src/main/resources/application.yaml
bskel patch approve --feature 001-organization-management --transaction <id> --reason "..."
bskel patch apply --feature 001-organization-management --transaction <id>
# ...or, to undo: bskel patch rollback --feature 001-organization-management --transaction <id> --reason "..."

bskel patch list --feature <id> shows every transaction and its status. See D-patch-transactions in DECISIONS.md.

Signed gate attestations (optional)

bskel gate export already produces a CI-independent report of every gate's current status, including a live verdict RECOMPUTED at export time (so a gate whose stored record still says pass but whose inputs have since changed is honestly reported as stale, never silently signed as passing), the tool's own version, git tree identity, unconditional artifact hashes, and a forced/revoked/waiver roll-up. Add --sign --key <privateKeyPath> to detached-sign it (Ed25519, via Node's own crypto module — no new dependency), then verify it offline, on any machine, without network access or trusting whatever produced it:

bskel attest keygen --out ~/.bskel-keys        # writes attest-private.pem (0600) + attest-public.pem
bskel gate export --feature 001-organization-management \
  --sign --key ~/.bskel-keys/attest-private.pem --out attestation.json
bskel attest verify --file attestation.json --pubkey ~/.bskel-keys/attest-public.pem

--sign refuses a dirty working tree (same --allow-dirty convention bskel preflight already uses) — the acknowledgement, if you pass it, is recorded inside the signed payload itself, not just a flag you happened to type. attest verify's exit code reflects signature validity only — whether the gates inside actually passed is a separate, printed summary. Three opt-in, default-off checks narrow what "valid" is allowed to mean for your use case without touching that exit-code contract: --expect-head <sha> (refuse an attestation about the wrong commit), --max-age-minutes N (refuse a stale one), --reject-dirty (refuse one signed over an acknowledged-dirty tree) — each failing exits 22, distinct from 1 (signature invalid), so "authentic but not what you asked for" is never confused with "not authentic". See D-gate-attestation-signing and D-attestation-payload-completeness in DECISIONS.md.

Signed observe receipts (optional)

bskel observe emit generates opt-in runtime middleware that checks real traffic against a feature's contract and logs a verdict-only receipt per call (JSON Pointer + constraint kind, never an observed value); bskel observe import --receipts <path> turns a stream of those receipts into a committed report backing the conformance gate. By default a receipts file is trusted at face value once it's structurally valid — a human could hand-fabricate one. Add --pubkey <path> to observe import to verify each receipt's optional signature instead, reusing the same bskel attest keygen-generated keypair signed gate attestations use:

bskel attest keygen --out ~/.bskel-keys        # same command as above -- one keypair, multiple uses
# then, per deployed app (one-time, at the app's own startup):
#   TypeScript: import { setSigningKey } from './observe/receiptSign'; setSigningKey(pem);
#   Java:       set the bskel.observe.signing-key-pem Spring property (e.g. an env var)
#   Python:     receipt_sign.configure(os.environ.get("BSKEL_OBSERVE_SIGNING_KEY_PEM"))
bskel observe import --feature 001-organization-management --receipts receipts.jsonl \
  --pubkey ~/.bskel-keys/attest-public.pem [--require-signature]

Unset/no key configured means every receipt stays unsigned — fully backward compatible with every app already using this feature. --pubkey alone verifies signatures where present and tolerates unsigned receipts (excluding them from the report's matched counts, with a printed warning); --require-signature makes any unsigned or invalid receipt abort the whole import. See the "cryptographic receipt attestation" update in D-runtime-conformance-receipts in DECISIONS.md.

Every command is read-only until you explicitly run one of the mutating steps above — bskel status/bskel next (no arguments needed) tell you which gate is next and print the exact copy-pasteable command for it, without touching anything.

The full gated workflow, what each phase writes, and every flag is documented in SKILL.md (present in this repository, not in the installed npm package — see "What ships in the package" below).

Compatibility

| Requirement | Constraint | Why | |---|---|---| | Node.js | >=18 | ES2022 (Object.hasOwn) + ESM top-level await — nothing newer is used anywhere in the runtime code (verified by grep across every recent-ES-addition pattern; see D-npm-packaging in DECISIONS.md) | | git | required | every gate is git-state-derived | | ripgrep (rg) | required for scan/handles | every scanner adapter shells out to it at detect() time behind a blanket try/catch — traced live, missing rg does NOT throw, it silently makes every real adapter detect nothing (degrades to the low-confidence generic-grep fallback). bskel scan's report now carries a rg_available: false field plus an explicit unknowns warning whenever this happens, so it stays distinguishable from a genuinely-unrecognized repo — see D-zero-config-scan in DECISIONS.md | | gh (GitHub CLI) | optional | only used for preflight's 3-way default-branch cross-check; already soft-guarded, never a hard requirement | | python3 | optional | only needed to run this repository's own cross-language codec test — bskel itself never invokes python3 | | a build wrapper (gradlew/pom.xml+mvnw/package.json) | optional | only bskel verify --build needs one; handles emit never compiles anything itself |

Run bskel doctor in any target repo to see exactly which of these it found, with a remediation string for anything missing.

Supported scanner adapters (auto-selected by specificity, never hardcoded — see D-adapter-registry in DECISIONS.md):

  • java-spring — Spring Boot (build.gradle/pom.xml + src/main/java). Full capability set: operation extraction, request-body detection, and a real codegen provider for handles emit.
  • ruby-rails — Rails 8 (Gemfile + config/application.rb + config/routes.rb). Statically expands conventional routes/resources and extracts ActiveRecord table/primary-key metadata. Operation ids are deterministic bskel syntheses, not source declarations; --runtime-routes explicitly boots the trusted application for authoritative framework routing. Scanner only: request-shape extraction and handles codegen are not supported.
  • python-fastapi — FastAPI + SQLModel. Real codegen provider for handles emit; contract-grade operation extraction is not supported (FastAPI generates operation ids at runtime) — pass a real OpenAPI document via --openapi-file for a trustworthy contract.
  • typescript-express — TypeScript + Express + TypeORM. Real codegen provider for handles emit (entities come from @Entity/@PrimaryGeneratedColumn); no operation extraction — plain Express has no operationId concept, so pass --openapi-file for a contract. See D-typescript-express-provider in DECISIONS.md.
  • javascript-express — plain-JavaScript Express, both ESM and CommonJS, with no ORM (raw mysql2/mariadb), including serverless-http/Lambda deployments. Scanner only — routes and their real absolute paths are resolved through a full mount-graph walk (including direct CommonJS require() mounts and module.exports), but every capability is honestly false: there is no codegen provider, because raw SQL string literals carry no trustworthy table/primary-key/column-allow-list metadata. See D-javascript-express-adapter in DECISIONS.md for the measured reasoning.
  • generic-grep — unconditional last-resort fallback (Express/Flask/FastAPI-shaped route detection). Reconnaissance only, never contract-grade — always confidence: "low", requires --accept-low-confidence to proceed past a feature-scoped scan.

Generated-file policy

bskel handles emit writes real Java/Python source into your repository. Two things are always true about what it writes:

  • fetch() is wired to a real, existing, already-tested read-only service method — never hand-written business logic. It's generated only when a matching <Entity>Service method exists and takes exactly the one resource UUID argument a resolver always passes (a mismatch there means "no resolver generated", not "generate one and hope", since silently calling the wrong overload can drop a required scoping argument — see D-security-8 in DECISIONS.md).
  • patchField() is always a stub. Real codebases mix at least three different partial-update DTO conventions; guessing wrong would silently bypass real validation. A human finishes this by hand, every time.

Reruns are safe by construction, not by convention (D-handles-ownership in DECISIONS.md): safety is derived from the generated file's actual on-disk content, not from a manifest that might be absent (a fresh checkout, CI, or a repo that doesn't commit .sbf/). A file that diverged from what bskel generated (a hand-finished patchField(), someone else's edit) is never silently overwritten — it reports a conflict, and the escape hatch (--force --reason "...") is always audited, never silent.

Security model

bskel generates code that runs in production, so it was put through an adversarial security review (Codex, security-only lens, independent of the build process) — 10 numbered items, all fixed, each with an inline D-security-N comment at its exact location in the code (the last item bundles three additional lower-priority fixes from Codex's own broader "other things worth checking" pass). Highlights (full record in DECISIONS.md's "Security hardening pass" section):

  • Prototype-pollution guards everywhere a user-controlled string indexes a plain object (operation_id values like "constructor"/"__proto__" are rejected, not silently resolved via the prototype chain).
  • Path-traversal containment on every stack-catalog-driven file write (--choice, and every catalog entry's own declared template/path fields).
  • No predictable temp files, no silent permission downgrades in the generated bootstrap scripts that touch .env (mktemp + an unconditional chmod 600).
  • Authority derivation is per-method, not per-file — a controller's first @PreAuthorize match no longer silently applies to every resolver generated from that file; an unsupported annotation shape (hasAnyRole, SpEL) fails closed to a TODO_ROLE placeholder rather than guessing.
  • A companion authorization annotation (@PostAuthorize, @Secured, @RolesAllowed) next to an otherwise-safe @PreAuthorize refuses auto-materialization entirely (D-resolver-policy-contract) — a real, closed IDOR: @PreAuthorize("hasRole('USER')") sitting beside @PostAuthorize("returnObject.ownerId == authentication.name") used to auto-materialize ROLE_USER and silently ignore the ownership check. handles emit now generates an AuthorizationPolicy interface that blocks the target app from starting (not the build) until a human implements it.
  • Handle recovery cross-checks type/kind/pointer against the registry row, not just the raw UUID — the most severe finding: an attacker who controls the handle's type field could otherwise request a different, more sensitive resource's snapshot history that happens to share the same UUID.

Troubleshooting

Start with bskel doctor — it names exactly which required tool is missing and why, or run bskel status/bskel next to see which gate is currently blocking and the exact command to resolve it.

Every command shares one exit-code table (lib/exit-codes.mjs) and, with --json, an additive diagnostic envelope on payload-less early exits — the number is the stable contract, reason in the envelope is supplementary precision:

| Exit | Meaning | |---|---| | 0 | OK | | 2 | a required gate hasn't passed yet, or a referenced resource/adapter/provider doesn't exist (--json's reason field disambiguates which) | | 3 | a gate is awaiting a disposition decision (bskel scan disposition/bskel contract waive) | | 4 | a gate is stale — either an input actually changed, or (preflight only) the pass is simply too old | | 10 | not inside a git repository | | 11 | preflight: HEAD is behind the real default branch | | 12 | preflight: the three independent sources for "what is the default branch" disagree, or none could be determined | | 13 | preflight: uncommitted changes present (--allow-dirty to override) | | 14 | bad arguments | | 16 | a low-confidence scan was blocked (--accept-low-confidence to proceed anyway) | | 17 | the selected adapter/provider doesn't support a capability the command needs | | 18 | preflight: a git fetch was attempted and failed (--offline to accept a local-only verdict instead) |

Common cases:

  • preflight fails with STALE_BASE: your branch really is behind — git worktree add <path> -b <branch> origin/<default-branch> (or rebase in place), then re-run.
  • scan exits 16: the scanner fell back to generic-grep (low confidence, no real parser). If this repo actually is Java/Spring or Python/FastAPI-shaped, run bskel doctor first — it explains exactly why the real adapter didn't detect it, rather than reflexively passing --accept-low-confidence.
  • handles emit/contract emit exits 17: the adapter that scanned this repo doesn't declare the capability that command needs (e.g. generic-grep never declares codegen.handles — there's no codegen provider for a route-pattern-only stack). bskel doctor lists every installed adapter's declared capabilities.
  • A previously-passed preflight now reports stale with ttl_expired: passes expire after 30 minutes by default (data-derived, see D-preflight-freshness in DECISIONS.md) — re-run bskel preflight, or pass --max-age-minutes 0 to disable the TTL for a deliberately long-running or offline session.

The full exit-code/reason taxonomy, global flags (--help/--version/--json/--quiet), and the complete gated-workflow reference live in SKILL.md and DECISIONS.md in this repository.

What ships in the package

npm install ships only what bskel reads at runtime — bin/, lib/, contracts/, scanners/, handles/ (including every codegen template), stack/ (including the catalog and bootstrap templates), schemas/, and scripts/preflight-base-ref.sh. This repository's own test suite, SKILL.md (this project's Claude Code skill definition), DECISIONS.md, and CATALOG.md are not part of the published package — clone this repository directly if you want those.

License

Dual-licensed: AGPL-3.0-or-later for open-source use, or a commercial license for closed-source/proprietary use without AGPL's copyleft obligations. See COMMERCIAL-LICENSE.md for why, and how to obtain one.