sdlc v0.4.0 release checklist
State: closed/archived · Genre: checklist
The first real push of sdlc through the publishing channel (“Publishing sdlc: assemble, not snapshot”). Everything below is inert prose until a human runs it — nothing here executes automatically. This covers Phases 1-3 of the sdlc plan (PR #2284) only; P5/P6 are out of scope for this release.
Retargeted 2026-09-26: this release now ships to sksizer/sdlc, sdlc’s
own self-hosted single-plugin marketplace and product home, not
sksizer/marketplace-sdlc — see
D-P3NF-sksizer-sdlc-is-product-home. sksizer/marketplace-sdlc PR
#25 — the earlier
attempt at weaving sdlc into that pre-existing multi-plugin catalog — is
closed, not merged. sksizer/marketplace-sdlc itself is untouched and stays
in service as a separate catalog; it is simply no longer sdlc’s target.
scripts/sdlc-mirror/assemble.ts now has two layouts: --layout product
(this release — replaces the whole target tree, the default for a target
whose origin is sksizer/sdlc) and --layout marketplace (the original
surgical merge, preserved for a target actually shaped like
marketplace-sdlc). Every command below has been updated for the new
target; anywhere this file still says “the mirror” or “the checkout,” read
sksizer/sdlc.
Read .sdlc/runs/p14-install-evidence.md first.
That file is both the template for what “validation evidence” means below and the record of two
blockers a real release could not proceed past at the time it was written (Findings A and B,
§5’s (c) and (d) — both fixed since).
An opus adversarial review pass afterward found two more real gaps in assemble.ts and the publish
workflow (§5’s (e) and (f)); both are fixed as of this writing too, but §5 is still worth reading
before a real release — it says what was verified and how, not just “fixed.” (b) below predates
the retarget and describes a marketplace-sdlc-specific concern (another plugin’s content living
alongside sdlc’s in that repo) that does not apply to --layout product’s sksizer/sdlc target,
which sdlc owns outright — see the note at the top of (b).
0. What “sdlc v0.4.0” actually means
Section titled “0. What “sdlc v0.4.0” actually means”- Version 0.4.0 everywhere:
solutions/ontological/package.json, thesdlc.mdplugin manifest’sap-version:frontmatter,CHANGELOG.md, and every rendered plugin manifestagent-pants buildproduces. This is already the version onmain— Phase 14’s own version-bump work (#2331) landed it as prep, ahead of the actual release (see the wrinkle this causes in §2 before running anything). - Base: Phases 1-3 only, on top of
origin/sdlc/p3-vault. Nothing from P5 (sdlc/p5-config— config CLI,hooks:,sdlc apply, removal ofplugin_source) or P6 (host code) is in this release. - Mirror tag grammar (per
PUBLISHING.md’s “Tag grammar”): monorepo tagsdlc/v0.4.0, mirror tagv0.4.0(bare, nosdlc-prefix — confirmed against the actualpublish-lib.ymlpush step, which pushesv${VERSION}). Some earlier campaign notes call the mirror tagsdlc-v0.4.0; that was shorthand for “the v0.4.0 release of sdlc”, not a literal git ref — go with what’s actually implemented. This grammar is unchanged by the retarget: it names a tag shape, not a repo.
1. Preconditions
Section titled “1. Preconditions”- #2296,
#2312, and this plan’s own
Phase 14 PR (#2331) are
merged to
main. - The retarget PR (this one —
--layout product, the roster/docs/decision updates below) is reviewed and merged tomain. - Local CI green on
main, post-merge:bun run solutions/ontological/cli/sdlc.ts quality run --config sdlc.yaml --trust. Verified againstsdlc.yaml’sverbs.checklist andquality run --help: this one command already runs everything incheck— includingbun test solutions/ontological scripts ...andbun test ./.claude— so there is no separate “run the tests” step;--name checkis the default, no need to pass it explicitly. -
solutions/ontological/package.jsonreads0.4.0onmain. -
bun run solutions/ontological/scripts/build-node.ts --checkpasses (exit 0) onmain— this is exactly the kind of drift (cli/sdlc.jssilently going stale behind a skill-script move) that can reappear without warning;sdlc.yaml’scheckverb runs it, but it’s worth a direct look before a real release regardless. - No stale local
sdlc/v0.4.0tag pointing anywhere but the intended release commit (git tag -l 'sdlc/v0.4.0', and if present,git ls-remote --tags origin 'sdlc/v0.4.0'to confirm it isn’t already onoriginbefore deleting a local one withgit tag -d) —release-lib.ts’s “tag free locally” preflight fails otherwise. - A human has read §5 in full and either fixed each item or explicitly accepted the risk in writing (e.g. a comment on the release PR).
-
gh auth statusshows a token withreposcope coveringsksizer/sdlc(public repo — write access needed to push the branch and open the PR in §2a).
2. The real release commands
Section titled “2. The real release commands”Two things happen somewhat independently and can run in either order, but
assemble against the real sksizer/sdlc checkout first — reviewing that
diff is the whole reason this release doesn’t just let CI’s tag-triggered
pipeline do everything unattended (see
§5b). Unlike
every other release on this channel (and unlike sdlc’s own releases from
v0.5.0 on, per §2b), this first
push to sksizer/sdlc is a PR, not a direct branch push — it replaces the
repo’s entire pre-existing (Python-era) tree, which deserves a human’s
review on the mirror side too, not just a diff read locally.
2a. Assemble into sksizer/sdlc, with human review before a PR
Section titled “2a. Assemble into sksizer/sdlc, with human review before a PR”# Fresh clone dedicated to this release — NEVER a real local clone of# sksizer/sdlc reused across runs (assemble.ts refuses that path outright# via checkProtectedCheckout; this workflow must never touch one either).git clone https://github.com/sksizer/sdlc.git /tmp/sdlc-releasecd <this monorepo checkout, on main, post-merge>bun run scripts/sdlc-mirror/assemble.ts /tmp/sdlc-release --layout product --target-is-scratch--layout product is also what assemble.ts defaults to for a target whose
origin remote is sksizer/sdlc (resolveLayout) — passing it explicitly
here is just belt-and-suspenders, matching what CI’s own invocation does.
--target-is-scratch is required either way: /tmp/sdlc-release’s origin
resolves to the now-protected sksizer/sdlc coordinate, so
checkProtectedCheckout refuses it without that flag, exactly as it already
refused a marketplace-sdlc clone before the retarget.
This packs the same dependency closure and runs the same standalone
validation (bun install per harness, skill-script smoke, agent-pants check, node-only probe) as --layout marketplace always has — see
docs/PUBLISHING.md. What’s different under --layout product: instead of a === SURGICAL MERGE REPORT === block naming only
packs/sdlc/ and the "sdlc" manifest entry, the assembler removes
every existing top-level path in the target except .git (tracked paths
via git rm, untracked cruft via a plain filesystem delete — never a
history rewrite, just an ordinary working-tree change the next commit
records) and replaces it with the rendered plugin trees for all three
harnesses plus a root README.md/LICENSE/.gitignore/CHANGELOG.md. Run
--dry-run first (... --layout product --dry-run) to see exactly what
would be removed and added before committing to anything:
-
A human reads the
--dry-runoutput (or, on a real run, the removed/added file lists the assembler logs) andgit -C /tmp/sdlc-release status/git -C /tmp/sdlc-release diff --statbefore doing anything else. Confirm the only surviving pre-existing path is.gititself — everything else in the diff should be either a deletion of the old Python-era tree (python/,tests/,pyproject.toml,.github/workflows/— the old CI, gone with everything else —justfile,vision.md, …) or an addition this assembler is expected to make. This is real, irreversible-once- merged content replacing a public repo’s entire tree — see §4 for what “irreversible” actually means here. -
Command exits 0. See §5 for the leftover-path-gate false positives a real run hit historically (all fixed) — worth knowing the shape of a real failure here versus a new one.
-
Capture the same categories of evidence
.sdlc/runs/p14-install-evidence.mdcaptures: the dependency-closure list, the removed/added path lists (this layout’s equivalent of the merge report), the 3xbun installresults, and the node-only probe output for--versionplus at least two relocated ops (pick different ones than any already exercised in a rehearsal, for fresh coverage) — in all 3 harnesses. Save it as.sdlc/runs/sdlc-0.4.0-release-evidence.md(gitignored by default under.sdlc/*— force-add it the same way.sdlc/runs/p14-install-evidence.mdand.sdlc/runs/p2.mdare force-tracked:git add -f). -
Once satisfied with the diff, open a PR instead of pushing straight to
main— this first push replaces the whole pre-existing tree, so it gets the same review a normal PR would, even though nobody but this pipeline authored the branch:Terminal window cd /tmp/sdlc-releasegit checkout -b release/v0.4.0git add -Agit commit -m "release: sdlc v0.4.0 (bun/node) — replaces the Python-era tree"git push -u origin release/v0.4.0gh pr create --repo sksizer/sdlc --base main --head release/v0.4.0 \--title "release: sdlc v0.4.0 (bun/node) — replaces the Python-era tree" \--body "First public release of the bun/node sdlc rewrite. Assembled by \scripts/sdlc-mirror/assemble.ts --layout product from sksizer/dev \(solutions/ontological). See docs/planning/notes/N-EAYX-sdlc-0-4-0-release-checklist.md \and docs/planning/decisions/D-P3NF-sksizer-sdlc-is-product-home.md."bun run scripts/sdlc-mirror/assemble.ts /tmp/sdlc-release --layout product --prprints the equivalent commands for the current version without running any of them. Its branch step adapts to the checkout (no step when already onrelease/v<version>,git switchwhen the branch exists,git switch -cotherwise), and its PR body names the source sksizer/dev commit instead of this checklist’s path. -
Someone reviews and merges the PR on GitHub before continuing to §2b. This is the actual gate: nothing past this point should happen until that merge lands.
2b. Tag the release in this monorepo
Section titled “2b. Tag the release in this monorepo”Because solutions/ontological/package.json is already 0.4.0 on
main (the version bump was done as prep work inside the Phase 14 branch,
not reserved for release time), just release-lib sdlc <bump> --push cannot
be used in its normal form — verified directly:
$ bun run solutions/ontological/scripts/release-lib.ts sdlc 0.4.0 --dry-runerror: explicit version 0.4.0 must be greater than current 0.4.0
$ bun run solutions/ontological/scripts/release-lib.ts sdlc patch --dry-runrelease-lib: sdlc 0.4.0 -> 0.4.1 (tag sdlc/v0.4.1) # wrong version
$ bun run solutions/ontological/scripts/release-lib.ts sdlc minor --dry-runrelease-lib: sdlc 0.4.0 -> 0.5.0 (tag sdlc/v0.5.0) # also wrongrelease-lib.ts’s nextVersion requires the target to be strictly greater
than the manifest’s current version — there is no “tag the current version,
nothing to bump” mode. Since there’s nothing left to bump (the manifest
already reads 0.4.0, and bun.lock/tests were already exercised by the
Phase 14 PR’s own gate), the equivalent by hand, once main has the merge
commit and §1’s preconditions are all green, and once §2a’s PR has merged on
the sksizer/sdlc side:
git checkout main && git pull --ff-only# confirm package.json really is 0.4.0 and the tree is clean before tagginggit tag -a sdlc/v0.4.0 -m "release(sdlc): v0.4.0"git push origin sdlc/v0.4.0This tag lives on this repo (sksizer/dev) and is purely informational
here — it does not, by itself, push anything to sksizer/sdlc (§2a already
did that, by hand, as a PR). This is a decision point, not just a mechanical
workaround: an alternative is to teach release-lib.ts an explicit
“already at target, tag only” path (out of scope here — release-lib.ts is
on the “do not modify” list for this task). For releases after 0.4.0, prefer
NOT pre-bumping the version inside a feature branch, so release-lib.ts’s
normal patch/minor/major bump can do the tagging itself the way every
other library on this channel works — and, per
docs/PUBLISHING.md, from v0.5.0 on this repo’s own
sdlc/vX.Y.Z tag push is what fires publish-lib.yml’s normal
tag-triggered flow against sksizer/sdlc directly (no PR — a straight
branch push, same as every other library), since by then sksizer/sdlc
holds only sdlc’s own prior releases, not foreign Python-era content that
needs a human’s eyes on the replacement.
If OSS_PUBLISH_TOKEN is configured as a repo secret, pushing this tag also
fires publish-lib.yml in CI, which will redundantly try the assemble lane
again against sksizer/sdlc — harmless for v0.4.0 specifically, since by
the time this tag is pushed §2a’s PR has already merged and git tag v${VERSION} on CI’s own fresh clone will fail with “tag already exists”
(§2a’s commands above push v0.4.0 to the mirror only once the PR merges,
which CI’s automated push would then find already there) — but confirm this
is understood rather than confusing on the day.
3. Post-push checks
Section titled “3. Post-push checks”-
gh release list --repo sksizer/sdlcshows nothing new — this release does not create a GitHub Release, only a tag (matches every other library on this channel). - The mirror’s own
.github/workflows/release.ymlfor the npm binary is not wired up yet (see Finding in §5a) — do not expect an npm publish to happen from this tag push. That’s deliberately a separate, later step once the release-wiring gap is closed. Note that the old Python-era.github/workflows/insksizer/sdlcis gone as of this release (§2a’s tree replacement) — there is currently no CI at all onsksizer/sdlcuntil that gap closes. - Spot-check the mirror manually:
gh repo clone sksizer/sdlc /tmp/sdlc-verify -- --depth 1and confirm the singlesdlcplugin installs and runs (repeat a subset of §2a’s node-only probe against this fresh clone, not the release checkout that already has extra staging artifacts in its working tree).
4. Rollback
Section titled “4. Rollback”Because §2a is a PR, most of the usual “irreversible push” risk is already
mitigated before it ever reaches main on sksizer/sdlc: closing the PR
without merging is a full rollback with zero cleanup. Once merged, the
mirror is a normal git repo, pushed to with an ordinary fast-forward merge —
never force-pushed, matching this channel’s own convention
(PUBLISHING.md: “The mirror gets one squash commit
per release” — this first push is the one exception in shape, a whole-tree
replacement rather than a snapshot of one directory, but the same “never
force-push” rule applies). That means:
- Before the PR merges: close it. Nothing on
sksizer/sdlc’smainchanges; therelease/v0.4.0branch can be deleted or left alone. - Before anyone has pulled/installed the bad release, but after the PR
merged:
git revertthe merge commit onsksizer/sdlc’smainand push normally (a new commit, not a force-push), then delete and re-push the tag if it must point somewhere else (git push origin :refs/tags/v0.4.0 && git tag v0.4.0 <good-commit> && git push origin v0.4.0— only safe because nothing (no npm publish exists yet per §5a) has consumed that tag downstream). Note that reverting this particular merge commit restores the OLD Python-era tree, not an empty repo or a previous sdlc release — there is no previous sdlc release on this repo to fall back to. - If sdlc’s own monorepo tag
sdlc/v0.4.0was pushed prematurely or wrong: same idea —git push origin :refs/tags/sdlc/v0.4.0then re-tag once fixed. This repo’s tags are informational triggers, not artifacts anyone external depends on directly. - What is NOT possible: unpublishing an npm package version once published (moot until §5a’s gap closes and a first npm publish actually happens — at that point, follow npm’s own unpublish window rules, which this checklist does not repeat).
5. Known open gaps — read before touching any command above
Section titled “5. Known open gaps — read before touching any command above”One gap, (a), predates this checklist and is still open. Five more — (c) and
(d) (Findings A and B, from the install-evidence file), (e) and (f) (from a
later opus adversarial review), and (g) (also from that review, but in
agent-pants rather than assemble.ts/the publish workflow) — were found by
verification passes and are now fixed. Read what each one
actually was before assuming “fixed” means “was never a real risk”:
(a) Release-wiring gap (pre-existing, still open). assemble.ts only
vendors sdlc’s rendered plugin-pack surface (the three harness roots
under --layout product, or packs/sdlc/ under --layout marketplace) —
it does not vendor sdlc’s buildable CLI source (solutions/ontological’s
package.json, lib/, scripts/, cli/) that build-binary.ts/
build-npm-wrapper.ts need to cross-compile the npm binaries. The
mirror-side release template at
scripts/sdlc-mirror/templates/release.yml
assumes that source tree lands in the mirror at the same
solutions/ontological/ relative path it has today, but nothing in the
pipeline ships it there. Net effect: even after §2 pushes the plugin
correctly, npm install @sksizer/sdlc will not work until this gap is
closed — that template is not copied into sksizer/sdlc’s
.github/workflows/ yet specifically because of this open question. See
that template’s own header comment for the full reasoning. The retarget to
sksizer/sdlc (D-P3NF-sksizer-sdlc-is-product-home) makes this gap
easier to close later (its own Migration section tracks moving sdlc’s real
source there — T-3WLL), but does not close it now: v0.4.0 still ships
the vendored-tarball closure, not native source.
(b) Stale-5-pack human sign-off — DOES NOT APPLY to the sksizer/sdlc
target. This finding was about sksizer/marketplace-sdlc’s real main,
which carries 5 plugins (authoring, codebase, demo, repo, review)
that predate this pipeline and are NOT sdlc’s to touch — see
mergeSdlcPackIntoTarget’s surgical-merge behavior, still exactly as
described below, for that target. It is preserved here verbatim because it
records a real, hard-won finding about agent-pants build’s sweep behavior
that remains true for --layout marketplace. For this release,
sksizer/sdlc under --layout product owns its entire target tree
outright — there is no foreign plugin’s content to protect, and (e) below’s
scratch-build-then-copy structure exists for a different reason under this
layout (isolating the render from the target, not protecting a sibling
plugin). If a future release ever needs --layout marketplace again (e.g.
against the real marketplace-sdlc), this paragraph’s original concern
still applies there in full:
sksizer/marketplace-sdlc’s realmaintoday carries 5 plugins (authoring,codebase,demo,repo,review) that predate this pipeline. Earlier drafts of this checklist assumed an assemble run would delete all 5 automatically and that a human’s only job was to sign off on that deletion before the push. That assumption no longer holds, and it was never actually safe: (e) below found that at least some content under those 5 pack directories belongs to a different, unrelated plugin (dev-agent), not to a stale sdlc split — so an automatic “vault doesn’t produce it, therefore delete it” sweep across the whole harness tree (which is what a rawagent-pants build --out <target>does) could have deleted someone else’s live plugin.--layout marketplacenever attempts this: it only ever touchespacks/sdlc/and the"sdlc"manifest entry, and reports every other pack directory it finds as “left untouched” for a human to triage separately. What a human must still do, as its own manual step, outsideassemble.ts, before EVER running--layout marketplaceagainst the realmarketplace-sdlcagain: look at what is actually left underauthoring/codebase/demo/repo/reviewin a real checkout, work out which parts (if any) are genuinely stale sdlc leftovers safe to delete by hand and which belong todev-agent, and do that cleanup — if any — as its own separate, reviewed commit.
(c) Finding A — leftover-path gate false-positives on cli/sdlc.js
(fixed). assemble.ts’s gate (scanForLeftoverPaths) failed on 8 hits
inside the built cli/sdlc.js bundle in every harness, every time,
unconditionally (before --skip-validate was even checked), blocking every
real run. Full reproduction and root-cause analysis in
.sdlc/runs/p14-install-evidence.md, Finding A; what was fixed and how the
gate was hardened further during the adversarial-review fix round is
described above, in §2a’s checklist item for this same command — not
repeated here. This gate is layout-independent — it scans whatever ends up
under HARNESS_PACK_DIRS regardless of how it got there — so this fix
applies equally to --layout product.
A third, emergent instance of the same class of false positive surfaced
after that hardening merged with a parallel fix round that added a second
bun build --target=node output, lib/services/docs/list-ops.js (for
sdlc docs generate running installed, no bun) — the two branches never saw
each other’s diff, so the gate’s exemption list only covered the bundle
that existed when it was written. A real assemble run caught it immediately
(543 hits, all in list-ops.js, the same confirmed-inert categories as
Finding A). Fixed by generalizing the exemption from “cli/sdlc.js
specifically” to “any bundle-node-entry.ts output” — see
scripts/sdlc-mirror/assemble.ts’s NODE_BUNDLE_RELPATHS.
(d) Finding B — the committed cli/sdlc.js was stale (fixed).
Independent of Finding A: the currently-committed
solutions/ontological/cli/sdlc.js was built once
(371db2097) and never rebuilt after the CLI-ops migration commits
(19b5cce54, d574f63c8, 070b9d00a, 87f1775fc) that moved the 8 skill
scripts behind CLI ops. build-node.ts --check confirmed it was stale; the
bundle contained none of the 8 relocated ops (verified directly — e.g.
grep -c dedup-search solutions/ontological/cli/sdlc.js → 0). Full
reproduction in .sdlc/runs/p14-install-evidence.md, Finding B. Fixed in
a65ad2c19, which rebuilt all 4 committed copies (solutions/ontological/cli/sdlc.js
and the 3 pre-rendered pack copies). Nothing in today’s quality gate would
have caught this drift on its own at the time; that gap is closed now too —
build-node.ts --check and build-list-ops.ts --check are both wired into
sdlc.yaml’s check verb list, so a future stale-bundle regression fails
local/CI checks directly instead of waiting on this checklist’s §1
precondition to catch it.
(e) Assembler could have clobbered an unrelated plugin’s content (found by
an opus adversarial review, fixed) — the origin of --layout marketplace’s
scratch-build-then-merge design. assemble.ts used to run agent-pants build --out <target> directly against the real mirror checkout.
agent-pants build treats a harness’s WHOLE tree (everything under its one
marketplace manifest — every pack, not just sdlc’s) as owned by whichever
vault built it, and sweeps anything under there the vault doesn’t currently
produce as “stale,” including a whole foreign plugin directory and its
manifest entry. That is exactly right for a tree only sdlc has ever built —
which is precisely what a --layout product target like sksizer/sdlc is,
so under that layout applyProductLayout copies the scratch build’s harness
roots into the target wholesale, no per-file merge needed — and exactly
wrong for a real marketplace-sdlc checkout, which — per this same review —
holds at least some content belonging to a different plugin (dev-agent)
alongside sdlc’s. Verified by hand: a scratch checkout seeded with a foreign
dev-agent pack lost that pack and its manifest entry entirely after a
direct agent-pants build --out <target> run. Fixed, for --layout marketplace, by building sdlc’s pack into an isolated scratch directory
first, then merging only packs/sdlc/ and only the "sdlc" manifest entry
into the real target (mergeSdlcPackIntoTarget in assemble.ts) — every
other pack directory and manifest entry is left byte-for-byte untouched, and
both --dry-run and a real run report exactly what was touched vs. left
alone. Regression-tested against a constructed foreign-pack fixture in
scripts/sdlc-mirror/assemble.test.ts. Human review of a real assemble
run’s --dry-run/report output is still recommended before the real push
— the fixture tests prove the mechanism works, not that a specific future
real checkout holds nothing this reasoning hasn’t anticipated. The same
review pass also found and closed a related gap: several packages/ts/*
packages in sdlc’s vendored dependency closure (color, manifest-graph,
markdown-util, system-model) shipped with no package.json files
allowlist, so their packed tarballs relied on npm’s unaudited default of
“everything not gitignored.” All four now declare one, and assemble.ts
fails the assemble outright (or warns loudly under --dry-run) if a future
closure package is missing one.
(f) OSS_PUBLISH_TOKEN exposed to untrusted install scripts during
assembly (found by the same review, fixed). .github/workflows/ publish-lib.yml’s “Clone mirror for assembly” step embedded
OSS_PUBLISH_TOKEN into the clone URL whenever the secret was configured,
even though that checkout only ever needs read access — the token then sat
in .git/config for the rest of the job, reachable by anything running
inside that checkout, including third-party install scripts from sdlc’s
vendored dependency closure that assemble.ts’s own standalone validation
installs. Fixed two ways: that clone is now always anonymous (the mirror
repos are public; the one step that actually needs write access re-points
origin at the authenticated URL immediately before it pushes, as it
already did), and the standalone-validation bun install now runs against
a manifest that declares an explicit trustedDependencies allowlist
(exactly node-pty, the one dependency in the closure with a load-bearing
install script — see TRUSTED_CLOSURE_INSTALL_SCRIPTS in assemble.ts),
so bun blocks every other dependency’s install/postinstall script by
default instead of running whatever the closure happens to contain. This
CI clone step now also passes --target-is-scratch (added alongside the
retarget, since the protected-checkout guard covers sksizer/sdlc the same
way it always covered sksizer/marketplace-sdlc — CI’s fresh clone would
otherwise be refused).
(g) Codex/Cursor skill prose called the CLI through a Claude-only token —
install-breaking on both harnesses (found by the same opus review, fixed).
Every skill in the vault writes its CLI call site the same way, as literal
prose: ${CLAUDE_PLUGIN_ROOT}/cli/sdlc <verb> .... agent-pants copied that
text unchanged into all three rendered packs. ${CLAUDE_PLUGIN_ROOT} is a
Claude Code plugin token, substituted only by Claude Code itself when it
loads skill/command/agent Markdown (Claude Code’s plugin manifest reference,
“Environment variables” section: the token resolves in skill/command/agent
content specifically, with no counterpart documented for another harness).
Neither Codex CLI’s nor Cursor’s own skills documentation mentions any such
substitution — Cursor’s instead instructs authors to “reference scripts in
your SKILL.md using relative paths from the skill root.” An unset
${CLAUDE_PLUGIN_ROOT} expands to nothing, so every Codex/Cursor skill that
called the CLI resolved the token to an empty prefix and failed outright
(/cli/sdlc: No such file or directory) — reproduced directly, both before
and after the fix, in .sdlc/runs/p14-assemble-evidence.log’s dated
“per-harness CLI invocation fix” entry. Fixed in agent-adapt-core’s render
(solutions/agent-pants/crates/agent-adapt-core/src/export.rs): a new
per-harness CliInvocation on RootSpec leaves Claude’s text untouched
(Claude Code substitutes the token itself) and rewrites a Codex/Cursor call
site to the skill-relative scripts/sdlc shim instead — config-driven off
the existing shim_target (T-VAW3 AC-3), not a hardcoded string. The shim
itself is now emitted only where it can matter: into a Codex or Cursor pack,
and only for a skill whose rendered body actually contained a call site to
rewrite (a Claude pack never gets it at all; a skill that never calls the
CLI doesn’t either, even on Codex/Cursor). Verified against all three
harnesses’ actual rendered packs, copied outside the repo with bun hidden
from PATH, running real task-work/pr-respond/orchestrate command
lines exactly as their SKILL.md writes them, from each skill’s own
directory — see .sdlc/runs/p14-assemble-evidence.log for the full
transcript and a negative-control run proving the pre-fix failure was real.