v0.7.0
Added
withRemoteSkills(...) — non-Composer skill sources
Declarative consumption of three real-world shapes that don't fit Composer's vendor model:
return BoostConfig::configure()
->withAgents([Agent::CLAUDE_CODE])
->withRemoteSkills([
// Bundle mode — fetches the named `.skill` release asset, unzips.
RemoteSkillSource::githubBundle('peterfox/agent-skills', 'v1.2.0', [
'composer-upgrade',
'phpstan-developer',
]),
// Path mode — fetches the repo tarball at the given ref, extracts named subdirs.
// `'.'` covers whole-repo-is-one-skill layouts (blader/humanizer style).
RemoteSkillSource::githubPath('mattpocock/skills', 'main', [
'grill-with-docs' => 'skills/engineering/grill-with-docs',
]),
]);Resolved on the next composer install / update through the existing BoostAutoSync hook — no separate command, no separate cache-warm step. First sync hits the network; later syncs are offline-fast (cache lives at <project>/.boost-remote-cache/, auto-added to the managed .gitignore). Removing an entry prunes its agent-dir output on next sync; removing an entire source prunes every skill it last contributed.
Set BOOST_GITHUB_TOKEN to lift anonymous GitHub access from 60 to 5000 requests/hour. CI runs that resolve withRemoteSkills(...) cold should always export the token. BOOST_REMOTE_STRICT=1 escalates any remote-source failure (network unreachable, malformed archive, name-mismatch) to an aborting error; default is warn-and-skip. boost doctor lists every declared remote source, flags moving refs ('main', 'latest', branch names) with a ⚠, and reports per-skill cache presence — all offline.
boost sync --check is network-free and side-effect-free: cold-cache sources are surfaced as would-fetch advisories in SyncResult::errors, never touching the network or writing to the cache.
SkillRenderer plugin contract (@experimental)
use SanderMuller\BoostCore\Config\BoostConfig;
use SanderMuller\ProjectBoostLaravel\Rendering\BladeRenderer;
return BoostConfig::configure()
->withAgents([Agent::CLAUDE_CODE])
->withSkillRenderers([new BladeRenderer]);Plugin seam for rendering template-flavored skill bodies (SKILL.blade.php, SKILL.twig, …) before per-agent fan-out. The dispatcher matches longest-extension-first (so .blade.php beats .php when both are claimed); the implicit PassthroughRenderer always handles .md and is re-appended after any withDisabledRenderers([FQCN]) deny-list. Render failures default to warn-and-skip (recorded in SyncResult::errors); BOOST_RENDER_STRICT=1 escalates the first failure to an aborting SkillRenderException. The flag is separate from BOOST_REMOTE_STRICT so a project can keep renders lenient (a single broken Blade skill should not abort CI) while making remote-source resolution strict, or vice versa.
The contract is @experimental — pin to an exact boost-core version if building against it. Lock-in happens after a second non-trivial consumer from a different problem domain validates the shape, mirroring the FileEmitter plugin lock-in criteria. Reference consumer: sandermuller/project-boost-laravel ships a BladeRenderer that delegates to laravel/boost's RendersBladeGuidelines trait, so .ai/<pkg>/skill/<name>/SKILL.blade.php files render with the $assist = GuidelineAssist runtime context they expect.
GuidelineLoader reuses the same dispatcher — a BladeRenderer registered for skills also discovers Blade guidelines in .ai/guidelines/. Files whose extension no registered renderer claims are silently skipped, bit-identical to pre-renderer boost-core when no renderer is registered.
Caller-controlled vendor injection on SyncEngine::sync()
$engine->sync(
projectRoot: __DIR__,
injectedVendorSkills: ['laravel/boost' => $skills],
extraSkillRenderers: [new BladeRenderer],
injectedVendorGuidelines: ['laravel/boost' => $guidelines],
);Three new optional parameters for wrapper packages whose source layout VendorScanner cannot reach (laravel/boost's .ai/<pkg>/... is the motivating case — sandermuller/project-boost-laravel uses this seam). Tag-filtered and collision-detected identically to scanned vendors. Same-vendor name collisions between injected and scanned skills throw SkillSourceCollisionException, caught in SyncEngine::sync and converted to a SyncResult::errors entry (lenient) or rethrown (strict). All three default to []; existing call sites are unchanged.
boost where — skill origin tracing
vendor/bin/boost whereLists every skill that would land in agent dirs, grouped by source:
.ai/skills/ (host)— host-authored skills, with(shadows <vendor>)on overrides.<vendor/package>— Composer-allowlisted vendors publishing viaresources/boost/skills/.<vendor/package>from aRemoteSkillSource— non-Composer sources declared viawithRemoteSkills(...).
Same resolution pipeline as boost sync --check (tag-filtered, collision-resolved). Caller-injected vendor skills (the wrapper pattern, e.g. project-boost-laravel) are NOT visible — those are runtime-only inputs to SyncEngine::sync() and the wrapper package owns its own inspection surface.
Complements boost tags (lists every tag and which would unlock filtered skills) and boost doctor (cache freshness + remote source diagnostics).
SyncResult::renderDeleteAttribution(): ?string
Canonical attribution renderer for destructive deletes. Returns null when nothing was deleted (or in check-mode results), otherwise a multi-line string naming the three possible causes (tag-filter, removed withRemoteSkills entry, stale-source prune) followed by the deleted paths.
if ($attribution = $result->renderDeleteAttribution()) {
$this->warn($attribution); // Laravel artisan
// or $io->warning($attribution); // Symfony console
}Single source of truth for the attribution wording — boost-core's own SyncCommand and wrapper commands (e.g. project-boost-laravel's artisan project-boost:sync, future custom CLIs) use it so the operator-visible delete audit signal stays identical across invocation surfaces. Addresses a gap where wrapper commands that render their own output from $result->writes saw the per-line deleted <path> action but missed the cause-attribution that vendor/bin/boost sync produced. The helper closes the gap symmetrically.
skill-origin-tracing bundled skill
A resources/boost/skills/skill-origin-tracing.md skill that triggers when downstream agents see questions like "why is skill X present", "why is skill Y missing", "where does skill Z come from", or "did host shadow X". Routes them to boost where (and boost tags for tag-filtered cases) instead of grepping vendor/.
Tag enum gains Laravel-ecosystem cases
Livewire, Volt, Inertia, Filament, Flux, Pest, Tailwind. Surfaces the tag vocabulary laravel/boost's bundled skills declare, so withTags(Tag::Livewire, …) autocompletes properly. Non-authoritative — string fallback continues to work for any vocabulary the enum doesn't cover.
Fixed
GuidelineLoaderdiscovers.blade.php(and any renderer-registered extension) in.ai/guidelines/. Previously globbed*.mdonly — host-authored Blade guidelines were silently dropped, causing content loss inCLAUDE.mdfor users with templated guidelines.FileWriterrefuses to follow user-placed symlinks. A sync write under.claude/skills/<name>/that would resolve through a user-placed symlink (e.g..claude/skills/<name>→../../.ai/skills/<name>/) now bails withWriteAction::SKIPPED_SYMLINKand surfaces in the summary asskipped-symlink=N. Preserves the "live symlinks owned by consumer" contractSyncEngine::pruneDeadSymlinks()documented but the write path did not honor.boost sync --checkis network-free and writes-free.RemoteSkillSyncCoordinator::ingestIntoVendorMap()accepts a$checkOnlyflag; sources missing from the offline cache are excluded from the ingest call and surfaced as awould-fetchadvisory inSyncResult::errors. Restores the dry-run-purity invariant that pre-companion-refactor consumers relied on.- Host-vs-vendor skill shadowing is surfaced. A host
.ai/skills/<name>/that shadows an allowlisted-vendor skill of the same name now produces a<name> shadows <vendor>line inSyncCommand's output. Plumbed via a newSyncResult::$hostShadowsfield and a&array $shadowsout-param onSkillResolver::resolve(). Existing host-wins precedence andCollidingSkillsExceptionsemantics unchanged. boost synclists each deleted path inline + attributes the cause. A delete event (a previously-installed agent-dir skill pruned because its source skill is no longer tag-eligible after awithTags()change, or a removedwithRemoteSkillsentry, or a stale-source prune) previously surfaced only as a count in the success summary.SyncCommandnow emits a warning naming the three possible causes followed by the list of relative paths wheneverdeleted > 0. Behavior unchanged in--checkmode (where would-delete was already listed).TagReporterpasses the renderer dispatcher.boost tagsandboost doctornow discover renderer-claimed extensions (e.g..blade.phpwith a registeredBladeRenderer), matching the file setboost syncwould emit.RemoteSkillIngesterhonorsBOOST_RENDER_STRICT. Render failures inside the per-skill loop now escalate viaSkillRenderExceptionwhen the flag is set, mirroringSkillLoader's strict path.locateSkillFileprefers the canonicalSKILL.<ext>across all globs. A remote-skill cache slot containing bothREADME.mdandSKILL.blade.phpnow correctly loads the latter; previously the first-glob-with-any-match silently ingested README as the skill body.RemoteSkillIngestersurfaces same-source different-version skill collisions. TwowithRemoteSkillsentries pointing at the same repo at different versions both listing the same skill name now produce a clear error (lenient:SyncResult::errors; strict:RemoteFetchException).RemoteSkillSyncCoordinatordetects collisions with scanned/injected vendor maps. A remote source sharing a vendor key with an already-populated entry throwsSkillSourceCollisionException, caught bySyncEngine::syncand converted to aSyncResult::errorsentry.extraSkillRenderersno longer shadow user-registered renderers. Extras are inserted between user renderers and the trailing implicitPassthroughRenderer. Final order:[...user, ...extras, Passthrough]. With first-match-wins dispatch, the user'sboost.phpregistry stays authoritative.TarballExtractorarchive-path prefix length corrected. The strip-length for in-archive entry names now includes thephar://URL prefix thatPharDataiteration prepends. (Linux CI rendered this as a PharData-vs-tarregression and we pivoted to systemtar -xzfvia Symfony Process for portability.)BundleExtractor::assertNotSymlinkfails closed on unreadable external attributes. A crafted ZIP that suppresses attribute readability would previously bypass the symlink check; now throwsRemoteExtractException::SYMLINK.SkillLoaderfalls back togetPathname()whengetRealPath()returns false (broken symlinks, open_basedir restrictions, file disappears between Finder enumeration and the realpath call). Previously hit aTypeErroron the false case.BoostConfigBuilderdistinguishes implicit vs explicitPassthroughRendererby object identity. A user passingnew PassthroughRenderer()viawithSkillRenderersis now treated as a regular renderer for conflict detection.- Multi-extension naming fallback fix.
SKILL.blade.phpwith noname:frontmatter now resolves to skill nameSKILL(previously would have beenSKILL.blade). The dispatcher now returns aMatchedRenderer { renderer, extension }tuple so the loader can strip the full matched extension.
Changed
SkillLoader::load()accepts aSkillRendererDispatcherparameter (default = passthrough-only). External direct callers that construct aSkillLoaderare unaffected unless they want to register a non-default renderer.SyncEnginebuilds the per-sync dispatcher fromBoostConfig::skillRenderersafter config-load.RemoteSkillIngester::ingest()accepts the same dispatcher, so remote.blade.phpskills render through the registered renderer too. The internalloadOnewas generalized from a hard-codedSKILL.mdlookup to walk the dispatcher's claimed extensions, falling back to.md.SyncEngine's injection-merge logic extracted toInjectedVendorMergerto keep the engine's class-level cognitive complexity tractable.
Added (internal — public exception class)
SkillSourceCollisionException(src/Sync/SkillSourceCollisionException.php). Thrown byInjectedVendorMergerandRemoteSkillSyncCoordinatorwhen caller-config (injected vendor map / remote source declaration) would silently overwrite an existing entry under the same vendor key. Caught inSyncEngine::sync()and converted to aSyncResultwith the message as an error — consumers that wrapsync()and expect it to never throw on user-config issues keep working. Distinct fromCollidingSkillsException(which models cross-vendor name collisions detected bySkillResolver).
Upgrade notes
No migration required from 0.6.x. The three additive surfaces (withRemoteSkills, SkillRenderer, SyncEngine::sync injection params) are opt-in; existing projects keep working unchanged. See UPGRADING.md for adoption notes.
@experimental APIs (SkillRenderer, the three injection params): pin to an exact boost-core version ("sandermuller/boost-core": "0.7.0" rather than "^0.7.0") if your project depends on the precise shape. The shape survived the entire rc cycle without churn, but lock-in waits for a second non-trivial consumer from a different problem domain.
Companion package status
sandermuller/project-boost-laravel — the reference Laravel companion that consumes laravel/boost-bundled skills via the injection seam, ships its own BladeRenderer, and exposes a Laravel artisan project-boost:sync command — accepts 0.7.0 stable on its existing ^0.7.0-rc1 constraint without composer.json changes. Cut a composer update sandermuller/boost-core to pick up stable.
Full Changelog: 0.6.2...0.7.0