Skip to content

feat: add partials for shared, inlined text fragments across skills - #21

Merged
steve-calvert-glean merged 1 commit into
mainfrom
feat/partials
Jul 28, 2026
Merged

feat: add partials for shared, inlined text fragments across skills#21
steve-calvert-glean merged 1 commit into
mainfrom
feat/partials

Conversation

@steve-calvert-glean

Copy link
Copy Markdown
Contributor

Summary

Skills that need the same procedural prose repeated across many files (a consistent auth flow — "try OAuth first, fall back to a token, or ask for the user's email and look up credentials" — is the motivating case) had no reuse mechanism. This adds a partials/ directory of reusable {{> name}} text fragments, resolved once at pluginpack build time.

  • source.partials config option: a project-level directory (not scoped to one source plugin), mirroring source.skills's shape.
  • Real mustache library (new production dependency, @types/mustache in dev), not a hand-rolled parser — gets missing-partial-renders-empty and clean standalone-line whitespace handling for free, both verified directly against the Mustache spec's own test suite before deciding.
  • View is always {} — no config/env data is ever exposed to interpolation. An earlier, broader "dynamic value injection" idea (secrets, env-specific config) was explicitly scoped out: build output is committed to a repo the target host installs from, and baking secret-like values into that committed output would be a real security footgun. This is pure text reuse, full stop.
  • Hooks into collectPluginFiles (render.ts) and withRootFiles (targets/engine.ts) — covers skills/agents/commands/rules, additionalFiles, and a target's rootFiles.
  • Partials may reference other partials (native recursive composition); a circular reference is the one thing pluginpack itself catches, since Mustache.js has no protection against it — via dependency-graph cycle detection at load time, before any render call.
  • Known, documented limitation: because substitution is real Mustache rendering, other {{...}}-looking text in the same file (e.g. a skill documenting Handlebars/Jinja/Mustache itself) is also processed against the empty view and typically disappears. Considered and rejected a narrower hand-rolled {{> }}-only parser to avoid this — reproducing Mustache's standalone-line whitespace handling and nested-partial resolution by hand means reinventing battle-tested infra a mature library already solves correctly.

Test plan

  • npm run typecheck
  • npm run lint
  • npm test (92/92 — 12 new tests: happy path, missing partial, nested composition, circular reference, multi-marker, cross-plugin sharing, no-op backward compat, byte-safety, duplicate name, missing directory, rootFiles, documented collision behavior)
  • npm run audit (production-scoped) — 0 vulnerabilities, mustache itself is clean
  • npm run build && node dist/cli.js docs --check
  • npm run docs (includes markdown-code check)
  • npm pack --dry-run
  • Manual smoke test: built a fixture with source.partials configured against claude + cursor targets, confirmed the substituted auth-flow text reads naturally in context in both, and pluginpack validate passes for both

Skills that need the same procedural prose repeated across many files
(a consistent auth flow is the motivating case: "try OAuth first, fall
back to a token, or ask for the user's email and look up credentials")
had no reuse mechanism — every SKILL.md that needed it had to
duplicate the prose by hand.

- New source.partials config option: a project-level directory of
  reusable {{> name}} text fragments, shared across every source
  plugin (not scoped to one), resolved once at build time via the
  real `mustache` library.
- Substitution hooks into collectPluginFiles (render.ts) and
  withRootFiles (targets/engine.ts) — every .md/.mdc/.markdown/.txt
  file pluginpack emits participates, including additionalFiles
  (merged into the same map before collectPluginFiles sees it) and a
  target's rootFiles.
- View is always {} — no config/env data is ever exposed to
  interpolation. An earlier, broader "dynamic value injection" idea
  (secrets, env-specific config) was considered and dropped: build
  output is committed to a repo the target host installs from, and
  baking secret-like values into that committed output would be a
  real security footgun. This feature is pure text reuse, full stop.
- Missing partial -> renders as "" (Mustache's own documented
  behavior), not a build error — a standalone {{> name}} line
  disappears with no orphaned blank line, matching how the library
  already handles this. Partials may reference other partials
  (native recursive composition); a circular reference is the one
  thing pluginpack itself must catch, since Mustache.js has no
  protection against it ("just avoid infinite loops") — checked via a
  dependency-graph cycle detection at load time, before any render
  call happens.
- Known, documented limitation: because substitution is real Mustache
  rendering, any other {{...}}-looking text in the same file (e.g. a
  skill documenting Handlebars/Angular/Go templates/Jinja/Mustache
  itself) is also processed against the empty view and typically
  disappears. A narrower, hand-rolled {{> }}-only parser was
  considered specifically to avoid this, but reproducing Mustache's
  standalone-line whitespace handling and nested-partial resolution by
  hand means reinventing battle-tested, easy-to-get-subtly-wrong
  infrastructure a mature library already solves correctly — the
  tradeoff was made deliberately in favor of the real library.

12 new tests in tests/core.test.ts covering the happy path, missing
partial, nested composition, circular reference, multi-marker files,
cross-plugin sharing, backward compatibility (no-op when
unconfigured), byte-safety (non-UTF8 content untouched when no {{
marker present), duplicate partial names, a missing partials
directory, rootFiles substitution, and the documented collision
behavior (locked in as a regression guard, not something to "fix").
@steve-calvert-glean steve-calvert-glean added the enhancement New feature or request label Jul 28, 2026
@steve-calvert-glean
steve-calvert-glean merged commit 975c2be into main Jul 28, 2026
1 check passed
@steve-calvert-glean
steve-calvert-glean deleted the feat/partials branch July 28, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant