Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Boost Core Laravel Package

sandermuller/boost-core

View on GitHub
Deep Wiki
Context7
1.4.2

A compatibility patch. sandermuller/boost-core pinned sebastian/diff to ^7.0, which made it a hard blocker for any consumer that wanted PHPUnit 13 — and therefore Pest 5. The pin reached those projects three ways: a direct require-dev, and transitively through sandermuller/boost-skills and sandermuller/project-boost-laravel.

Changed

  • sebastian/diff constraint widened to ^7.0 || ^8.0 || ^9.0. PHPUnit 13.2 requires sebastian/diff ^9.0, so the old pin left Composer with nothing to resolve. Surfaced by a downstream Pest 4 → 5 upgrade.

Internal

  • boost where --diff=<name> now builds its unified diff with StrictUnifiedDiffOutputBuilder instead of UnifiedDiffOutputBuilder, which sebastian/diff 9.0 removed — widening the constraint alone would not have been enough. The --- vendor: / +++ host: header lines come from the builder's fromFile / toFile options rather than being written by hand, so the text of the diff is unchanged; the only visible difference is that those two lines are no longer dimmed. Only options present in both diff 7 and diff 9 are passed, so behaviour is identical on every supported major.
  • CI now resolves dependencies on branches other than main. Composer guesses the root package version from the checked-out ref, so on a feature branch it resolved as dev-<sha> and the extra.branch-alias entry never applied. The self-referential sandermuller/boost-skills dev dependency requires sandermuller/boost-core, so nothing satisfied it and every workflow died at composer install. The four PHP workflows now set COMPOSER_ROOT_VERSION: dev-main, and the alias itself — still pointing at 1.2.x-dev — was bumped to 1.4.x-dev.
  • CurlHttpTransport::get() rejects an empty URL up front with a network-unreachable RemoteFetchException instead of handing '' to cURL, and HttpTransport::get() documents that failure mode. Covered by new tests.
  • Style-only rewrites in SkillAssetCollector and GuidanceWriter for the current Rector rule set.

Upgrading

Nothing to do. No public API change, no configuration change, and boost where --diff output is unchanged. sandermuller/boost-skills (^1.4) and sandermuller/project-boost-laravel (^1.0) already accept this release.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.4.1...1.4.2

1.4.1

A single-bug patch, surfaced by production dogfood: an advisory warning was travelling down the fatal channel, so a fully successful sync reported errors. Any project with a Cursor-, Amp-, Kiro- or Junie-targeted command using argument placeholders was affected.

Fixed

  • Command-transpile advisory warnings no longer fail the sync. Cursor/Amp's "no placeholder syntax" warning and Kiro/Junie's named-argument warnings were pushed into SyncResult::errors — the fatal channel — rather than SyncResult::diagnostics. Two consequences for any project with a command using $ARGUMENTS/$N/$name on one of those agents. First, boost sync, boost where and project-boost:sync exited non-zero, which broke composer install/composer update wherever project-boost-laravel wires the sync into post-install-cmd. Second, the engine reads that same channel as a partial-sync signal, so the clean-slate stale pass, the manifest-orphan reap and the .boost/manifest.json write were skipped on every sync — those projects never built an ownership record. The warnings now route through SyncResult::diagnostics, matching Diagnostic's documented contract and every other lenient warning in the pipeline. They still render under the operator-visible "Diagnostics" section of boost sync and boost where; the exit code and the error-state gate are what change.

Docs

  • Corrected the command-arguments skill and AgentTarget::planCommands()'s docblock, which both described the old (incorrect) routing through SyncResult::errors.
  • README: propagated skill dependencies through the laravel/boost comparison table, the tag-filtering rule (which contradicted the dependency-rescue section) and the boost validate / boost doctor CLI rows.

Internal

  • Regression coverage on three surfaces: the engine (no errors, command body written verbatim, warning present in diagnostics), the second-order manifest write, and the CLI exit code for both boost sync and boost sync --check.
  • Removed a duplicated fanOut() docblock that had to be hand-mirrored on every signature change.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.4.0...1.4.1

1.4.0

Skills can now declare hard dependencies on other skills. Sync guarantees the dependency ships whenever the dependent does — including pulling a dependency back in when tag filtering would otherwise have dropped it. Designed against the real cross-reference graph of sandermuller/boost-skills, which dogfoods the feature.

Added

  • Skill dependencies via metadata.boost-requires. A skill lists the skills it hands off to, as bare names in the Agent Skills standard's metadata map (space-delimited, mirroring boost-tags):

    ---
    name: interview
    metadata:
      boost-requires: "write-spec"
    ---
    

    Whenever the skill ships, every required name ships too. A dependency that tag filtering would drop is rescued — pulled in despite the tag mismatch, transitively, and surfaced as an INFO diagnostic so the pull-in is always visible. The semantics stay predictable: names bind to whatever provider wins resolution (a host .ai/skills/ override satisfies the dependency), withExcludedSkills() always wins over rescue (warning, never an override), a dependency that exists nowhere warns without failing the sync, cycles simply co-ship, and candidate collisions follow the existing resolver rules (CollidingSkillsException unless --force; same-provider duplicates always throw). sync --check previews rescues identically to a real sync.

  • boost validate checks dependencies over the same resolved set a sync ships: a malformed boost-requires is an error (fails --strict — unlike sync, which warns and keeps the skill), an unsatisfied dependency is a warning, and a require that crosses a tag boundary is an info-level nudge under -v to confirm the hard dependency is intentional.

  • boost doctor gained a "Skill dependencies" section reporting unsatisfied or excluded dependencies; silent when no installed skill declares any.

  • Wrapper-integration surface: the BoostRequires parse seam (parse() / declaresRequires(), mirroring BoostTags — names are not case-folded, and malformed values do not fail closed) and appended Skill::$requires / Skill::$requiresValid constructor properties, per the frozen-constructor append rule. Injected vendor skills participate in rescue on both sides.

Docs

  • New README "Skill dependencies" section, including authoring guidance: declare only hard hand-offs — conditional references ("delegate where synced") and routing notes ("NOT for X — use Y") stay undeclared, or rescue drags scoped tooling into projects that opted out via tags.

Internal

  • New SkillDependencyResolver (the rescue fixpoint) with a full unit matrix — transitive chains, cycles, collision and duplicate handling, exclusion fallback, aggregated warnings — plus end-to-end coverage for rescue, --check parity, pruner interaction, and injected + remote skill sources. The tag filter now returns dropped skills grouped by reason so rescue candidates stay available through the whole pipeline.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.3.1...1.4.0

1.3.1

Fixed

  • boost sync --check now previews the deletions a real sync performs. Two destructive passes were invisible to --check: the manifest-orphan reap (a de-selected agent's boost-owned guidance file, or a dormant emitter's output) and the dead-symlink prune. A --check CI gate could report no drift while the next real sync deleted a tracked file. Both now emit WOULD_DELETE under --check, so the dry run predicts what a real sync removes; the ownership manifest is still never written on a check run. One narrow residual — a dead symlink occupying a path the sync also re-emits — is documented in the code.

  • The remote tarball extractor validates every entry before writing a byte. TarballExtractor now rejects path-traversal (..), absolute-path, and symlink members, and enforces the per-file / total / entry-count caps, from the archive listing before extraction runs — closing a window where those checks only ran after tar had already extracted to a staging directory. Enumeration behaves identically under GNU tar and bsdtar, and a post-extraction scan of the staged tree remains as defense-in-depth. Reachable only via path-mode GitHub tarball fetches; behavior for well-formed archives is unchanged.

Docs

  • Documented boost new (scaffold a skill or guideline) and boost scan (re-run the vendor allowlist picker) in the README CLI reference — both are live, non-hidden commands that were missing from the table.

Internal

  • Added committed adversarial tarball fixtures and direct tests for the traversal, absolute-path, symlink, and total-size guards, plus --check parity tests for the orphan-reap and dead-symlink paths.
  • Pest's .phpunit.cache/ result-cache directory is now gitignored.
  • Resolved a Rector/Pint disagreement over a docblock-only import in SkillAssetCollector.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.3.0...1.3.1

1.3.0

Skills stop being single-file. A nested skill's companion files — scripts/, references/, examples/ — now emit beside the rendered SKILL.md in every configured agent's skill directory, so a skill that ships a runnable script no longer has to embed it in the skill body and have the agent materialize it at runtime.

Added

  • Skill asset-sibling emission. Every non-SKILL.* file under a nested skill's <name>/ directory (any depth; hidden and backup/editor-temp files excluded) is collected at load time and emitted verbatim to <skillsDir>/<name>/<relativePath> for each configured agent. Applies uniformly to host-authored .ai/skills/, Composer vendor, wrapper-injected, and remote skills — remote bundles' scripts/ siblings, previously extracted into the cache but never fanned out, now emit too. Assets ride the existing sync manifest, clean-slate stale cleanup (removing an asset — or the whole skill — reaps the emitted copies on the next sync), and tag-gating (a filtered skill ships no assets). Flat-layout skills (skills/<name>.md) have no directory to own and collect nothing.

    New [@api](https://github.com/api) value type SkillAsset (relativePath, contents) lets a wrapper attach companion files to an injected skill; Skill gains an appended-with-default assets constructor property per its frozen-surface append rule. A wrapper injecting skills with assets must also claim each asset's emit path in BoostWrapperContract::injectedEmitPaths(), or a bare-CLI sync reaps the copies as stale — documented on the contract and in PUBLIC_API.md.

    Emitted assets are plain file writes with no executable bit — invoke scripts via their interpreter (node .claude/skills/<name>/scripts/run.mjs), not directly.

Fixed

  • Legacy flat-sibling prune no longer fires on asset writes. The pre-existing cleanup that deletes an obsolete flat <name>.md after writing <name>/SKILL.md keyed on the path shape, so an asset emitted at a deeper …/examples/SKILL.md would have deleted the skill's own just-written examples.md sibling. The prune is now scoped to the skill's entry write explicitly. (Caught in review before this feature ever shipped — no released version was affected.)

Upgrade note: none — a strict minor over 1.2.x. Skills without companion files sync byte-identically to before. Skill packages that adopt asset siblings should floor their boost-core constraint at ^1.3. No breaking change to configuration, CLI, hooks, or the 1.0 [@api](https://github.com/api) surface.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.2.1...1.3.0

1.2.1

Added

  • Paired visible-default conventions tokens. A bare <!--boost:conv …--> token resolves only under boost-core. An engine with no resolver — notably laravel/boost, which installs a package's SKILL.md and preserves HTML comments verbatim — leaves it inert, so an inline token reads as a word gap and its fallback stays hidden inside the comment. The paired form closes that gap:

    Run <!--boost:conv path="testing.runner" mode="inline"-->Pest<!--boost:conv:end--> to verify.
    

    boost-core replaces the whole span with the resolved value; the visible default doubles as the inline fallback (an explicit fallback= still wins). A resolver-less engine leaves both comments inert, so the visible default reads as ordinary prose — Run Pest to verify. — no gap. Resolution order is unchanged (declared → schema default → fallback). The ```boost:conv fence takes the same <!--boost:conv:end--> marker and buffers its body as one block, so a multi-line span resolves whole; paired spans resolve before bare tokens so the open comment is never consumed as a stray unpaired token; inline-code and plain-fence examples stay literal; an orphan end marker is inert and keeps the Project Conventions block.

    The contract addition is documented in PUBLIC_API.md (Textual / wire formats) and docs/conventions.md; a new authoring skill, conventions-token-migration, guides vendor-skill authors migrating bare tokens to the paired form.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.2.0...1.2.1

1.2.0

Added

  • Paired visible-default conventions tokens. A bare <!--boost:conv …--> token resolves only under boost-core. An engine with no resolver — notably laravel/boost, which installs a package's SKILL.md and preserves HTML comments verbatim — leaves it inert, so an inline token reads as a word gap and its fallback stays hidden inside the comment. The paired form closes that gap:

    Run <!--boost:conv path="testing.runner" mode="inline"-->Pest<!--boost:conv:end--> to verify.
    

    boost-core replaces the whole span with the resolved value; the visible default doubles as the inline fallback (an explicit fallback= still wins). A resolver-less engine leaves both comments inert, so the visible default reads as ordinary prose — Run Pest to verify. — no gap. Resolution order is unchanged (declared → schema default → fallback). The ```boost:conv fence takes the same <!--boost:conv:end--> marker and buffers its body as one block, so a multi-line span resolves whole; paired spans resolve before bare tokens so the open comment is never consumed as a stray unpaired token; inline-code and plain-fence examples stay literal; an orphan end marker is inert and keeps the Project Conventions block.

    The contract addition is documented in PUBLIC_API.md (Textual / wire formats) and docs/conventions.md; a new authoring skill, conventions-token-migration, guides vendor-skill authors migrating bare tokens to the paired form.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.1.2...1.2.0

1.1.2

Fixed

  • Dead symlinks are now counted in deleted=N. boost sync prunes broken legacy symlinks left by older installs, but previously discarded the removal count — those deletions were invisible in the summary line. They are now recorded as deletions and listed in the delete attribution. The attribution message is also corrected: it previously claimed every deleted path had "a source no longer eligible", which was wrong for a dead-symlink prune. The message now names that cause alongside the source-ineligible ones. (#147)

  • boost doctor version-gates the project-boost:reconcile offer. The foreign-seeded guidance warning now names php artisan project-boost:reconcile — the guided takeover introduced in sandermuller/project-boost-laravel 1.1.0 — only when the installed wrapper is actually ≥ 1.1.0. On an older wrapper that command does not exist; project-boost:sync is the correct alternative and is shown instead. Dev or unparseable wrapper versions also fall back conservatively to avoid pointing at a possibly-absent command.

Changed

  • Symfony dependency constraints widened to ^6.4||^7.0||^8.0 (was ^7.0||^8.0). boost-core now installs alongside symfony 6.4 LTS consumers — for example rectorphp/rector-src, which pins symfony/console ^6.4. Runtime code uses only basic Console/Finder/Process/Yaml APIs present in symfony 6.4; the dev toolchain (pest/paratest) continues to require symfony ≥ 7.4 and CI runs on 7.4/8.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.1.1...1.1.2

1.1.1

A patch over 1.1.0. The new boost doctor coexistence section pointed a foreign-seeded project at php artisan project-boost:reconcile — a guided-takeover command the project-boost-laravel wrapper does not yet provide — so a wrapper user with a laravel/boost-seeded CLAUDE.md was sent to a command they do not have.

Fixed

  • boost doctor steers foreign-seeded guidance at project-boost:sync, not an unbuilt command. The coexistence section now points at php artisan project-boost:sync — which exists and re-derives laravel/boost's bundled guidelines into the assembly safely — and calls out that a direct hand-edit of the seeded file is the real at-risk content to move into .ai/guidelines/ first. The named guided-takeover offer can return once the wrapper ships it.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.1.0...1.1.1

1.1.0

Coexistence transparency for laravel/boost projects. boost-core now explains itself at the laravel/boost ↔ boost-core seam — the source of the most-reported adoption confusion — and guards against silently overwriting guidance another tool authored. Additive over 1.0.0; no public-API change.

Added

  • boost install explains its skipped pickers. When no installed package publishes boost-core skills/guidelines (a resources/boost/skillsresources/boost/guidelines directory), or no selected vendor publishes tags, the vendor/tag pickers used to skip silently — which read as "install ignored my vendors/tags." They now print a short NOTE explaining why the step was skipped, so a re-run's behavior is legible.
  • boost install notes laravel/boost coexistence. When laravel/boost is installed, install explains that its bundled skills + guidelines ship through the project-boost-laravel wrapper (php artisan project-boost:sync), not boost-core's vendor allowlist — so it never appears in the picker. The note branches on wrapper presence: without the wrapper it steers you to install it first, rather than at a command that does not exist yet.
  • boost doctor reports the laravel/boost coexistence state. A new "laravel/boost coexistence" section detects laravel/boost with or without the project-boost-laravel wrapper, states the division of labor (laravel/boost owns the MCP server + Laravel docs; boost-core assembles the guidance files and fans skills out to every agent), and steers you to php artisan project-boost:sync rather than a bare vendor/bin/boost sync (which composes a thinner set and would overwrite laravel/boost's guidance). It classifies each guidance file as boost-owned or foreign-seeded — recognizing a file another tool authored even before boost-core has ever synced the project — and offers php artisan project-boost:reconcile to capture that content safely.
  • Overwrite protection for guidance files. boost-core wholesale-owns CLAUDE.md / AGENTS.md / GEMINI.md / the Copilot file. When a sync is about to wholesale-replace a non-empty guidance file boost-core does not own (one a foreign or operator writer authored, whose content differs from the assembly), it now emits a WARNING naming the file before overwriting it. The overwrite still proceeds — behavior is unchanged — but the takeover is no longer silent. Content that reaches boost via .ai/guidelines/ (or a wrapper's injected guidelines) is preserved; anything else in the file is replaced.

Internal

  • A new advisory-layer CoexistenceReporter is the single place that recognizes another tool's guidance marker and names laravel/boost; the sync engine stays tool-agnostic. The guidance-overwrite warning is content-agnostic (it protects against any foreign writer, not just laravel/boost).

Upgrade note: none — a drop-in MINOR over 1.0.0. All changes are additive: new advisory output in boost install / boost doctor, and a new warning diagnostic on a guidance-file takeover that already happened silently before. No configuration, CLI, hook, or plugin-contract change.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/1.0.0...1.1.0

1.0.0

The first stable release. boost-core's public surface is now under Semantic Versioning: from here on, breaking changes to it land only in a MAJOR bump, and MINOR/PATCH releases stay backward-compatible. 1.0.0 freezes exactly the surface 0.23.3 shipped — there is no behavioral change to the documented API. The committed surface is enumerated in PUBLIC_API.md.

The committed surface

The SemVer promise covers the public surface only:

  • Config authoring APIBoostConfig::configure(), the BoostConfigBuilder with*() methods, the Agent/Tag enums, and RemoteSkillSource.
  • CLI — the bin/boost command names, their documented options, and exit codes (0 ok, 1 failure, 2 usage).
  • Composer hooksBoostAutoSync::run / runWithSummary; new parameters are always optional-with-default.
  • Plugin contractsFileEmitter, SkillRenderer, BoostWrapperContract and their DTOs, locked at 1.0 (parameterless constructors; a contract method signature change is a major bump).

Everything marked [@internal](https://github.com/internal) (the whole engine) and on-disk regenerable state (the sync manifest, remote-skill ledger, user-scope manifests, the .boost/.config/boost/ runtime dir, cache sentinels) is explicitly NOT covered and may change in any release.

Fixed

A pre-1.0 hardening review of the migration and symlink-cleanup paths — the behavior the 1.0 freeze locks — surfaced a set of edge cases, all fixed here. No public-API change; these are correctness fixes to internal behavior.

  • boost doctor no longer hides genuinely-unrenderable sources on wrapper projects. The wrapper-aware downgrade (a .blade.php source has no bare-CLI renderer but the wrapper Blade-renders it) classified on a substring of the skip message — and every skip message embeds the advisory example a BladeRenderer for `.blade.php` . So a genuinely-dropped .rst / .mdx source was downgraded to the wrapper note too, suppressing the real data-loss warning. Classification now keys off the actual source extension.
  • A de-selected agent's guidance file is now reaped on the first post-migration sync. The exemption that keeps a configured agent's unchanged guidance file (CLAUDE.md / AGENTS.md / …) from being mistaken for a stale managed file was scoped to all known agents, so a guidance file for an agent no longer configured lingered indefinitely (the manifest-gated reaper has no entry for it on the first sync after migrating off the pre-0.12 layout). The exemption is now scoped to the guidance files actually emitted this sync, so a dropped agent's guidance is cleaned up.
  • The dead-symlink prune never follows a symlinked agent parent directory. The guard against descending a symlinked directory only covered the managed dir itself (.claude/skills); if an ancestor was the symlink (.claude → a shared location, with a real skills dir inside), is_dir() still followed it and the prune could unlink broken links outside the project tree. A canonical-path containment check now refuses to scan anything that resolves outside the project root (a project legitimately reached through a symlink still works).
  • A managed agent dir that is itself a symlink is now classified, not skipped. When .cursor/skills (etc.) was itself a symlink, the scanner returned early without recording it — so a broken managed-dir symlink was never pruned and never reported by boost doctor. It is now classified like any other link (broken → pruned, live → preserved + reported) while still never being followed.
  • Dead-link pruning covers every agent dir, even for embedding callers. An engine constructed with a subset of agent targets (a wrapper, or an embedder) pruned dead links only under its own targets, while boost doctor scans them all — so a stale link under a non-active agent dir was reported but never cleaned. The prune now spans the full agent set, so sync removes exactly what doctor reports.
  • The .config/ layout keeps the legacy root .boost/ ignored even when nothing is owned. The permanent legacy-ignore guard (so a teammate's stale root .boost/manifest.json never surfaces as untracked mid-migration) was gated on "this sync writes a manifest"; a migrated project that currently owns nothing dropped the ignore. The legacy ignore is now unconditional on the .config/ layout; only the active manifest dir stays conditional.
  • Symlink-cleanup hints name the right directories. The "remove the link and re-sync" hint in both boost sync and boost doctor hardcoded find .claude .agents .cursor — incorrect when the reported link lives under another agent root. Both now derive the find roots from the actual reported paths.

Internal

  • The [@internal](https://github.com/internal) UnrenderableSourceScanner now returns UnrenderableSource value objects (path + message) so callers classify on the real extension rather than parsing the message; the symlink scanner exposes a shared cleanupRootsFor() used by both commands. Six regression tests cover the fixes above.

Upgrade note: none — a drop-in upgrade from 0.23.3. 1.0.0 introduces no breaking change; it promotes the existing surface to a SemVer-stable contract. See PUBLIC_API.md for the committed surface and UPGRADING.md for future migrations.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.23.3...1.0.0

0.23.3

Documentation contract

  • The named-argument contract is now explicit. Consumers pass named arguments to [@api](https://github.com/api) methods and construct [@api](https://github.com/api) value objects directly, so the PARAMETER NAMES are part of the frozen contract — a rename or reorder is breaking even when the class is unchanged. PUBLIC_API.md now pins the constructor parameters of Skill / Guideline / RenderContext / WrittenFile / EmitterResult / Diagnostic and the method parameters of BoostSync::sync() + BoostConfig::load(). Two value-shape pins are spelled out: Diagnostic::$level stays a string (error/warning/info, not enum-ified within 1.x) and SyncResult::$errors stays list<string>.
  • The schema-version multi-vendor model is documented. The host schema-version for an unpinned project is seeded max(minRequired across allowlisted vendor schemas) ?? 1, and because the host carries a single version, all conventions-vendors allowlisted in one host must share a schema-version major — multi-catalog contribution at different majors is unsupported within 1.x (per-vendor versioning would be a future handshake redesign). Previously implicit engine behavior; now an explicit contract.
  • Both skill-source shapes are pinned as accepted. Flat <name>.<ext> and nested <name>/SKILL.<ext> are both accepted and stay accepted within 1.x — a future minor will not silently drop flat-file acceptance and un-discover a flat catalog.

Internal

  • A reflection arch-test walks the [@api](https://github.com/api) value objects' constructor parameter names and SyncResult's read surface (asserting $errors stays an array and Diagnostic::$level stays a string), so a rename, reorder, level→enum, or errors→non-array trips CI. It closes the gap an import-scanning guard misses: WrittenFile / EmitterResult / Diagnostic are read off SyncResult by property and never imported.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.23.2...0.23.3

0.23.2

Fixed

  • The dead-symlink prune no longer follows a symlinked agent root directory. 0.23.0's prune of broken symlink-era links across agent dirs guarded child entries but not the directory handed to the scanner — and is_dir() follows symlinks. So if an agent root (e.g. .claude) was itself a symlink to a shared location, the prune could descend into that external target and unlink its broken links, outside the project tree. The scanner now refuses to descend through any symlinked directory, the root included. Live symlinks are still never touched.
  • boost doctor no longer over-states drift on wrapper projects. The wrapper-aware Drift note claimed a bare-CLI diff was "expected, not real drift" — but a bare diff can't distinguish an expected wrapper-composition delta from genuinely stale output. It now surfaces the count and routes to php artisan project-boost:sync --dry-run as the authoritative check (still without the destructive vendor/bin/boost sync steer).
  • boost doctor no longer claims the wrapper renders a source it can't. The "no renderer" skip downgrade (warning → note on a project-boost-laravel project) applied to every unrenderable source, but the wrapper only registers a Blade renderer. Only .blade.php skips downgrade to a note now; any other unrenderable extension stays a warning — it is genuinely skipped by both entry points.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.23.1...0.23.2

0.23.1

Fixed

  • The skill loader ignores backup / editor-temp files. A SKILL.md.bak parked beside a real SKILL.md starts with SKILL., so the depth-1 entry rule treated it as a skill source — and sync emitted a spurious "skipped — no renderer for its .bak extension" for each. SkillSourceScope (the single predicate behind the loader and boost doctor) now excludes backup/editor-temp filenames (a trailing ~, or a final .bak/.orig/.tmp/.swp/.swo extension) up front. No more false warnings; the files are never discovered as skills.
  • boost sync --check now fails on a leaked conventions token in emitted output. When a skill's output path is a pre-existing symlink, sync skips writing it (SKIPPED_SYMLINK) and the symlink's target — the raw source — is what the agent reads, so any unresolved <!--boost:conv--> token in that skill leaks verbatim. boost validate --strict and boost doctor already caught this (the symlink-following on-disk leak scan); the gap was sync --check. It now runs the SAME scan (a single shared ConventionTokenLeakScanner::errorDiagnostics) and fails identically, so both CI gates behave the same. Plain boost sync stays lenient. No file is clobbered — boost flags loud and the operator removes the symlink + re-syncs (consistent with 0.23.0's no-clobber symlink handling).

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.23.0...0.23.1

0.23.0

A dogfood-hardening release. A seven-package family adoption of 0.22 + the .config/ layout — real production apps and the wrapper stack — surfaced one data-loss bug, one composer update footgun, a stranded [@api](https://github.com/api) seam, and two cases of boost doctor giving misleading (one actively destructive) advice on wrapper projects. 0.23.0 fixes all of them. Additive — no API break; a strict superset of the 0.22 surface.

Added

  • BoostConfig::load()'s exceptions are [@api](https://github.com/api). BoostConfigNotFoundException, InvalidBoostConfigException, and AmbiguousBoostConfigException are now part of the frozen surface, catchable by name — a [@throws](https://github.com/throws) of an [@api](https://github.com/api) method is part of its contract, so a wrapper can give a tailored message per failure mode. load() now declares all three with explicit [@throws](https://github.com/throws).
  • BoostTags is [@api](https://github.com/api) — the canonical tag-parse seam. BoostTags::parse(array): array{0: list<string>, 1: bool} + declaresTags(array): bool (pure static, the other half of the already-[@api](https://github.com/api) FrontmatterParser). A wrapper that injects skills/guidelines now computes the SAME [tags, valid] the engine does — including the fail-closed contract (a non-string metadata.boost-tagsvalid = false → ships nowhere) — instead of reinventing the parse and diverging on the malformed case. parseString() stays the [@internal](https://github.com/internal) lexer.

Fixed

  • Sync no longer deletes committed guidance files on the first post-migration sync. A project migrating from the pre-0.12 gitignored-guidance layout carries a managed .gitignore block that still lists CLAUDE.md/AGENTS.md. An UNCHANGED guidance file is absent from a run's write-set, so the stale-managed cleanup reaped it — deleting the committed file on the first sync, recreating it only on a second pass. The cleanup now exempts the agents' guidance paths unconditionally (a de-selected agent's guidance is still reaped by the manifest-gated orphan reaper). An [OK] Sync done can no longer silently delete a tracked CLAUDE.md/AGENTS.md.
  • A stale boost.php no longer 500s composer update. A pre-0.20 variadic withTags(Tag::A, Tag::B) call throws a raw TypeError when boost.php is loaded (during require) — before any migration could run. The loader now wraps the require and converts any evaluation failure into the typed, catchable InvalidBoostConfigException, with a migration hint pointing at the array form (withTags([...])) and the original error preserved as the cause. withTags() itself is unchanged (still array-only — the 0.20 break stands).
  • boost doctor is wrapper-aware in two more places. On a project-boost-laravel project: (1) the Drift section no longer reports the wrapper-composed CLAUDE.md/AGENTS.md as drift and no longer steers the operator at vendor/bin/boost sync — which would overwrite the wrapper-composed guidance with a degraded bare-CLI version; it now points at php artisan project-boost:sync --dry-run. (2) A .blade.php guideline with no bare-CLI renderer is downgraded from a "no renderer — data loss" warning to an informational note (the wrapper Blade-renders it upstream).
  • The tag-typo heuristic no longer false-flags sibling tags. laravel / laravel-cloud and github / github-issues — two distinct tags both shipped by installed skills — are no longer reported as a "possible typo". The genuine signal (a declared-but-unused lookalike of a used tag) still fires.
  • Stale symlink-era orphans are cleaned across all agent dirs. boost-core used to symlink vendor skills into agent dirs and now copies; a repo upgraded across that boundary kept leftover symlinks. Sync now prunes DEAD (broken) symlinks in EVERY agent dir — including a de-configured agent's (e.g. .cursor/ after dropping CURSOR), which the old configured-only prune missed. LIVE symlinks are never touched (boost can't prove a resolving link is its own legacy artifact vs an intentional operator link), so the skipped-symlink output is now a NOTE ("preserved by design") rather than a warning, with a hint for converting to copies by hand. boost doctor gains an Agent-dir symlinks section reporting dead (next sync prunes) + live (preserved) links.
  • .config/ layout keeps the legacy root .boost/ ignored. On the .config/ layout the managed .gitignore block now retains .boost/ permanently, so a teammate who pulls a freshly-migrated repo before their own sync never sees a stale (never-tracked) .boost/manifest.json surface as untracked.
  • The skipped-symlink warning lists the actual paths instead of pointing at vendor/bin/boost --check (not a valid command).
  • Sync idempotency is pinned by a regression test — one sync converges; a check right after is clean; a second sync is a pure no-op (no delete-then-recreate).

Docs

  • README documents that moving an existing config into .config/ needs the directory created first (git mv won't make the parent), and that sync rewrites the managed .gitignore block for you.
  • UPGRADING's 0.20 withTags() entry no longer claims boost sync auto-migrates a variadic call (it can't — the require-time TypeError precedes any rewrite); it now states the hand-edit requirement + the clearer 0.23 load-time error.
  • UPGRADING gains a standing note that tracked guidance + the post-update autosync mean a composer update can produce a committable guidance-file diff (expected, not drift; a no-op when nothing changed).

Upgrade note: none from 0.22.x. Coming from older 0.x, the only hard break that needs a hand-edit is withTags(...)withTags([...]) (0.20) — and 0.23 now reports that as a clear, catchable error instead of a raw fatal. This is the last 0.x surface addition before the 1.0 freeze: 1.0.0 will freeze the 0.23.x surface.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.22.0...0.23.0

0.22.0

The final 1.0-readiness release. A family + downstream-consumer review of the public surface — driven by the packages that actually build on boost-core (the Laravel wrapper, the skills source, a production app) — surfaced real gaps a class-by-class annotation pass had missed: contracts that were [@api](https://github.com/api) but unimplementable without [@internal](https://github.com/internal) symbols, a designed integration point left internal, a freeze doc that wasn't shipped, and a discovery bug freezing would have locked in. 0.22.0 closes all of them, so the deepest consumer now runs entirely on frozen surface. All additive — no API break.

Added

  • A wrapper-integration surface. SanderMuller\BoostCore\Sync\BoostSync is the [@api](https://github.com/api) façade for a wrapper that drives a sync with injected vendor skills/guidelines and extra renderers: BoostSync::make(?InstalledPackages, ?string $configFile)->sync(projectRoot, checkOnly, injectedVendorSkills, extraSkillRenderers, injectedVendorGuidelines): SyncResult. The engine behind it stays internal and free to evolve. The result type (SyncResult + WrittenFile, EmitterResult, Diagnostic, WriteAction, EmitterAction) and the injection payload types (Skill, Guideline) are now [@api](https://github.com/api).
  • [@api](https://github.com/api) bridges so wrappers never reach engine internals. Agent::target(): AgentTarget (agent value → its target, no concrete *Target classes), AgentTarget::skillRelativePathForName(string) (emit path from a name, no Skill), BoostConfig::load(projectRoot, ?configFile) (read a project's config without the internal loader), and FrontmatterParser + ParsedDocument (reuse the parser for boost-tag parity instead of forking YAML-head parsing).
  • A family-CLI extension point. BoostBaseCommand is [@api](https://github.com/api) (narrow): addWorkingDirOption() + resolveProjectRoot() are the frozen helpers a wrapper command extends.
  • RenderContext::$projectRoot. A SkillRenderer can resolve project-relative paths from the render context instead of reaching for a global container. Additive (trailing, nullable).
  • Schema-version handshake enforcement. An out-of-range conventions schema-version (a vendor schema whose required range the host doesn't satisfy) now produces an error-level diagnostic and is NOT applied — failing boost validate --strict and boost sync --check. Plain boost sync stays lenient (it reports but doesn't fail, so installs aren't broken). Previously this mismatch was silently ignored.
  • A declared public-API document that ships. PUBLIC_API.md is no longer export-ignored, so the 1.0 contract travels with the package. It now also enumerates the frozen formats & conventions a class-only freeze misses: vendor publishing paths + extra.boost.* keys, the consumed frontmatter keys, the conventions-schema render annotation, token forward-compat (an unknown token mode is a hard error, never silent corruption), and the emitted-output layout.

Fixed

  • Nested skill reference files no longer ship as phantom skills. Skill discovery was depth-unbounded, so a references/*.md inside a <name>/SKILL.md skill (as shipped by some Laravel-side vendors) was emitted as a bogus top-level skill. Discovery is now scoped to a top-level *.<ext> OR a depth-1 */SKILL.* — and the loader and boost doctor share one rule, so they classify identically. A vendor that happened to rely on the leak will see those phantom skills disappear.

Internal

  • The skill-source scope, the schema-version gate, and the frontmatter parser are now single-source-of-truth seams shared across the sync and audit paths.

Wrapper note: boost-core 0.21's FileEmitter::emit()iterable change means a FileEmitter still on the ?EmittedFile signature hard-fatals when boost-core 0.21+ loads it. If you ship one, migrate the signature first — see UPGRADING.md. Laravel consumers adopt 0.22/1.0 once the umbrella packages ship emit()-iterable-compat releases.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.21.0...0.22.0

0.21.0

Breaking

  • FileEmitter::emit() now returns iterable<EmittedFile> (was ?EmittedFile). An emitter can emit zero (an empty iterable to skip), one, or many files in a single sync — each is validated, written, and reported independently. Update your emitter's return type and wrap a single file in an array:

    // before
    public function emit(SyncContext $ctx): ?EmittedFile
    {
        return new EmittedFile(relativePath: '.mcp.json', content: $json);
    }
    // after
    public function emit(SyncContext $ctx): iterable
    {
        return [new EmittedFile(relativePath: '.mcp.json', content: $json)];
    }
    

    Returning null no longer compiles; return [] to skip. See UPGRADING.md. This is the only FileEmitter shape change planned before 1.0 — the signature locks at the 1.0 tag.

Added

  • Multi-file emitters. A single emitter can produce a whole set of files (e.g. an .mcp.json plus a sidecar). Orphan reaping is per-FQCN aware: when an emitter stops producing a file it once owned, that dormant file is reaped on the next sync — unless the emitter is fully down this run, in which case its prior files are preserved (never lossy).

Fixed

  • A generator emit() that throws mid-iteration no longer aborts the sync. If an emitter yields some files and then throws, it is recorded errored and the sync continues with the remaining emitters. Emit is all-or-nothing on failure: a crashed emitter never half-applies — files it yielded before the throw are not written. (Return an array rather than a throwing generator if you want already-computed files to land regardless of a later problem.)

Internal

  • Branch alias advanced to 0.21.x-dev.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.20.0...0.21.0

0.20.0

0.20.0

The 1.0-readiness release: boost-core now declares and locks its public API surface, so the next step to a 1.0 tag is a decision, not more work. One small breaking change in the config API, and a clear line drawn between what semver covers and what's internal.

Breaking

  • withTags() now takes an array. It was the only boost.php collection setter that was variadic; it now matches every other one (withAgents, withRemoteSkills, …):

    // before
    ->withTags(Tag::Php, Tag::Jira)
    // after
    ->withTags([Tag::Php, Tag::Jira])
    

    boost install's tag picker writes the new array form, and boost sync still parses + migrates an existing variadic withTags(...) in your boost.php — but update the call by hand to avoid a TypeError when the config is next loaded directly. See UPGRADING.md.

Added

  • A declared, enforced public API. Every class is now marked [@api](https://github.com/api) or [@internal](https://github.com/internal), PUBLIC_API.md enumerates the committed surface, and the README has a new Versioning & stability section. The promise covers: the boost.php authoring API (BoostConfig, the builder, Agent/Tag, RemoteSkillSource), the CLI (command names, options, exit codes), the BoostAutoSync composer hooks, and the plugin contracts. Everything else — the whole sync engine — is [@internal](https://github.com/internal) and excluded. An architecture test fails the build if a new engine class isn't marked, so the boundary can't erode.
  • The FileEmitter and SkillRenderer plugin contracts are locked stable ([@api](https://github.com/api)), no longer experimental — including the SyncContext / EmittedFile / RenderContext value objects. Parameterless constructors only; their method signatures won't change within 1.x.
  • --config works on every command. slots, tags, and paths now accept --config <path> like the rest, so a .config/boost.php-layout project can point any command at its config.
  • A deprecation policy. Stable elements are deprecated (with [@deprecated](https://github.com/deprecated) + a runtime notice) in a minor and removed no earlier than the next major — documented in PUBLIC_API.md.
  • The public surface is self-contained. No [@api](https://github.com/api) method or property exposes an internal engine type — an architecture test fails the build if one ever does. AgentTarget's [@api](https://github.com/api) surface is narrowed to the path/identity methods wrapper packages use; its planning/formatting methods are internal. The FileEmitter context exposes the installed-package set via InstalledPackages / PackageInfo, both now part of the stable contract.

Changed

  • boost --version reports the real installed version instead of a hardcoded placeholder.
  • boost doctor is documented as advisory-only — it exits 0 even when it surfaces drift, leaked tokens, or shadows (non-zero only on a config-load failure). Gate CI on boost sync --check or boost validate --strict, which do fail on findings.
  • The legacy convert-conventions command is hidden from the command list (still runnable for stragglers mid-migration; no longer part of the committed CLI contract).
  • boost install and boost scan fail fast with guidance under --no-interaction / no TTY instead of hanging on the interactive picker.

Internal

  • The engine is fully [@internal](https://github.com/internal)-annotated behind a pest architecture guard; BoostConfig's positional constructor is internal (build via BoostConfig::configure()).
  • boost-core now dogfoods the .config/boost.php layout for its own configuration.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.19.0...0.20.0

0.19.0

User-scope sync gains the cleanup-on-remove story project-scope has had since 0.14.0, and the remote-skill orphan ledger moves out of the repo root to follow the .config/ layout.

Added

  • User-scope cleanup-on-remove (boost sync --scope=user). Globally-installed packages that ship skills now get the same lifecycle reaping project-scope already has. boost-core records a per-package ownership manifest at ~/.boost/manifests/<vendor>__<package>.json (each emitted path → its sha, plus the package's install path), and uses it to clean up on the next sync:
    • Dropped/renamed skills — when a package stops shipping a skill, its ~/.{agent}/skills/<vendor>__<package>/<skill>/ copy is removed. A still-shipped skill is kept on every agent (so syncing with a narrowed agent set never deletes a live copy for an agent it doesn't drive), while a dropped skill is reaped under every agent.
    • Removed packages (--scope=user --all) — a composer global removed package's user-scope copies are reaped and its manifest deleted. "Removed" is decided by the recorded install path being gone on disk, never by mere absence from the discovered set, so running --all from a project-local context can't mass-delete a still-installed package's files. A package that was updated to drop all its skills, or replaced in place by a different package at the same path, is reconciled too.
    • Safety contract — deletes require a clean run and a sha match, so an operator-edited file (its sha diverged) is preserved, a symlinked target is never claimed or unlinked, and a failed delete retains ownership so the next sync retries. boost sync --scope=user --check reports a pending reap as drift (with a write/reap breakdown) and changes nothing on disk.

Changed

  • The remote-skill orphan ledger moved into the manifest directory. For projects using withRemoteSkills(...), the ledger that tracks remote-managed skills is now .boost/remote-manifest.json (or .config/boost/remote-manifest.json under the .config/boost.php layout) instead of .boost-remote-manifest.json at the repo root. It now follows the active config layout like the sync manifest, and no longer litters the project root. The move is automatic: a pre-0.19 root-level ledger — and a ledger left in the other layout after moving boost.php between root and .config/ — is migrated on the next sync, with the stale copy removed only after the new one is written (so a transient write failure never leaves you with no ledger). No action required; the ledger is gitignored, regenerable engine state.

Internal

  • Extracted UserScopeManifest, UserScopeReaper, and UserScopeManifestWriter from the sync engine so the new cleanup logic lives in focused, independently-tested units and the engine's complexity budget holds.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.18.3...0.19.0

0.18.3

A bug-fix patch for withRemoteSkills(...) consumers.

Fixed

  • The remote-skill orphan manifest is no longer reaped on every sync. A project using withRemoteSkills(...) saw a spurious Deleted 1 file(s) … - /.boost-remote-manifest.json warning on every sync after the first, and remote orphan-pruning was effectively dead — each sync deleted the manifest, so the next sync read an empty one and skipped pruning. Root cause: the managed-file enumerator skipped the sync manifest (.boost/) but not the remote-skill manifest, which is likewise written outside the write pipeline; it was classified as a stale managed file and reaped. StaleFileCleaner::enumerateManagedFiles now skips .boost-remote-manifest.json too. Removing withRemoteSkills entirely still prunes the orphaned skill directories (reported as drift) and cleans the manifest — verified by regression tests in both directions.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.18.2...0.18.3

0.18.2

A maintenance + documentation patch. No behavior change — internal refactor plus README coverage of the 0.18.0 keep-reason observability.

Internal

  • SyncEngine decomposition. Two cohesive, self-contained clusters moved out of the ~2,400-line engine into stateless collaborators, lowering its cognitive-complexity baseline from 263 to 182 (~31%):

    • SyncManifestWriter — the post-sync ownership-manifest write and the root↔.config/ stale-layout reconcile (including the --check advisory). The engine hands it the enumerated on-disk managed files, so the collaborator never calls back into the engine.
    • StaleFileCleaner — the retired-paths registry sweep, the clean-slate prune of managed files no longer emitted, the shared managed-file enumerator, and the recursive delete. Params-only; the retired-paths registry is passed in rather than back-referenced.

    Method bodies are verbatim and the full test suite passes unchanged (no test edits), confirming the extraction is behavior-preserving.

Documentation

  • The README now documents the keep-reason observability shipped in 0.18.0 (#87): when the drop gate KEEPS the ## Project Conventions block, boost sync (advisory INFO), boost doctor (a "Project Conventions block" section), and boost where --conventions (a kept / dropped / not applicable / status unavailable block-status line) name the artifact and cause holding it open. The boost doctor and boost where --conventions CLI-reference rows are updated for the keep-reason and runtime-manifest-location surfaces.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.18.1...0.18.2

0.18.1

A small follow-through patch that rounds out the .config/boost/ runtime directory shipped in 0.18.0 — documenting it, surfacing it, and making boost sync --check honest about its one-time migration cleanup. No behavior change for root-layout projects.

Added

  • boost doctor shows the active runtime-manifest location. Doctor now prints whether the gitignored sync manifest lives at root .boost/ or .config/boost/ (following the config layout), so operators on the .config/boost.php layout can confirm placement at a glance. Shown only when manifest handling is active — with gitignore management disabled (withGitignoreManagement(false) / BOOST_SKIP_GITIGNORE) boost-core never reads or writes a manifest, so the line is suppressed rather than naming a path that won't be used.

  • boost sync --check reports a stale old-layout manifest a real sync would prune. After moving the config between root and .config/, a real sync prunes the now-stale manifest left at the old location; --check now surfaces that pending one-time cleanup as an advisory (captured before the prune runs, so check and a real sync report it identically). Advisory only — it never registers as drift or fails --check: the manifest is gitignored, regenerable, engine-internal state that boost-core deliberately excludes from drift accounting, and a real sync's deletion of it is invisible to git status, so flagging drift would be a false-positive for benign git-invisible housekeeping. Closes the known-limitation noted in the 0.18.0 release.

Documentation

  • The README ownership-manifest section now documents that the manifest follows the config layout to .config/boost/manifest.json under the .config/boost.php layout, that a root ↔ .config/ move carries ownership forward and prunes the stale copy, and that .config/boost/ is a reserved emitter path.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.18.0...0.18.1

0.18.0

An observability + config-layout release. Two classes of silent capability loss now surface instead of disappearing, the kept ## Project Conventions block now explains itself, and projects on the .config/ layout get their runtime state grouped under .config/boost/. All additive and backward-safe — no behavior changes for projects that don't hit these cases.

Added

  • Warn on unrenderable skill/guideline sources instead of dropping them silently (#85). A source whose extension has no registered renderer — e.g. a SKILL.blade.php with no BladeRenderer declared — used to vanish from the sync with no signal. boost sync now emits an advisory warning naming the file, the unclaimed extension, and the fix (register a SkillRenderer, or rename to SKILL.md); boost doctor reports the same across host and allowlisted-vendor sources. A shared UnrenderableSourceScanner is the single source of truth — skills are scoped to SKILL.*, guidelines warn on any file whose extension no renderer claims that isn't a recognized binary/data asset (images, archives, JSON/YAML, etc.). Advisory-only: it never fails boost sync --check.

  • boost validate advises on dangling legacy $.<root> conventions references. A pre-token $.slot reference (e.g. $.testing.runner) is only ever detected by boost-core — never resolved — so it emits literally; and because the ## Project Conventions block is CLAUDE.md-only, it dangles unresolved for every non-Claude agent. boost validate now surfaces each distinct legacy ref as a warning-level diagnostic pointing at the first emitted file it appears in, advising migration to a boost:conv token or an inlined value. Warning-level by design — it does not fail --strict, since a ref may be mid-migration. Detection is prose-scoped and inline-code-masked, so documented $.slot examples in the shipped migration skills are not flagged.

  • Keep-reason observability for the Project Conventions block (#87). When the drop gate KEEPS the ## Project Conventions block rather than dropping it, the engine now records WHY — the skill or guidance file carrying the legacy $.<root> ref, unresolved token, or prose pointer that pins it open (or a single no-migration-yet note for a pure-conventions project that hasn't adopted tokens). The gate decision is byte-identical; this is purely an additive provenance channel. Surfaced on three read surfaces: boost sync emits advisory INFO diagnostics (quiet unless -v), boost doctor adds a "Project Conventions block" section listing the reasons when kept, and boost where --conventions prints a kept / dropped / not-applicable / status-unavailable block-status line. An operator who migrated their skills to tokens but still sees the block can now find the one artifact holding it open instead of black-box probing.

  • .config/boost/ runtime directory for the .config/ layout. When boost.php resolves under .config/ (0.17.0's .config/boost.php), the sync ownership manifest now lives at .config/boost/manifest.json instead of the root .boost/, so all boost artifacts group under .config/. Migration is handled in BOTH directions (root ↔ .config/): the manifest reader prefers the active layout and falls back to the other layout's copy so prior ownership carries forward, and a real sync prunes the now-stale old-layout manifest (file + empty dir) so it never lingers unignored. Root-layout projects are completely unaffected.

Internal

  • DoctorCommand decomposition: the conventions-check and remote-skill report clusters moved into dedicated ConventionsReporter / RemoteSkillsReporter collaborators, dropping the command's cognitive-complexity baseline below 80 (entry removed). Behavior-preserving.
  • ValidateCommand gained an InstalledPackages injection seam for test parity with WhereCommand / DoctorCommand.
  • SyncResult gained conventionsBlockKept, conventionsKeepReasons, and conventionsEvaluated (the last distinguishes "gate ran and dropped" from "gate never ran" so block status is never misreported when a check-only sync carries benign advisories such as an uncached remote skill).
  • The boost where --conventions --json shape is unchanged (a top-level list of slot rows); block status is a human-output addition only, to avoid breaking existing automation.
  • Regression coverage added across the loaders, sync engine, doctor, where, conventions inliner, and manifest tests for every path above, including no-false-positive guards (assets, documented $.slot examples) and both-direction config-dir migration.

Known limitation

boost sync --check does not report the one-time stale old-layout manifest cleanup after a config-location migration. The manifest is gitignored, regenerable, engine-internal state that has never been part of --check drift semantics; the next real sync resolves it.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.17.0...0.18.0

0.17.0

boost.php can now live under .config/ — the first step toward a tidier repo root. Additive and fully backward-compatible: a root boost.php behaves exactly as before, with no migration.

Added

  • .config/boost.php as an alternative config location. boost-core resolves its config from boost.php at the repo root OR .config/boost.php, via a single new BoostConfigPath resolver shared by every reader and writer. Exactly one may exist — having both is a hard, explanatory error (AmbiguousBoostConfigException) rather than a silent pick, so you never edit the file boost ignores. Source paths still resolve against the project root, so the two locations are fully interchangeable. BoostConfigPath is public, so wrapper packages inherit the same resolution.

  • --config <path> on sync, validate, where, doctor, convert-conventions, scan, and new — point at an explicit config file. A relative path resolves against the project root (not the process CWD), so it's stable regardless of where boost is invoked from.

  • boost install --config-dir scaffolds a new config at .config/boost.php instead of the root (root stays the default). When a config already exists, it is edited in place — boost never creates a second one.

  • boost where prints the resolved config path, and boost doctor reports the config location up front: a both-files ambiguity surfaces as a clear "Config location" section, and a .config/boost.php whose explicit __DIR__-relative source path resolved under .config/ (a silent empty-resolve) is flagged with a fix hint.

Changed

  • The scaffolded boost.php and the canonical config example no longer use __DIR__-relative source paths. Defaults are project-root-relative and location-independent; if you override a source path, use an absolute one — __DIR__-relative values break if the config file is later moved into .config/.

Notes

Back-compat: every existing root-boost.php project behaves identically — no migration, no new required config. The .boost/ state directory and the emitted agent files (CLAUDE.md, .claude/, …) are unchanged. Wrapper packages that want to honor .config/boost.php should rebuild against ^0.17; root-config consumers need nothing.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.16.3...0.17.0

0.16.3

A conventions-token leak-scan fix for symlinked host-shadow skills, a clearer retired-path cleanup message, and an internal comment cleanup. Patch — ^0.16 compatible, no consumer bump needed.

Fixed

  • Conventions-token leak scan now sees symlinked host-shadow skills. boost doctor and boost validate --strict scan the emitted skill set for leaked <!--boost:conv …--> tokens, but the enumeration did not descend directory symlinks. A host skill shadow served through a symlink (e.g. .claude/skills/<name>.ai/skills/<name>) therefore hid any raw token in its SKILL.md from the scan — boost doctor reported clean while the agent read the literal token. The scan now follows the immediate skill-directory symlink, so the file it reports is the one the agent actually reads and a leak there surfaces. The traversal resolves only the one-hop shadow link and never follows links recursively, so a cyclic symlink under a skills directory cannot make the scan loop. boost still never WRITES through these consumer-owned symlinks; only the read-only scan follows them.

Changed

  • Clearer retired-path cleanup message. The diagnostic printed when boost removes a retired generated path (e.g. a former Copilot emit target) was rewritten in plain language — what was removed, why boost owns the path, and how to change emitted output — instead of the dense ownership-contract phrasing.

Internal

  • Trimmed historical noise from source comments (version stamps, issue references, prior-behavior narration) with no behavior change, and added regression guards pinning the conventions block's CLAUDE.md-only placement and the symlinked-shadow leak scan.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.16.2...0.16.3

0.16.2

A cross-platform determinism fix, a vendor-author migration guide, and an internal decomposition of the sync engine. (Rolls up the never-tagged 0.16.1 docs.)

Fixed

  • Guideline emission order is now deterministic across platforms. Emitted guidance files (CLAUDE.md / AGENTS.md / …) could come out with their guideline sections in a different ORDER on different filesystems (e.g. macOS APFS vs a Linux CI runner's ext4) — a pure reorder, identical headings, zero content change. For a project whose CI regenerates and commits boost output, that produced an infinite auto-fix loop: one platform writes one order, the other rewrites the other, each "fixing" the other forever. The resolver now emits a stable order — host-authored guidelines first (in their existing loader order, unchanged), then vendor / injected guidelines by (vendor, source path). The fix is output-ordering only: it does not change which guideline wins a name collision, host-override shadowing, or any reported diagnostic — only the sequence in the written file, which is now byte-identical on every platform. (Surfaced from production dogfood; pairs with the wrapper packages' own source-ordering fix.)

Added

  • conventions-token-migration skill — a shipped, author-facing guide (resources/boost/skills/) for package authors migrating their skills/guidelines off $.slot conventions references onto render-time <!--boost:conv …--> tokens: the recipe, the mode×type matrix + prose-vs-fence placement, the authoring footguns (inline-code-wrapped tokens stay literal; an errored token ships raw; map sub-keys need ^0.16), dropping the obsolete slot-table scaffolding, and the verify-before-ship workflow (boost where --conventions → sync both declared and unset states → boost doctor / boost validate --strict). Pins the consumer-floor rule for token-bearing skills. (Originally drafted for 0.16.1, which was never tagged.)

Internal

  • SyncEngine decomposition (behavior-preserving). The sync engine's conventions inlining + drop-gate, the 0.14 reconcile-on-sync orphan reap, and the wholesale markerless guidance write were extracted into focused collaborators (ConventionsPass, OrphanReaper, GuidanceWriter) backed by a shared filesystem utility (ManagedFileOps), cutting the engine's cognitive complexity by ~31% with no change to observable behavior — every step verified against the full suite + multi-run side-effect characterization (orphan reap, manifest ownership, the never-lossy empty-assembly guard). Closes cross-module helper duplication. No API change.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.16.0...0.16.2

0.16.0

Conventions-token observability. When a <!--boost:conv …--> token does NOT resolve, a raw HTML-comment token lands in an emitted agent file — and the agent then reads the literal token instead of the value, silently. The dominant cause is a token-bearing vendor skill synced by a consumer still on boost-core < 0.15: the old engine has no inliner, so it copies the token verbatim with no error. 0.16.0 makes that leak visible at three escalating surfaces, and resolves a slot-addressing gap found while migrating real skills to tokens.

Added

  • Conventions-token leak detection — three surfaces, one classifier. Detection reuses the inliner's own line scanner, so what counts as a leak can never drift from how inlining works:
    • boost sync warns inline at render time, with a file:line locator — the leak is surfaced where it is born.
    • boost doctor runs an always-on, advisory scan of the emitted set — per-agent guidance files (CLAUDE.md / AGENTS.md / GEMINI.md) and per-agent SKILL.md files, including gitignored copies — and lists every leak with its cause. It never scans .ai/ sources (those legitimately carry tokens).
    • boost validate turns each leak into an error diagnostic, so boost validate --strict fails CI on a leaked token. Canonical CI recipe: run boost sync (or composer install), then boost validate --strict over the post-sync emitted set.
  • Actionable causes. A token that resolves cleanly yet sits raw on disk → "re-sync with boost-core ≥0.15" (it was emitted by an older engine, or the file is stale). A token that errors → the resolver's own message (unknown slot, type×mode mismatch, …). A surviving ```boost:conv fence opener → the fence was never cleanly processed.
  • Open-vocab map sub-key resolution. A token targeting a dynamic key of an additionalProperties map — e.g. path="mcp.jira" — now resolves. Previously it errored as "unknown slot" because resolution short-circuited when the schema had no statically-defined leaf for the key, so map sub-keys were unaddressable whether declared or defaulted. The resolver now descends into additionalProperties and sources a sub-key default from the nearest default-bearing ancestor map. Three-state resolution (declared → schema default → fallback) and declared-empty precedence are unchanged.

Fixed

  • A leaked token inside an opt-in fence is no longer a blind spot. A multi-line (mode="yaml") token lives in a ```boost:conv fence; on clean resolution the engine strips that boost:conv info-string. If a token inside the fence instead errors, the engine now keeps the info-string rather than stripping it — so the unresolved token stays detectable on disk by the same surviving-fence-opener signal that catches a pre-0.15 emit, with no risk of false-positiving on a documentation example in a plain code fence. A cleanly-resolved fence is byte-identical to before.

Internal

  • Detection is conservative by design: a token in prose (or a surviving opt-in fence) is a leak; a token in a plain code fence or an inline-code span is an intentional literal and is never flagged — so skills and guidelines that document token syntax in fenced examples don't trip the check. The fence-opener signal is decided by the full fence state machine, not a flat text match, so a boost:conv line nested inside another fence is correctly treated as content, not an opener.
  • Engine-only and additive: no behavior change to a project with no tokens, and no new public API beyond the doctor check, the validate gate, and the shared scan classifier on the inliner.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.15.0...0.16.0

0.15.0

Conventions inlining. Project-convention values can now be resolved into the skills and guidelines boost-core generates — at sync time — instead of being rendered as a separate always-loaded ## Project Conventions block in CLAUDE.md. Once a project's synced content is fully token-based, that block is dropped: the conventions still drive the output, they're just baked into the prose that needs them rather than carried as a standing context tax on every turn.

This is Phase 1 (the engine). It's fully backward-safe: until a vendor skill actually uses a convention token, nothing changes — the block renders exactly as it did pre-0.15. Migrating vendor/host skills to tokens is a per-package, opt-in follow-up; the block auto-drops for each consumer as their skill set converges.

Added

  • Render-time convention tokens. A skill or guideline body may reference a convention slot with a token — <!--boost:conv path="github.default_base_branch" mode="inline"--> — and boost resolves it against the project's withConventions([...]) declarations at sync time. Resolution is three-state by path existence: a declared value wins; otherwise the schema default; otherwise an inline fallback. A declared false or [] counts as declared (not missing), so an intentional empty list renders as none rather than silently falling through to a default.
  • Type × mode matrix. Each slot renders in the mode that fits its shape: scalars as inline/yaml/json; scalar lists as comma-joined inline, bullets, yaml, or json; maps as yaml/json. A schema may pin the allowed render modes for a slot. A mode that doesn't fit the value's type is a render error.
  • The always-loaded block drops once a project is fully migrated. When every synced skill and guideline gets its conventions from resolved tokens — and nothing still needs the runtime block, and no token errored — boost stops emitting the ## Project Conventions section. The gate fails toward keeping the block: a legacy $.slot reference, an unresolved token, a prose pointer to "the conventions section above", or any render error all keep it. The scan is ownership-aware — it inspects only content that survives this sync, and it strips boost's own previously-rendered block before scanning so the heading can't keep itself alive.
  • boost where --conventions (with --json). An on-request audit of every convention slot and where its effective value comes from: declared, schema-default, or missing. Surfaces what the inliner will resolve before you sync.

Fixed

  • Convention render errors now fail --check. An unknown slot, a type/mode mismatch, a multi-line value asked to render inline, or a slot with no resolvable value is reported as an error and keeps the block rather than emitting a half-resolved file — so a broken token can't silently ship degraded guidance.

Internal

  • Inlining runs over both vendor and host skills/guidelines. Tokens are recognized in prose and in fenced code (opt-in via a boost:conv info-string); inline-code spans, balanced-backtick runs, and an escaped <!--\boost:conv--> are left literal. Tokens are body-only — frontmatter is never substituted.
  • The drop gate keys off the live post-sync content set, not just this run's emissions, and is ownership-aware: for a boost-owned guidance file it scans the residual outside boost's managed region; for a file boost doesn't own it scans the whole thing minus boost's own rendered block. This prevents both wrongly dropping the block while an operator-authored dependency is still live and wrongly keeping it on boost's self-reference.
  • Phase 1 ships the engine only; no vendor skill emits tokens yet, so in practice the block continues to render until the skill packages migrate on their own cadence. The schema that governs slot shapes and render-mode pins is owned by the shared conventions schema package.

Spec and implementation were independently model-reviewed across multiple rounds and ratified by the maintainers of the dependent packages before merge. Sourced from production dogfood across the boost stack.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.14.0...0.15.0

0.14.0

Reconcile-on-sync orphan reap. Using the 0.13.0 ownership manifest, boost-core now removes the files it emitted once it no longer emits them — closing the "delete it by hand" gap for project-scope output. Two cases land together: a dormant FileEmitter (its dep was removed, so it emits nothing) and a de-selected agent's guidance file (you dropped the agent from withAgents(...)). Both reap only what boost can prove it owns, and never touch operator-authored content.

Added

  • FileEmitter dormancy reap. When a FileEmitter's backing dependency is removed (its emit() returns null, or the package is gone), the file it previously wrote — e.g. .mcp.json — is removed instead of left behind pointing at a dead tool. Emitter outputs are recorded in .boost/manifest.json as category: file, provenance: emitter:<fqcn>. Reaping is conservative and never-lossy:
    • an output is recorded as boost-owned only when boost created it fresh or already owned it — a first-time takeover of a pre-existing file you maintain is not claimed (and warns), so it's never reaped;
    • before deleting, the on-disk content is sha-revalidated — if you hand-edited the emitter's output (e.g. tweaked an .mcp.json), the divergence is detected and the file is preserved;
    • a disabled emitter (withDisabledEmitters) or one that errored this run keeps its file (disabling means "stop regenerating", not "delete");
    • only a regular file is removed — if you've since replaced the path with a directory or symlink, it's left alone;
    • a failed delete (permissions) retains ownership so the next sync retries rather than leaking.
  • Agent de-selection reaps the orphaned guidance file. Removing an agent from withAgents(...) now removes that agent's now-stale CLAUDE.md / AGENTS.md / GEMINI.md — but only when boost owns it (the on-disk sha still matches what boost wrote). A guidance file you've hand-edited is preserved. This closes the gap where a de-selected agent's guidance file lingered indefinitely while its skill directory was already pruned.
  • FileEmitter authoring guardrails. An emitter may only write to a path it alone owns. A path that collides with a boost- or operator-owned surface — a guidance file (in any case spelling), .gitignore, .boost/, any agent's skill/command root, a source directory (.ai/, resources/boost/), or a wrapper-claimed path or its descendants — is rejected with a diagnostic and never written, tracked, or reaped. Emitter paths are canonicalized and case-folded before these checks, so ./CLAUDE.md and claude.md can't slip through.

Fixed

  • Redundant per-file entries in the managed .gitignore block. When a wrapper injected skills into a directory boost already ignores at the directory level (e.g. .claude/skills/), boost-core also emitted a line for every individual …/SKILL.md beneath it — pure bloat that re-grew on every sync. The managed block now drops any per-file entry already covered by a directory-level pattern, keeping it compact. (Reported from project-boost-laravel adoption.)

Internal

  • The reap is a dedicated, manifest-gated pass — it consults the prior manifest's ownership rather than raw .gitignore membership, so the delete predicate matches the stated ownership contract by construction. Two-phase lifecycle is unchanged: decisions read the prior manifest, the new one is written last on a fully-successful sync; an absent manifest means exact pre-0.14 behavior (no new reaping).
  • Emitter ownership is keyed by file identity (inode) where available, so a case-only output rename on a case-insensitive filesystem neither deletes the live file nor loses its ownership record; on filesystems without stable inodes this degrades to a benign, documented preserve-rather-than-reap.
  • No Composer-plugin reintroduction. boost-core retired its plugin and runs as a library plus sync hooks, so cleanup is driven by reconcile-on-sync, not package-uninstall events. User/global-scope cleanup-on-remove remains deferred; the manifest's scope field already carries the forward-compatible schema for it.

Sourced from production dogfood and downstream adoption feedback across the boost stack.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.13.0...0.14.0

0.13.0

A DX + observability minor anchored by a sync ownership manifest. boost-core now records what it emits — so it knows what it owns — which structurally resolves the ownership-signal tension behind the 0.12.0 empty-guard trade-off. Plus guideline-shadow parity in boost where/boost doctor, so a host guideline that silently shadows a vendor one is finally visible.

Added

  • Sync ownership manifest (.boost/manifest.json). On every successful sync boost-core writes a manifest of the files it emitted — each with a sha256, a category (guidance/skill/command), a provenance (engine or wrapper:<vendor/package>), and a scope (project/user). With ownership recorded, boost-core can safely clear or prune its own files without ever touching operator-authored content — replacing "guess from file content" with "consult the manifest." The manifest is gitignored via the managed .gitignore block (it's regenerable emit-state, not source). Absent a manifest — every pre-0.13 project, a fresh clone, the first 0.13 sync — behavior is exactly 0.12: no new clearing or pruning. The manifest only ever enables safe destructive actions; it never makes the no-manifest path more aggressive.
    • Markerless guidance now converges safely. An empty assembly clears a guidance file only when the manifest proves boost owns it (listed and the on-disk sha matches what boost last wrote). A file boost can't prove it owns — or one you've hand-edited since (sha diverged) — is preserved. The 0.12 empty-guard still holds for every non-owned file.
    • Hand-edits are surfaced, never silently clobbered. If you edit a boost-owned guidance file and a later non-empty sync would regenerate it, boost-core regenerates and warns — naming the file and pointing you at .ai/guidelines/, where durable content belongs. The prior content is always in git.
    • Stale guideline files get pruned. Tag-filtering away an agent's entire guideline set now prunes the orphaned guidance file when boost owns it (engine provenance, manifest-listed), instead of leaving it on disk indefinitely.
    • Wrapper-emitted files are preserved across bare-CLI runs. Paths tagged wrapper:<vendor/package> are never pruned by a bare-CLI sync (which can't reproduce a wrapper's injection set); wrapper-path cleanup defers to the next wrapper-driven sync. The 0.11 BoostWrapperContract stays as the cold-start fallback for the manifest-absent window.
  • boost where guideline-shadow parity. A host .ai/guidelines/<name>.md that shadows an allowlisted vendor guideline of the same name is now annotated in boost where output ((shadows <vendor>)), counted in the shadow NOTE, and diffable via --diff=<guideline-name>. The shadow check respects the active withTags(...) filter: a vendor guideline that wouldn't emit anyway (tag-filtered) is not reported as shadowed, and --diff resolves against the tag-eligible vendor copy — no contradictory false positives. Brings guidelines to parity with the existing skill-shadow surfacing.
  • boost doctor reports guideline shadows. Host→vendor guideline shadows now surface in doctor too, reusing the same computation as where, so the two agree on the shadow story.

Fixed

  • Conventions render independently of the active agent set. ->withConventions([...]) writes the Project Conventions section to CLAUDE.md even when the Claude agent isn't in withAgents(...) — a Codex/Copilot/Gemini-only project that declares conventions no longer needs Claude active to get them rendered.

Internal

  • New src/Sync/SyncManifest.php value object owns manifest read/write/compare, the category-specific ownership rule (guidance is listed-and-sha-matched; skills/commands listed), and the source-dir exclusion invariant (a manifest never lists, and a prune never resolves, a path under .ai/ or resources/boost/ — protecting dual-role publisher repos whose shipped product lives there).
  • SyncEngine reads the prior manifest at the start of a sync for all destructive decisions and writes the new manifest last, only on a fully successful, non-render-failed sync — so a first 0.13 sync can't promote a pre-existing file to owned mid-run, and a partial/failed sync leaves the last-known-good manifest untouched. Destructive clear/prune runs only after all non-destructive writes succeed.
  • Wrapper provenance is attributed by path-matching each emitted path against the injecting wrapper's injectedEmitPaths() (via WrapperEmitDiscovery), not by injection vendor-key — so the manifest tags the injector, the intersection of emitted-and-declared paths, and stays correct under multiple installed wrappers.
  • The no-conventions-schema INFO is dormancy-gated; boost doctor reuses a single drift sync for both drift and shadow reporting.

The boost install composer.json script scaffold considered for this release was deferred — safely scaffolding a dev-only plugin's sync into Composer lifecycle hooks proved intractable across --no-dev and bare-CLI install paths, and is largely redundant with the plugin's existing auto-sync. It returns as a focused follow-up if real demand appears.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.12.0...0.13.0

0.12.0

Markerless agent-guidance files. The agent-guidance files boost-core emits — CLAUDE.md, AGENTS.md, GEMINI.md — are now wholesale boost-owned and carry no markers. boost-core regenerates each file in full on every sync from your .ai/guidelines/ sources and boost.php conventions. The <!-- boost-core:guidelines:* --> / <!-- boost-core:conventions:* --> marker pairs are gone: fewer moving parts in the emitted file, and one consistent ownership model across every path boost-core manages.

Changed (action may be required)

  • Agent-guidance files are markerless and wholesale-owned. Put operator-authored guidance in .ai/guidelines/ (it's assembled into the file on every sync) rather than hand-editing the emission target. On the first 0.12.0 sync of a legacy marker-bounded file, the markers are stripped and any genuine content outside them is preserved once below the generated body, with a warning pointing you at .ai/guidelines/. Nothing is silently lost.
  • Legacy Project Conventions YAML. If a file still carries a boost-core:conventions:* marker block, migrate it into boost.php with vendor/bin/boost convert-conventions before upgrading (while the markers still exist). After a 0.12.0 sync has stripped the markers, copy any preserved conventions YAML into boost.php's ->withConventions([...]) chain by hand — convert-conventions requires the markers and no longer applies once they're gone.
  • Track your guidance files. Keep CLAUDE.md / AGENTS.md / GEMINI.md in version control so boost-core's wholesale output is reviewable in diffs and recoverable from git. boost-core's managed .gitignore block already keeps them out of the ignore list.

Added

  • Empty-assembly guard. Sync never blanks a non-empty guidance file. When boost resolves no guidelines and no conventions, an existing non-empty CLAUDE.md / AGENTS.md / GEMINI.md is left untouched (an INFO records this) instead of being overwritten with empty content. Adopting boost-core in a repo that already has a hand-written or boost install-generated CLAUDE.md — including via auto-sync on a routine composer update — no longer risks wiping it. Delete the file manually if you genuinely want it empty. Legacy marker-bounded files are exempt: they're provably boost-written, so they still converge.

Fixed

  • Conventions render independently of the active agent set. ->withConventions([...]) once again writes the Project Conventions section to CLAUDE.md even when the Claude agent itself isn't in withAgents(...) — matching pre-0.12 behavior. A Codex/Copilot/Gemini-only project that declares conventions no longer loses them.
  • Quieter sync for skills-only vendors. The "N of M allowlisted vendor(s) ship no conventions-schema.json" INFO is now dormancy-gated: it's suppressed when conventions aren't declared and no vendor ships a schema, so a skills-only vendor in a project that doesn't use conventions stops producing worrisome noise. It still surfaces when you've declared conventions (a missing schema is then actionable) or when any vendor ships a schema file — including a malformed one, which still proves the conventions subsystem is in play.

Internal

  • New GuidanceComposer owns markerless assembly and the one-time legacy-marker migration (strip the guidelines region, unwrap the conventions region preserving its YAML, drop stale inline duplicates, preserve genuine residual once + warn).
  • SyncEngine consolidates the guideline and conventions writes into a single wholesale markerless write per unique guidance file; AgentTarget::plan() now emits skill writes only.
  • boost doctor flags bare-name exclude keys (those missing a vendor/package: prefix) that would otherwise silently no-op.

Sourced from production dogfood and adoption feedback across the boost stack.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.11.0...0.12.0

0.11.0

Drift-comparison wrapper-injection awareness. Closes the correctness half of the wrong-entry-point bug class. 0.10.0 closed the discoverability half (the boost doctor entry-point-mismatch banner); 0.11.0 stops bare-CLI boost sync from false-positive-flagging wrapper-injected files for deletion.

Why this matters

When a wrapper package (sandermuller/project-boost-laravel) is the install pipeline, it injects skills and guidelines at sync time via SyncEngine::sync()'s injectedVendorSkills / injectedVendorGuidelines runtime args. The wrapper's canonical entry point (php artisan project-boost:sync) writes those files to disk.

A bare-CLI boost sync / boost sync --check carries no injection args, so the resolve pass produces an empty vendor-skill set. The generic stale-file cleanup pass (0.9.1) then classifies every previously-injected file as stale-to-delete:

[WARNING] Drift detected: 34 file(s) would change.
  - .agents/skills/fluxui-development/SKILL.md
  - .agents/skills/inertia-svelte-development/SKILL.md
  ... (30 wrapper-injected SKILL.md files marked for deletion)

Sourced from production dogfood: a downstream Laravel consumer added a workflow-rule note to their CI explicitly forbidding boost sync invocations — documenting a workaround for what should be an engine guarantee.

Added

BoostWrapperContract — wrapper emit-surface declaration

Wrapper packages declare a BoostWrapper class implementing SanderMuller\BoostCore\Contracts\BoostWrapperContract at any of their PSR-4 prefixes:

namespace SanderMuller\ProjectBoostLaravel;

use SanderMuller\BoostCore\Contracts\BoostWrapperContract;

final class BoostWrapper implements BoostWrapperContract
{
    /** [@param](https://github.com/param)  list<string>  $activeAgents  agent enum values in withAgents(...) */
    public static function injectedEmitPaths(string $projectRoot, array $activeAgents): array
    {
        return ['.agents/skills/some-injected-skill/SKILL.md', /* ... */];
    }
}

boost sync reads the declared emit-paths and excludes them from stale-file cleanup, so bare-CLI sync no longer flags wrapper-injected files for deletion. The $activeAgents argument carries the project's withAgents(...) set so wrappers compute the correct per-agent paths (.claude/skills/… for Claude Code, the shared .agents/skills/… pool for Cursor / Copilot / Codex / etc.) using boost-core's AgentTarget API.

The declared paths also land in the boost-managed .gitignore block, so bare-CLI sync doesn't drop them from gitignore tracking (which would leak wrapper-emitted files into the operator's git working set).

Behavior

  • Wrapper absent / no BoostWrapper class — bare-CLI sync falls back to strict drift comparison (correct, just noisier). No engine-side wrapper allowlist to maintain; the 0.10.0 entry-point banner remains the discoverability surface.
  • Per-package failure isolation — a wrapper whose class fails to autoload (parse error / top-level throw), throws from injectedEmitPaths(), returns a wrong type, or declares the class without implementing the contract degrades that one package with a diagnostic, never aborts the sync.
  • Directory claims — a wrapper claiming a directory (.agents/skills/foo) preserves every file under it via prefix-match.
  • Guideline files excludedCLAUDE.md / AGENTS.md / GEMINI.md are filtered from the wrapper exclusion: they use marker-bounded managed regions with operator-tracking, not wholesale replacement, so the engine never routes them through file-level cleanup.

Scope

The contract covers stale-file-cleanup exclusion only. It does not regenerate wrapper-injected managed-region content (guideline sections injected via injectedVendorGuidelines) on a bare-CLI run — that content is not reproducible without the wrapper's runtime injection args. Bare-CLI runs that need the full injected content must use the wrapper's canonical entry point; boost doctor's entry-point-mismatch banner points operators there.

Compatibility

composer update sandermuller/boost-core to ^0.11. No boost.php or config changes required for direct consumers — the engine-side change is backward-compatible (wrapper-absent projects keep the prior behavior).

Wrapper maintainers opt in by shipping the BoostWrapper class. This is the first boost-core minor that adds a wrapper-contract surface (engine + wrapper coordinate via a discovery contract) rather than a purely engine-internal change — wrapper packages need a paired release to gain the precision; direct consumers absorb without action.

sandermuller/project-boost-laravel ships its BoostWrapper implementation in the paired release; until then, bare-CLI sync on a project-boost-laravel project falls back to strict drift comparison (the pre-0.11.0 behavior, surfaced by the entry-point banner).

Internal

  • SanderMuller\BoostCore\Contracts\BoostWrapperContract: new interface, static injectedEmitPaths(string $projectRoot, array $activeAgents): list<string>.
  • SanderMuller\BoostCore\Sync\WrapperEmitDiscovery: probes each installed package's PSR-4 prefixes for a BoostWrapper class, package-scopes the resolved class via a ReflectionClass file-path check (filters foreign classes occupying the same FQN), unions injectedEmitPaths() across all wrappers, canonicalizes returned paths (forward slashes, collapse ./duplicate-separator segments, reject .. traversal), and degrades per-package with diagnostics on every failure mode.
  • SyncEngine::cleanupStaleManagedFiles(): excludes wrapper-claimed paths (exact + directory-prefix match). SyncEngine::updateGitignore(): includes them in the managed block, filtering guideline-file basenames.
  • Also bundles the 0.11.0 Likely cause: wording pin — an exact-string regression assertion on the 0.10.2 residual-warning's non-exhaustive-hypothesis-set framing.
  • PHPStan baseline: SyncEngine cognitive complexity 210 → 222.

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.10.3...0.11.0

0.10.3

Symlink-aware cleanup. Closes the residual-path failure mode the 0.10.2 observability surfaced. The cleanup pass for retired Copilot paths now correctly handles symlinks left over from pre-0.9.6 boost-core's explicit symlink-emit shape, instead of silently leaving them on disk.

Why this matters

Boost-core's retired-paths cleanup walks .github/skills/ (retired in 0.9.1) on every Copilot-active sync and removes the directory and its contents. Before 0.9.6, boost-core explicitly emitted symlinks at this path for vendor-shipped skills:

.github/skills/mcp-development -> ../../vendor/laravel/mcp/resources/boost/skills/mcp-development

This was the older Copilot path-consumption route for laravel/mcp-shipped skills. Operators who upgraded across the 0.9.x line carry these symlinks until cleanup removes them.

The cleanup walker was failing on those symlinks:

  • SplFileInfo::isDir() follows symlinks → reports a symlink-to-directory as a directory.
  • The engine then called [@rmdir](https://github.com/rmdir) on the symlink, which fails because rmdir requires an actual directory.
  • The symlink stayed on disk; the parent .github/skills/ couldn't be removed while still containing the symlink; drift persisted across syncs.

0.10.2 made this failure visible by surfacing residual paths in a warning diagnostic. 0.10.3 fixes the underlying cause.

Fixed

deleteRecursive checks is_link() before is_dir()

In both the top-of-method dispatch (defensive — guards against a future caller passing a symlink as the cleanup root) and the iteration loop body, symlinks are now detected before the directory check and removed with unlink() instead of rmdir(). The change preserves PHP's default iterator behavior: RecursiveDirectoryIterator::hasChildren() defaults to $allowLinks=false, so the iterator never descends INTO symlinked directories — vendor content beyond the symlink target stays untouched.

Regression coverage asserts both halves of the contract: the retired symlink IS removed, and the vendor target IS preserved (vendor/laravel/mcp/.../skills/mcp-development/SKILL.md survives with original content).

Compatibility

composer update sandermuller/boost-core to ^0.10. No boost.php or config changes required. No breaking surface.

Operators upgrading from 0.10.0 or 0.10.1 who carry pre-0.9.6 symlinks will see them cleaned up on next sync. Operators on 0.10.2 who saw the residual-warning diagnostic will see the residuals removed and the warning stop firing.

Internal

  • SyncEngine::deleteRecursive(): is_link() check added at top of method + in the iteration loop body, ordered before is_dir() since the latter follows symlinks. Vendor-content-preservation contract pinned by iterator default + regression test.
  • Regression test: POSIX-only (skipped on Windows where symlink creation requires admin), uses the exact reproducer shape — .github/skills/<name> symlink pointing at vendor content, asserts symlink removal + vendor preservation + no residual-warning emission.
  • PHPStan baseline: SyncEngine cognitive complexity 205 → 210.

Tag-cut canonical command shape

Per the agent-handoff-command-shape discipline:

gh release create 0.10.3 \
    --target main \
    --title "v0.10.3" \
    -F internal/release-notes-0.10.3.md

Full Changelog: https://github.com/SanderMuller/boost-core/compare/0.10.2...0.10.3

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky