Skip to content

fix(build): guard the generated manifests and stop tests mutating the tree - #42

Merged
atamanvega merged 1 commit into
theam:mainfrom
ophiocus:fix/manifest-drift-guard
Aug 19, 2026
Merged

fix(build): guard the generated manifests and stop tests mutating the tree#42
atamanvega merged 1 commit into
theam:mainfrom
ophiocus:fix/manifest-drift-guard

Conversation

@ophiocus

Copy link
Copy Markdown
Contributor

What & why

Closes #41 — all four items.

The guard in #40 covers the skill and instruction trees but not the manifest pair, so its own headline claim (builder and validator cannot disagree) did not hold end to end. plugins/fullstack-dev-kit/plugin.json is dual-emitted from .codex-plugin/plugin.json, and the validator only checked it structurally$schema, the name pattern, the forbidden keys. Structure cannot tell you a generated file is current.

Confirming your reproduction on 56cd315:

edit the Codex manifest's description, skip the build validator
56cd315 (current main) exit 0 — stale portable copy ships
this branch exit 1portable … differs from the source it is generated from

The derivation moves into scripts/lib/bundle-sources.mjs as portableManifestFrom() + serializeManifest(). The builder writes through it; the validator re-derives from the source and byte-compares. Same rule the trees are held to, same shared-map reason it cannot drift.

The other three:

  • Collision (item 4). skillNameCollisions() reports a skill name present in both skills/ and codex/skills/. Both scripts now exit non-zero — the builder before it copies anything, so it fails at the cause rather than as unfixable drift afterwards.
  • Tests mutating the tree (item 2). Each mutating test now runs against a mkdtempSync() copy. Nothing tracked is opened for writing at all, so a SIGKILL mid-run cannot leave the tree dirty and break the next run's in-sync test and git diff --exit-code.
  • Dead DST_INSTR (item 3). Removed.

Type

  • Bug fix
  • New/improved stack profile (instructions/stacks/…)
  • New/improved adapter (tracker / PR host)
  • Docs
  • Other:

Verification

$ node scripts/build-codex-plugin.mjs && node scripts/validate-codex-plugin.mjs
✓ Codex plugin bundle is valid …
$ node --test
# tests 8   # pass 8   # fail 0
$ git diff --exit-code
$ echo $?
0

Tests go 5 → 8: manifest drift, the committed manifest being a byte-exact derivation of its source, and the collision guard.

Integrity checks on the tests themselves

Passing tests prove nothing until they fail without their fix, so I mutated each one out and checked that exactly the intended test died:

mutation result
validator stops byte-comparing the manifest 7 pass / 1 failediting the Codex manifest without rebuilding
collision guard removed from both scripts 7 pass / 1 faila skill name in two collections
builder stops using the shared derivation 5 pass / 3 fail
restored 8 / 8

That pass also found a false-pass hole in my own tests, now closed: the sandbox copy is asserted valid before each test mutates it. Without that, a tree missing from the copy list would make the sandbox invalid for an unrelated reason and every expect exit 1 assertion would pass for the wrong one. Proof it fires — dropping .agents from the copied trees turns 5 silent false passes into 5 loud failures.

Also checked, all clean: building twice is a no-op; a missing or malformed .codex-plugin/plugin.json produces one clear error rather than a crash or a confusing second one; bumping .claude-plugin/plugin.json without rebuilding is still caught by the existing version check and is not masked by the new one; and a fresh clone of this branch builds, validates and passes 8/8 with a clean tree.

One thing I noticed and deliberately did not sweep: the test assertions matched /instructions\/stacks\/php\.md/ while the validator builds those labels with path.join, which is backslashed on Windows. I normalised separators in the assertions I touched, but the same assumption may exist elsewhere in the repo — out of scope here, happy to file it separately if useful.

On the version bump

Not bumped, same reasoning as #40 and your call again: scripts/ never ships, and the bundle is byte-identical after a rebuild (git diff --exit-code above), so nothing users install changes. Say the word and I'll push the bump with the CHANGELOG entry moved under it.

Checklist

  • Keeps the kit stack-agnostic — tooling only, no stack or client specifics.
  • Doesn't weaken the quality gates or the telemetry privacy guarantees — it extends a gate; telemetry/ and hooks/ are untouched.
  • Docs updated in the same PR — CHANGELOG under [Unreleased]. No CONTRIBUTING change needed: the contributor-facing workflow is unchanged, only what validation catches.
  • If this touches packages/ or telemetry/, I've flagged it for extra review — it does not.
  • Version bumped in .claude-plugin/plugin.jsonnot bumped; your call. Reasoning above.

… tree

The drift guard added in theam#40 covers the skill and instruction trees but not the
manifest pair, so its own claim — builder and validator cannot disagree — did not
hold end to end. `plugins/fullstack-dev-kit/plugin.json` is dual-emitted from
`.codex-plugin/plugin.json`, and the validator only checked it structurally:
editing the Codex manifest and skipping the build left the portable copy stale
while validation still exited 0.

The derivation moves into `scripts/lib/bundle-sources.mjs` as
`portableManifestFrom()` + `serializeManifest()`. The builder writes through it and
the validator re-derives and byte-compares, so the manifests are held to the same
rule as the trees.

Also from the theam#41 review:

- `skills/` and `codex/skills/` copy into one bundle directory on purpose, but the
  same skill name in both was second-wins at build time and unfixable drift after.
  `skillNameCollisions()` reports it and both scripts exit non-zero at the cause.
- The guard tests edited `instructions/stacks/php.md` and restored it in `finally`.
  A run killed in between left the tree dirty, which then failed both the in-sync
  test and CONTRIBUTING's `git diff --exit-code` step. They now run against a
  disposable copy and never touch a tracked file.
- Removed the dead `DST_INSTR` left by the bundle-sources refactor.

Tooling only; no plugin version bump.

Refs theam#41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ophiocus
ophiocus requested a review from atamanvega as a code owner August 18, 2026 20:39
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

@ophiocus is attempting to deploy a commit to the Monkeys' projects Team on Vercel.

A member of the Team first needs to authorize it.

@atamanvega atamanvega left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ophiocus — I reviewed this with the kit's own pr-reviewer and it's a clean, correct fix. Build + validate pass, 8/8 tests, git diff --exit-code clean, no leaked tmp dirs. The manifest derive/compare is deterministic and false-positive-safe (same portableManifestFrom + serializeManifest on both sides, fixed key order), the collision guard fails in the builder before it copies anything (so the tree never enters the second-wins state), and the drift tests run against a mkdtempSync copy so nothing tracked is ever opened for writing.

Full disclosure: I'd independently written a near-identical fix (#43) this morning before spotting yours. Yours predates it and is the better of the two — it fails the collision at the cause in the builder, and it adds no new env var to the shipping scripts. So I'm closing #43 in favor of this one.

Two optional, non-blocking notes:

  1. On a collision, the validator still prints a derivative bundle copy of "…" differs from the source line alongside the clear collision error. Consider suppressing the per-file drift/orphan lines when skillNameCollisions() is non-empty, so the root cause leads. (That's the one nicety #43 had.)
  2. The cwd: root option in the test run() helper is a no-op — the scripts self-locate via import.meta.url, not cwd. Harmless, just slightly misleading.

Neither blocks. Approving — nice work closing the gap end-to-end. 🚀

@atamanvega
atamanvega merged commit 73b97fb into theam:main Aug 19, 2026
1 check failed
atamanvega pushed a commit that referenced this pull request Aug 20, 2026
…rift it causes (#53)

Both follow-ups from the #42 review, taken from the implementation in the closed
#43 rather than reinvented.

When two skill sources share a name the losing copy also reads as drift, so the
per-file missing/drifted/orphan lines are derivative noise on top of the real
cause. They are now suppressed while any collision is present. The manifest pair
is orthogonal to a name collision, so that check stays unconditional — a tree
with both problems still reports both.

Also drops `cwd: root` from the test `run()` helper. It never did anything: the
sandbox carries its own `scripts/`, and each script self-locates through
`import.meta.url`, so invoking the copy is what targets the copy.

Not adopted from #43: the `DEVKIT_ROOT` env var. Its absence was one of the two
reasons given for keeping #42, so the shipping scripts stay free of a test-only
knob.

Two tests: a collision with divergent contents reports the cause and nothing
derived from it, and a tree with both a collision and manifest drift still
reports both.

Refs #41, #42, #43.

Co-authored-by: Carlos Santana <1540596+ophiocus@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extend the bundle drift-guard to the generated manifests (portable plugin.json) + tooling nits

2 participants