droost / engine
Framework-free engine behind Droost: QA verify loop, AI-harness installers, skill emitters, scaffold blueprints, OKF wiki core, and code search/graph core for Drupal projects.
Requires
- php: ^8.3
- nikic/php-parser: ^5.0
- psr/log: ^2.0 || ^3.0
- symfony/process: ^6.4 || ^7.0
- symfony/yaml: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- drupal/coder: ^8.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5 || ^11.5
Suggests
- ext-pdo_sqlite: SQLite-backed code-graph/vector storage for repo-only (no-site) usage
Provides
None
Conflicts
None
Replaces
None
README
The framework-free engine behind Droost.
Droost is a developer-acceleration toolkit for AI coding agents working on Drupal. Most of what it does is not actually Drupal-specific: running a QA verify loop, writing AI-harness config files, generating scaffolds, composing wiki pages, indexing code. This package is that half — plain PHP with no Drupal dependency — so the same logic can serve the Drupal module, a standalone CLI, and repo-only tooling that has no installed site to boot.
Areas (each depends only on Support and Site, never on a sibling):
| Area | What it holds | Landed |
|---|---|---|
Support |
Shared primitives: project-root discovery, path guards, secret redaction, git HEAD, clock, state store | partly |
Site |
What the engine may know about the site it runs against: the extension-locator port and the no-site implementation | yes |
Verify |
The QA verify loop (lint, static analysis, tests) and its leg results | yes |
Skills |
Skill emitters: droost's own skill files rendered as SKILL.md | yes |
Harness |
Installers that write AI-harness files (AGENTS.md, SKILL.md, and friends) | yes |
Scaffold |
Blueprint registry and the framework-free code blueprints | partly — see below |
Wiki |
OKF wiki core: frontmatter, provenance, page composition, bundle reading | yes |
Search |
Code search and graph core: chunkers, extractors, index diffing | partly — stores and indexer still to come |
Why Scaffold is a partial lift
The scaffold area splits along a line that is not negotiable. Droost composes
Drush's ~70 code generators rather than reinventing them, and that runner needs
the Drush runtime container — Drush::hasContainer(), and Drush's own
ApplicationFactory. Seven blueprints (service, content-entity, config-entity,
event-subscriber, block, form, plugin) are built on it, and an eighth reflects
over DrushCommands to compute its reserved method list. None of those can
live in a package that does not depend on Drush, so they stay in the module.
What lives here is the spine — BlueprintInterface, AbstractBlueprint,
BlueprintRegistry, ScaffoldContext, ScaffoldResult — plus the eight
blueprints that emit from templates rather than delegating: access-handler,
config-schema, hook, kernel-test, mcp-tool, route-subscriber, sdc,
views-handler. The registry accepts both halves, so a consumer sees one set.
That the two halves are wired together at the container rather than merged is the point: the contract is framework-free even where an implementation is not.
The Site port, and why it is three-valued
Site\ExtensionLocatorInterface is how everything else asks what the project
has. Its isInstalled() returns true, false, or null — because the
engine runs both inside a booted site, where absence is a fact, and against a
bare checkout, where it is not knowable.
That distinction is load-bearing rather than fussy: letting null collapse
into false makes a plain checkout report as missing what is merely
unknowable, and a missing answer looks exactly like a negative one.
Site\UnknownSite answers null to everything, and callers are contracted to
read that as "cannot tell", never as "not installed". Since 0.7.0 the engine
itself reads only coreVersion() from the port; the guideline catalogue that
pruned on isInstalled() is gone.
Status
0.x — extraction in progress. Code is being lifted out of the droost module
area by area (phases B1 through B5 of the extraction plan); each tranche lands
behind a tagged release before the module switches over to it. Treat the API as
unstable until 1.0: breaking changes bump the minor, and consumers should pin
per minor (~0.1.0).
0.1.1 landed the pilot tranche — Support\ProjectRoot, Support\PathGuard,
Support\SecretRedactor, Support\GitHead, Verify\VerifyRunner,
Verify\LegResult — chosen because it has zero coupling to Drupal and
therefore proved the packaging pipeline on the smallest possible surface.
0.2.0 adds the guidance tranche (B2): Site\ExtensionLocatorInterface and
Site\UnknownSite, Guidelines\GuidelineProvider, Skills\Skill,
Skills\SkillProvider, Skills\SkillMdWriter, and the whole Harness area —
fifteen classes that write AGENTS.md, SKILL.md, and the per-harness config
files for Claude, Codex, Gemini, opencode and Qwen. The minor bumps because
consumers pinned to ~0.1.0 must opt in; nothing in 0.1.1 changed.
0.3.0 adds the scaffold spine and the eight template-only blueprints (B3),
for the reasons in "Why Scaffold is a partial lift" above.
0.4.0 adds the OKF wiki core (B4) — frontmatter parsing, provenance,
page composition, bundle reading — plus Support\Yaml, which reproduces
Drupal's YAML settings exactly (Dumper(2), inline level PHP_INT_MAX, the
same flag pairs) so a regenerated page is byte-identical to the last one. That
was verified against core's own serializer on a booted site, not assumed;
Wiki\BundleLocatorInterface is the one port it needs, and the module's
existing WikiSettings already satisfies it unchanged.
0.5.0 adds the pure search core (B5a): the chunkers, Graph\GraphVisitor
and Graph\YamlGraphExtractor, index diffing, and the embedding and
vector-store interfaces. The storage layer stays in the module for now — four
classes built on Drupal's database API, which need a port of their own before
they can move (B5b).
0.6.x lands B5b: the three storage ports (file manifest, code graph,
vector store), each with a SQLite implementation. They are what
bin/droost-index runs on, so it can index a repository and answer
code-graph questions with no Drupal site at all. The Drupal indexer stays in
the module, because it reads site configuration a bare checkout does not
have. The line also added:
- the recipe, migrate, plugin-deriver, media-source, ckeditor5-plugin and functional-test blueprints;
- metadata filters on the vector-store port, applied before the k-limit;
- slash-command materialization for the Claude installer (
CommandProvider, with nested command names).
It also let a guideline topic name the module it describes, which 0.7.0 took out again with the rest.
0.7.0 removes the guidance tranche. Guidelines\GuidelineProvider is gone
— its one non-guidance job, deriveMajor(), is now Support\CoreVersion::major()
— and Skills\SkillProvider lists only the consumer's own skill files instead
of rendering every guideline topic as a drupal-* skill. The harness writes the
AGENTS.md block (Harness\DroostBlock, what droost is) and every editor's
pointer to it unconditionally: InstallContext loses guidelinesMode, because
those files are also how a build pipeline's own AGENTS.md block reaches the
agent, and a switch that could cut that road silently is not one worth keeping.
The Claude installer now sweeps retired droost skills — only directories it
provably wrote and nobody has touched since — and its sentinel records the
SKILL.md hash so later sweeps can keep an edited skill. Breaking, hence the
minor.
0.7.2 records only unconditional declarations as code-graph symbols. A
class or function declared inside an if, a loop or a function body exists
only once that code has run, and is usually a fallback for the real one:
project_browser's fixture script declares class Drupal when
!class_exists('Drupal'). With core unindexed, that stand-in owned every
\Drupal:: call in a codebase, so the wiki put project_browser in scope for
every custom module that calls \Drupal. PhpGraphExtractor now runs
ParentConnectingVisitor, which GraphVisitor expects.
0.7.3 narrows that rule to what it was for. Only a declaration guarded by
its own absence, if (!class_exists('X')) { class X … } (or the interface,
trait, enum or function form), is skipped. 0.7.2 skipped every conditional
declaration, and a full rebuild of a real site showed the cost: webform's
WebformManagedFileBase, declared in both branches of a feature check, and a
theme's preprocess hook, declared only while a module is on, dropped out of
the graph although each is the only declaration of its name.
RepoIndexer::INDEXER_VERSION is 2, so a repository index rebuilds itself.
0.7.4 fixes two blueprints. The CKEditor 5 plugin's declaration no longer
names a library or admin_library the blueprint never writes, and the MCP
tool blueprint implements the doExecute() hook its base class calls rather
than overriding the final execute().
0.7.5 adds Site\ScopeClassifier and Site\Provenance: one answer to who
owns an extension's code, where droost had five that disagreed. Composer's
runtime record decides first (drupal-core is core, drupal-custom-* custom,
any other drupal-* package contrib, the deepest install directory winning),
then the first path segment named contrib or custom, and code placed by
hand is the project's own. A package installed at or above the docroot owns
nothing, so a module's own CI checkout does not make every extension contrib.
Paths are compared as written, never resolved, so a package composer linked
in from elsewhere is still found at its link.
0.7.6 is the wiki at scale:
Wiki\PageRendererwrites a page body from a factsheet alone, with no model. The same factsheet renders the same bytes, and a section nobody could measure says why.GenerationRow::unchanged()reports a regeneration that would write what a page already says.PageComposer::compose()records which writer made the body (droost:wiki:generate,droost:wiki:writeordroost:wiki:render) and types a theme's page as aDrupal Theme.
It also has two fixes:
- A hook an api.php documents is a
hook:NAMEsymbol owned by its module, so an implementation's edge reaches a module rather than nothing.RepoIndexer::INDEXER_VERSIONis 3. - The kernel-test and functional-test blueprints keep a class name that
already ends in
Test.
The rest of Support (clock, state store) arrives with the areas that need it.
Install
composer require droost/engine
Requires PHP 8.3 or newer. ext-pdo_sqlite is suggested — it backs the
SQLite code-graph and vector stores used for repo-only (no site) operation.
Development
composer install
vendor/bin/phpcs
vendor/bin/phpstan analyse
vendor/bin/phpunit
Coding standards are Drupal + DrupalPractice (phpcs.xml.dist), matching the
module so lifted files diff to near-zero. PHPStan runs at level max with an
empty baseline. CI runs all four on PHP 8.3 and 8.4.
License
GPL-2.0-or-later. See LICENSE.