Skip to content

Distribution — CLI-primary npm artifact; harnesses are control planes

Status: open/accepted

  • sdlc is the primary interface to the app. Harnesses — the Claude Code plugin today — are control planes into it, not the ship vehicle. A head defers to the installed app’s version, never the reverse. See Decision.
  • One npm artifact, @sksizer/sdlc, carries both doors: the app (substrate + CLI, bin sdlc) and the Claude harness (the plugin directory as a package asset). Consumers pin one version via their lockfile; plugin/CLI skew is impossible by construction.
  • While private, distribution is a git-pinned dist repo — release CI pushes the built publishable tree; consumers pin a tag/sha as a git dependency, no registry auth. Public npm when it goes public. Shipped code is JS-runtime-agnostic and the artifact ships built JS (Node refuses to type-strip under node_modules), so npx sdlc needs nothing but Node ≥ 20. Bun stays the workspace toolchain (D-0012-monorepo-tooling); the handful of Bun.* calls in shipped code are named migration debt.
  • Harness targets will multiply. Skills head for a target-agnostic source form transformed into per-target control planes (Claude Code is the first target). Not scheduled here; recorded so distribution choices don’t foreclose it.
  • Onboarding is CLI verbs: bun add -d @sksizer/sdlcsdlc project setupsdlc harness install claude.
  • Live-source stays available as an opt-in mode for any consumer. The built artifact is the Bun-free default; a consumer may instead run the substrate from a source checkout under Bun (linked install or SDLC_HOME), the same way the dev repo dogfoods. See Decision.
  • Resolves D-0001-project-structure’s deferred “substrate bridging mechanism” question: the bridge is a separate package install.

SDLC started as a Claude Code plugin and is still shaped like one: the repo’s package is named @sksizer/sdlc-plugin and the CLI lives inside the plugin tree. Downstream consumers now exist — moon workspaces, single node packages, Rust libraries — that need the deterministic surface (CI gates, site generation per D-0010-deterministic-site-assembly) without a Claude session. D-0001-project-structure deferred how the substrate reaches consumers “until the consumer is concrete”; it is now concrete.

FactSourceImplication
Repo is plugin-shaped (@sksizer/sdlc-plugin; CLI at plugin/cli/)package.json, D-0001-project-structureNaming inverts under CLI-primacy
Every skill→CLI invocation is plugin-root-relative (bun run ${CLAUDE_PLUGIN_ROOT}cli/sdlc.ts)SKILL.md corpus surveyThe plugin is self-contained; one artifact can carry both surfaces without skew
Substrate ships unbuilt, run from source under BunD-0001-project-structure, D-0006-typescript-substrateDev stays source-run; the publish boundary is where a build is acceptable — and, per Node’s node_modules rule, required
Node refuses to type-strip TS under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING)verified on Node v24An unbuilt npm package can never run on plain Node; the publish-time build is forced
Shipped code has a small, bounded Bun coupling: Bun.spawnSync (~5 sites), Bun.Glob (1), Bun.serve (dashboard); no bun: importsrepo scanViolates the runtime-agnostic rule set below; cheap, named migration debt
Multiple agent targets are anticipated beyond Claude Codethis decisionSkills head for a target-agnostic source form; the artifact layout must not foreclose per-target outputs
Consumers are polyglot (moon, node packages, Rust libraries)this decisionChannel must not assume more than a dev-dependency of a JS project shape
Repo stays private for nowthis decisionRegistry must support private packages
  1. The sdlc CLI is the primary interface to the app. Every effect lands through a registry op (D-0007-deterministic-op-substrate); harnesses, CI, and applications are interchangeable heads over the same tails (S-0004-sdlc-cli-llm-head-deterministic-tail, D-VSLI-distributed-work-runner-architecture).
  2. Harnesses are control planes — and executors. As control plane, the Claude plugin keeps thinning toward pure head-work — interpretation, questioning, sub-agent dispatch — with all effects through sdlc verbs; effectful skill-side scripts are migration debt, to be folded into registry ops. The arrow also reverses: the app dispatches harnesses as executors inside sdlc-triggered workflows — e.g. a future sdlc run <task> opening a collaborative session to sketch requirements, or driving a headless run to implement a step (D-VSLI-distributed-work-runner-architecture; D-VSLI-distributed-work-runner-architecture’s open claude -p head contract).
  3. The compatibility arrow points at the app. A head defers to the installed app’s version; a harness never carries its own divergent copy of the substrate for a consumer.
  4. Harness targets multiply; skills head for a target-agnostic source form. The Claude plugin is the first target and, today, also the hand-authored source. When a second target lands, skills are stored in an agnostic form and transformed into per-target outputs, each shipping as an asset of the same artifact and installing via sdlc harness install <target>. This ADR schedules none of that work; it records the direction so the artifact layout and the install verb don’t foreclose it.
  1. Publish one npm artifact, @sksizer/sdlc. It contains the app (substrate + CLI, bin: sdlc) and the Claude harness (the plugin directory as an asset). The single version number covers both doors.
  2. While private, distribution is a git-pinned dist repo — no registry. Release CI builds the package and pushes the publishable tree to a private dist repo under the same account; consumers add it as a git dependency pinned to a tag or commit sha (bun add git+ssh://…#<ref>, npm i git+https://…#<ref>). Local installs ride existing git credentials — no .npmrc. Move to public npm if/when the repo goes public.
  3. Shipped code is JS-runtime-agnostic. The substrate, CLI, and adapters use runtime-agnostic APIs — node: builtins and web standards; Bun.* globals and bun: imports are confined to dev tooling and tests. Node is the floor every consumer can count on (npx sdlc requires nothing else); Bun remains the workspace toolchain and dev runtime per D-0012-monorepo-tooling, and stays the fastest way to run the CLI. Runtime-specific APIs return only with a strong, recorded reason. The existing Bun.* calls are migration debt, gated against reintroduction once cleared.
  4. The npm artifact ships built JS; the repo stays unbuilt. Node refuses to type-strip TypeScript under node_modules by design (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING, verified on v24), so an unbuilt package can never satisfy the Node floor — the publish-time build is forced, not optional. A prepack step compiles the shipped trees to JS (+ sourcemaps) and keeps engines.node at a plain-JS floor (≥ 20). Dev is unchanged: the repo runs TS from source under Bun, and D-0001-project-structure’s “authored path equals runtime path” holds everywhere except inside the published artifact. Skills invoke the cli/sdlc launcher (not sdlc.ts directly), which resolves runtime and entry — Bun + source in the repo, Node + built JS in the artifact.
  5. Per-consumer pinning via lockfile is the default. Upgrades arrive as version-bump PRs (Renovate-able); one bump moves skills, CLI, and entity schemas atomically. User-level marketplace float is a convenience for scratch projects only.
  6. Onboarding goes through the CLI, including installing the control plane: bun add -d @sksizer/sdlc (or npm i -D), then sdlc project setup, then sdlc harness install claude — which materializes the plugin and writes the extraKnownMarketplaces stanza into .claude/settings.json (repo-local copy vs a node_modules pointer: see Open questions). A future sdlc harness install <other> slots in beside it.
  7. The dev repo keeps dogfooding from source via its ./plugin directory marketplace (P-0006-self-hosting-development unaffected).
  8. Live-source is a supported opt-in mode — for any consumer, not just the dev repo. The built JS artifact is the default and the Bun-free floor; a consumer may instead run the substrate from a source checkout under Bun, via a linked/local install (bun add file:…, npm link, a workspace ref) or an SDLC_HOME override that points the launcher at that checkout. The launcher’s runtime/entry resolution (point 8) is the single seam that chooses source-under-Bun vs. built-JS-under-Node — the build exists only because plain Node cannot type-strip TS under node_modules, and live-source sidesteps that by running source under Bun. Live-source requires Bun on PATH and, by design, forgoes point 9’s lockfile-pinned reproducibility — it is the contributor/debug mode, extending D-0001-project-structure’s “authored path equals runtime path” to consumers on demand.
  • CLI-primacy is context-polymorphism applied at the distribution boundary. D-VSLI-distributed-work-runner-architecture already makes the CLI tail the capability and heads interchangeable; shipping the CLI as the product and the plugin as one head makes the artifact match the architecture.
  • One artifact kills skew. Skills invoke the CLI plugin-root-relative, so wherever the plugin directory lands, it finds its own CLI. Splitting plugin and CLI into separate packages would create two pin points and a version-handshake problem for no benefit.
  • Runtime-agnosticism is P-0008-harness-agnostic-substrate applied to the runtime dimension. Coupling shipped semantics to Bun.* makes the runtime a hidden harness: every consumer pays a toolchain install for zero consumer-visible benefit, and swapping runtimes later means touching semantics instead of a shebang. Node is what every consumer’s CI already has. The coupling is also cheap to remove now — a handful of spawnSync/Glob/serve call sites — and only gets more expensive.
  • npm is the lowest-friction channel for an unbuilt TS app. Even a Rust-only consumer pays one dev-dependency and a lockfile — a standard pattern for docs/formatting tooling — and with a runtime-agnostic core, any Node-capable CI runs npx sdlc with no extra setup.
  • Lockfile pinning is required, not preferred. Entity schemas migrate (entities-migrate exists because versions move); a floating user-level install cannot hold one project back during a breaking change. The polish teardown demonstrated the failure mode.
  • The build lives only at the publish boundary. The dev repo keeps source-run (“authored path equals runtime path”, D-0001-project-structure); the published artifact is compiled because Node’s node_modules type-stripping refusal makes that the only way a plain-Node consumer can run it. One divergence point, mechanical, covered by sourcemaps.
OptionStrengthsWeaknessesVerdict
npm artifact, both doors insideOne pin, no skew, CI-trivial, near-zero code movementNeeds a publish-time build; read credential while privateChosen
Compiled binary (bun build --compile, GitHub Releases / proto)No runtime dependency at allForces asset-embedding refactor (schemas, templates, scaffold reads); harness still needs real files on diskDeferred — trigger: a consumer with no JS runtime
Separate packages (plugin via marketplace + CLI via npm)Independent release cadenceVersion skew between skills and verbs; two pin points; handshake machineryRejected
Git-tag install (bun add github:…#tag)No registry infraRepo root ≠ package root; no subdirectory installs; weaker integrityFallback only
User-level plugin float (status quo ante)Zero-step availability everywhereNo per-project pacing; unreproducible CIRejected as default
OptionVerdict
Runtime-agnostic source + publish-time build to JSChosennpx sdlc works with Node ≥ 20 alone; divergence confined to the publish boundary; dev stays source-run under Bun
Runtime-agnostic unbuilt source (type stripping)Rejected for the artifact — Node refuses to strip types under node_modules, so shipping source cannot satisfy the Node floor
Bun-requiredRejected — couples shipped semantics to one runtime; every consumer pays a toolchain install
OptionVerdict
Git-pinned dist repo (same account)Chosen — zero registry auth; local installs ride existing git creds; the lockfile pins the commit sha. Costs: a release push step; consumer CI needs a cross-repo read credential (PAT / deploy key — the default Actions token can’t fetch other private repos)
GitHub PackagesAlternative if semver ranges / Renovate ergonomics start to matter; free private packages, but every consumer carries .npmrc auth
npm private packagesPaid; no advantage over the above
Public npmBlocked while the repo is private; the target when it opens
  • Downstream CI is one line after install in any Node-capable job: npx sdlc entities validate, npx sdlc docs generate, a future npx sdlc site build (bunx equally).
  • A quality gate keeps shipped trees free of Bun.* / bun: once the migration lands, so the runtime-agnostic rule is enforced, not remembered.
  • Once a transform pipeline exists, the plugin directory inside the artifact becomes a generated target output rather than the source of truth; the consumer-facing install flow does not change.
  • Site assembly must become consumer-invocable: the Astro scaffold ships as a package asset and a self-contained verb builds the site against the consumer’s docs/planning/, with the consumer owning site.yaml and supplemental/. Direction set here; mechanics belong to D-0010-deterministic-site-assembly follow-up work.
  • The package rename (@sksizer/sdlc-plugin@sksizer/sdlc) records the inversion now; D-0001-project-structure’s harness-surface move (plugin/harnesses/claude/ etc.) later makes the tree tell the same truth. Its sequencing is unchanged — after in-flight changesets land; for consumers it is only a files: list change.
  • The “one artifact, two doors” property depends on skills staying plugin-root-relative. A skill invoking a PATH-resolved sdlc would reintroduce skew.
  1. Land this ADR.
  2. Migrate the Bun.* call sites out of shipped code: Bun.spawnSyncnode:child_process, Bun.Globnode:fs glob, the dashboard’s Bun.serve → Hono’s Node adapter. Add the CI gate that keeps Bun.* / bun: out of shipped trees. Normalize the skill corpus onto the plugin-root-relative cli/sdlc launcher and teach the launcher to resolve runtime and entry across three cases: (a) Bun + sdlc.ts in the dev repo; (b) Node + built sdlc.js in the installed artifact — the Bun-free consumer floor; (c) Bun + sdlc.ts from a source checkout a consumer opted into via a linked install or SDLC_HOME — the live-source mode (point 12). A consumer without Bun drives every skill through (b); a contributor still runs source through (c).
  3. Reshape the root package.json for publishing: rename to @sksizer/sdlc, add the bin entry, add a files: allowlist (the plugin tree plus license/readme), wire the prepack build (shipped trees → JS + sourcemaps), keep engines.node at the plain-JS floor (≥ 20).
  4. Audit path assumptions: anything resolved from cwd or git rev-parse --show-toplevel breaks when the package root is not the repo root (sites/df-docs/package.json’s generate script does this today).
  5. Add sdlc harness install claude; extend sdlc project doctor to assert the registered plugin path and the running CLI are the same install (/sdlc:info already diagnoses which install won).
  6. Wire tag-driven release CI that builds and pushes the publishable tree to the private dist repo; document consumer install (git dependency pinned to a tag/sha; cross-repo read credential for consumer CI).
  7. Pilot on one consumer — polish is the named candidate; its reinstall has been deferred pending exactly this pinnable artifact.
  8. Follow-up under D-0010-deterministic-site-assembly: sdlc site build with the scaffold as a package asset.
  • Where the installed plugin lives. Leading candidate: sdlc harness install claude materializes the plugin into a stable repo-local directory (e.g. .sdlc/plugin/, gitignored) and points the marketplace there — a predictable path that survives node_modules wipes and sidesteps any plugin-cache behavior over mutable/symlinked node_modules paths. The install verb re-materializes on version change; project doctor flags drift. Pointing the marketplace directly at node_modules/@sksizer/sdlc/plugin is the zero-copy alternative; the pilot decides.
  • Dogfooding the consumer path. Whether the dev repo’s CI should also install the published artifact and exercise the node_modules doors, rather than only running from source.
  • The agnostic skill source form. What the target-agnostic skill representation and its transform tooling look like. Deferred until a second harness target is real; the first transform defines the format.
  • A consumer with no JS runtime at all. Compiled binaries (bun build --compile + GitHub Releases / proto) remain the deferred answer; the trigger has not fired.
  • Filename per D-0002-entity-identifier-shape: incrementing D-0014, slug cli-primary-npm-distribution. There is no sdlc decision create verb; this file is hand-authored against the decision body template.
  • Until migration steps 2–3 land, the Bun.* call sites and the absence of a built artifact mean Bun is still required in practice; npx sdlc under plain Node is the supported path only after those steps. The interim is short and the ADR’s rule is the end state, not the present tense.

← Back to Decisions