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/sdlc →
sdlc project setup → sdlc 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.
| Fact | Source | Implication |
|---|
Repo is plugin-shaped (@sksizer/sdlc-plugin; CLI at plugin/cli/) | package.json, D-0001-project-structure | Naming inverts under CLI-primacy |
Every skill→CLI invocation is plugin-root-relative (bun run ${CLAUDE_PLUGIN_ROOT}cli/sdlc.ts) | SKILL.md corpus survey | The plugin is self-contained; one artifact can carry both surfaces without skew |
| Substrate ships unbuilt, run from source under Bun | D-0001-project-structure, D-0006-typescript-substrate | Dev 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 v24 | An 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: imports | repo scan | Violates the runtime-agnostic rule set below; cheap, named migration debt |
| Multiple agent targets are anticipated beyond Claude Code | this decision | Skills 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 decision | Channel must not assume more than a dev-dependency of a JS project shape |
| Repo stays private for now | this decision | Registry must support private packages |
- 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).
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- The dev repo keeps dogfooding from source via its
./plugin
directory marketplace (P-0006-self-hosting-development
unaffected).
- 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.
| Option | Strengths | Weaknesses | Verdict |
|---|
| npm artifact, both doors inside | One pin, no skew, CI-trivial, near-zero code movement | Needs a publish-time build; read credential while private | Chosen |
Compiled binary (bun build --compile, GitHub Releases / proto) | No runtime dependency at all | Forces asset-embedding refactor (schemas, templates, scaffold reads); harness still needs real files on disk | Deferred — trigger: a consumer with no JS runtime |
| Separate packages (plugin via marketplace + CLI via npm) | Independent release cadence | Version skew between skills and verbs; two pin points; handshake machinery | Rejected |
Git-tag install (bun add github:…#tag) | No registry infra | Repo root ≠ package root; no subdirectory installs; weaker integrity | Fallback only |
| User-level plugin float (status quo ante) | Zero-step availability everywhere | No per-project pacing; unreproducible CI | Rejected as default |
| Option | Verdict |
|---|
| Runtime-agnostic source + publish-time build to JS | Chosen — npx 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-required | Rejected — couples shipped semantics to one runtime; every consumer pays a toolchain install |
| Option | Verdict |
|---|
| 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 Packages | Alternative if semver ranges / Renovate ergonomics start to matter; free private packages, but every consumer carries .npmrc auth |
| npm private packages | Paid; no advantage over the above |
| Public npm | Blocked 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.
- Land this ADR.
- Migrate the
Bun.* call sites out of shipped code:
Bun.spawnSync → node:child_process, Bun.Glob → node: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).
- 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).
- 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).
- 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).
- 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).
- Pilot on one consumer — polish is the named candidate; its reinstall
has been deferred pending exactly this pinnable artifact.
- 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