PHPackages                             droost/workflow - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. droost/workflow

ActiveDrupal-module

droost/workflow
===============

The phased, gated pipeline an agent runs to build or change a Drupal site: plan, code, test, document, complete.

v0.1.1(yesterday)00GPL-2.0-or-laterPHPPHP ^8.3CI passing

Since Aug 13Pushed todayCompare

[ Source](https://github.com/Ignibyte/droost-workflow)[ Packagist](https://packagist.org/packages/droost/workflow)[ Docs](https://github.com/Ignibyte/droost-workflow)[ RSS](/packages/droost-workflow/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (7)Versions (3)Used By (0)

Droost Workflow
===============

[](#droost-workflow)

[![ci](https://github.com/Ignibyte/droost-workflow/actions/workflows/ci.yml/badge.svg)](https://github.com/Ignibyte/droost-workflow/actions/workflows/ci.yml)[![Packagist](https://camo.githubusercontent.com/5af6ba1a18b8b133e874fcdaa570c0274fe1eb521f155a9394a67e9244572639/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f64726f6f73742f776f726b666c6f77)](https://packagist.org/packages/droost/workflow)[![PHP](https://camo.githubusercontent.com/cc6054306d21fc1538eb98d371b55b96ad84b0282e4c8a306d997c9c44a982a9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e332d383839324246)](composer.json)[![License](https://camo.githubusercontent.com/cd781d0568c328bdea35f284d33e2b1e96675ef60910926c1dd75d5a4d46de66/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d47504c2d2d322e302d2d6f722d2d6c617465722d626c7565)](LICENSE)

The phased, gated pipeline an agent runs to build or change a Drupal site:

```
plan → code → test → document → complete

```

Each phase has an entry and an exit gate. Pass, and the run advances; fail, and it enters a bounded feedback loop or stops. What "pass" means is configured per repo, in one version-controlled file.

Droost Workflow is the **methodology** layer. Where [droost](https://www.drupal.org/project/droost) is what an agent *knows* about Drupal, this is how it *works*: the same pipeline, the same levers, whether it runs against a live site or from a plain checkout with no site at all.

Clean-room GPL. A sibling of, not a fork of, any proprietary pipeline.

Status
------

[](#status)

Built ticket by ticket, and past its skeleton:

StepWhatStateP6.1Config spine — the lever file, presets, run state**shipped**P6.2The five phases as a `.claude/` pack**shipped**P6.3Gate runner + honest degradation**shipped**P6.4Automated / pair modes and the mid-run swap**shipped**P6.5The drush live-site surface and the standalone CLI**shipped**P6.6The MCP surface (optional submodule)**shipped**—Hardening: the phase→gate map, engine-counted retries, a coverage gate that can pass**shipped**—`droost/workflow` published on Packagist**this release**The lever file
--------------

[](#the-lever-file)

A single repo-root file, `droost.workflow.yml`, is the source of truth:

```
mode: automated                 # automated | pair
preset: custom                  # custom | factory | fast
phases: [plan, code, test, document, complete]
gates:
  phpcs:          { on: true,  standard: "Drupal,DrupalPractice" }
  phpstan:        { on: true,  level: 6 }      # 0-9 | max | off
  phpunit:        { on: true }
  mutation:       { on: false, msi_min: 0 }
  playwright:     { on: false }
  coverage:       { on: false, min: 0 }
  rendered_check: { on: true }                 # artifacts are truth
max_gate_retries: 2
```

**It is a file, not Drupal configuration**, for four reasons: the agent must be able to read its own levers while the site is mid-build or broken; a plain Claude Code or Codex user reads the same file with no site at all; it is dev tooling and belongs with the code it gates; and it belongs in review, where loosening a gate shows up as a diff.

### Presets

[](#presets)

A preset is a **base**, not an alternative to per-gate control — explicit `gates:` entries are applied over it, so "factory but without Playwright" is one line rather than a fork.

- **`factory`** — everything on, strict. The software factory.
- **`fast`** — coding standards and a shallow analysis; nothing that costs minutes.
- **`custom`** — the values shown above; the ergonomic middle, and what `init`writes for you.

Three details worth knowing:

- **Anything that does not name a preset resolves to `factory`** — no file, an empty file, or a file that sets other things but never mentions one. A repo that has said nothing has not opted out of anything. This is deliberately one rule rather than three: an earlier revision defaulted a file that exists to `custom`, which meant `touch droost.workflow.yml` turned mutation, playwright and coverage off and dropped PHPStan from max to 6, silently. If you want the gentler set, name it — `preset: custom` — so the choice is visible in a diff.
- **Thresholds never imply `on`.** Writing `coverage.min` without `coverage.on` leaves the gate where the preset put it. An inferred switch would make `min: 0` and `on: false` two spellings of one intent with two different failure modes. The single exception is `phpstan.level: off`, which the level vocabulary has always included: it turns the gate off, and pairing it with an explicit `on: true` is refused rather than silently resolved, so the recorded levers can never claim a gate ran when it did not.
- **Something at the config path that is not a readable regular file is an error**, not an absent config. A directory, a broken symlink or an unreadable file would otherwise swap your gates for the built-in ones and report nothing unusual.

### Gate options

[](#gate-options)

Most gates carry their thresholds inline — `phpcs.standard`, `phpstan.level`, `coverage.min`, `mutation.msi_min`. One is easy to miss: `rendered_check.routes` is a comma-separated list of internal paths the live surface renders (`routes: "/,/pricing"`); omitted, it renders `/`. The option vocabulary is closed per gate — anything else is refused by name.

### Unknown keys are errors

[](#unknown-keys-are-errors)

A loader that shrugs at `phpstain:` hands back a run with static analysis quietly disabled and a report that says everything passed. So every unknown setting, gate, option, phase, mode and preset is refused by name:

```
droost.workflow.yml: unknown gate "phpstain" (known: phpcs, phpstan, phpunit,
mutation, playwright, coverage, rendered_check)

```

Which gates run when
--------------------

[](#which-gates-run-when)

WHETHER a gate runs is the lever file's business. WHEN it runs is the engine's phase map, frozen into each run when it begins:

```
plan: none
code: phpcs, phpstan
test: phpunit, mutation, playwright, coverage, rendered_check
document: none
complete: phpcs, phpstan, phpunit, mutation, playwright, coverage, rendered_check

```

Plan and document run nothing — there is nothing yet to measure, and prose needs no linter. Code gates the diff with static analysis. Test runs the functional gates. Complete re-runs the full enabled set as the terminal safety net — which is what makes dropped phases safe: a run configured without a test phase still meets every enabled gate once, at the end.

The feedback loop
-----------------

[](#the-feedback-loop)

A blocking gate does not end a run; it starts a bounded loop. Each failing invocation spends one attempt per blocking gate — recorded in run state as `feedback_attempts`, measured against `max_gate_retries` — and the agent fixes the cause between invocations. `max_gate_retries: 2` means one attempt plus two retries; `0` means one attempt and no retry. A missing tool spends budget exactly like a failure, because a missing binary re-invoked forever is the worst infinite loop of all.

When the budget is spent, the phase is recorded **failed** — terminal — and `run` refuses to execute anything further. Every surface renders the same envelope: `{outcome, current_phase, report, awaiting, retries}`, where `retries.exhausted` separates "fix it and run again" from "this run is over". Exit codes stay simple — paused is not failed, and both kinds of failure exit non-zero. Recovery from a terminal failure is deliberate: remove `.droost-workflow/run.json` and begin again.

Run state
---------

[](#run-state)

Run state lives beside the lever file, in `.droost-workflow/run.json`:

```
{ "v": 1, "run_id": "...", "phases": { "plan": "passed", "code": "active" } }
```

On the filesystem for the same reason the levers are. If run state lived only in Drupal's State API, a run started against a live site could not be resumed, inspected, or even described from a plain checkout — and the two surfaces would be two pipelines sharing a name.

Writes go through a temporary file and a rename, so a run that dies mid-write leaves the previous state intact. A state file that cannot be parsed is never deleted or replaced: it is still evidence of what a run was doing.

Two limits on that, stated because an overstated durability promise is worse than a modest one: there is no `fsync`, so a process crash is covered but a power cut is not; and there is no locking, so two processes doing load-modify-save against one file will lose an update without either being told. Nothing is ever torn. The model is one run per repo.

**A phase that failed is never quietly recorded as passed.** Advancing stamps the phase you are leaving as passed, so advancing away from a failed or skipped phase is refused outright — clearing a failure has to be a deliberate act, not a side effect of moving on. Advancing backward, or advancing a run that has already reached its terminal gate, is refused for the same reason: a report has to be able to describe the run honestly.

The pack
--------

[](#the-pack)

The five phases ship as a `.claude/` pack — one skill per phase, two commands, and a shared partial on using droost. Installing it into a repo writes:

```
.claude/skills/workflow-{plan,code,test,document,complete}/SKILL.md
.claude/commands/workflow/{run,status}.md
.claude/partials/droost-usage.md
droost.workflow.yml          # only if you don't already have one

```

Each skill states four things: its entry gate, the work, its exit gate, and **what it can and cannot check without a booted site**. That last section is the point. Nearly every droost tool needs a running site, so a CLI run has real blind spots — and a run that hides them produces a report nobody should trust.

**Ownership is explicit.** Every directory the pack owns gets a `.droost-workflow-pack` marker. Re-running the installer refreshes those directories and nothing else; a directory without the marker belongs to you and is refused rather than overwritten. Your `droost.workflow.yml` is never refreshed at all — it is version-controlled intent you wrote, and resetting your gates on an unrelated re-install would be an unpleasant surprise.

Install
-------

[](#install)

One package, two ways to consume it:

- **As a library / CLI** — any PHP project, no Drupal required: `composer require --dev droost/workflow`, then `vendor/bin/droost-workflow init` writes the pack and a default lever file. This is all the standalone surface needs.
- **As a Drupal module** — in a composer-built Drupal project the same require lands it at `modules/contrib/droost_workflow`; enable the module for the drush commands, and the `droost_workflow_mcp` submodule (which is what depends on `mcp_server`) for the MCP surface.

The lever file's `preset` is a scalar (`preset: custom`) — there is no `presets:` block to configure; a preset is a base the `gates:` entries overlay.

Two surfaces, one pipeline
--------------------------

[](#two-surfaces-one-pipeline)

The same run, the same levers, the same report — whether or not there is a site.

```
# Standalone. Any Drupal repo, no booted site, nothing running.
vendor/bin/droost-workflow init
vendor/bin/droost-workflow status
vendor/bin/droost-workflow run

# Against a live site.
drush droost:workflow:status
drush droost:workflow:run
drush droost:workflow:answer "yes, continue"
drush droost:workflow:swap automated
```

The only thing that differs is what the site-dependent gates can say. Verified on a real Drupal 11.4.4 site:

Surface`rendered_check`live (drush)**passed** — "1 route(s) rendered", a real sub-request to `/`standalone**skipped, no site** — with the reason recordedThat difference is the entire point. The CLI surface is not a degraded live run pretending otherwise; it is a run that tells you exactly which checks it could not perform. Everything else — which gates ran, their verdicts, the phase, the advance decision — is identical, because both surfaces call one facade and differ only in which site driver they inject.

The MCP submodule (optional)
----------------------------

[](#the-mcp-submodule-optional)

`modules/droost_workflow_mcp` exposes the same engine over MCP, as a third and fourth front onto the one `WorkflowFacade`:

- **`droost_workflow_status`** — read-only. The resolved levers (which preset, which gates, and where that came from), the phase order with each phase's status, the latest tally, and whether the run is awaiting an answer.
- **`droost_workflow_run`** — drives the run: gates the current phase and advances when they pass. `answer` and `swap` are ARGUMENTS of this one tool rather than separate tools, because they are sub-operations of driving a run and every extra destructive tool is another separately allow-listed surface.

Both take an optional `project` (absolute path to the repository); omit it for the site's own root. A root that is not a directory comes back as a failure envelope naming the path — never an exception, which over JSON-RPC would tell the caller nothing it could act on.

It is a SUBMODULE so a plain consumer of this package never pulls the alpha `mcp_server`: enable it only if you want the MCP surface. It depends on `droost:droost`, `mcp_server:mcp_server` and this module.

`droost_workflow_run` is **STDIO/Drush-only** — gated on the transport alone, no `allow_*` flag, the same posture `droost_verify` has for the same risk class (it spawns the project's own analysis binaries). Its whole body runs inside droost's Fiber shield, because a run can reach the rendered check and Drupal's renderer suspends the fiber in a way the MCP SDK misreads as a dropped response.

**Analysing it.** `./scripts/lint` does NOT type-check this submodule, and says so on every run. Its types come from three packages this checkout cannot have (`drupal/droost` is unpublished; `mcp_server` and `mcp/sdk` exist only inside a site), so no portable `scanDirectories` list can resolve them. Analyse it where they live:

```
ddev exec "cd /var/www/contrib/droost/droost_workflow && \
  php vendor/bin/phpstan analyse -c phpstan-mcp-site.neon \
  --autoload-file /var/www/html/vendor/autoload.php"
```

That reaches everything except `$container->get()`'s return type, which needs `phpstan-drupal`; droost's own Tool plugins carry the identical gap for the identical reason.

Requirements
------------

[](#requirements)

PHP 8.3+. The engine's only runtime dependency is `symfony/yaml` — no Drupal bootstrap is required to read config or run state, which is what lets the same code serve both surfaces.

Development
-----------

[](#development)

```
composer install
scripts/lint            # phpcs, phpstan (level max), phpunit
scripts/lint src/Config # scope phpcs to a subset
```

All three legs are **hard**. A missing binary fails the gate rather than skipping it and says which invocation it could not run — an environment that cannot run a gate is a broken environment, not a gate that does not apply.

License
-------

[](#license)

GPL-2.0-or-later.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/2007639?v=4)[Chad Peppers](/maintainers/chadmandoo)[@chadmandoo](https://github.com/chadmandoo)

---

Top Contributors

[![chadmandoo](https://avatars.githubusercontent.com/u/2007639?v=4)](https://github.com/chadmandoo "chadmandoo (16 commits)")

---

Tags

drupalworkflowAgentpipelinedroost

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/droost-workflow/health.svg)

```
[![Health](https://phpackages.com/badges/droost-workflow/health.svg)](https://phpackages.com/packages/droost-workflow)
```

###  Alternatives

[craftcms/cms

Craft CMS

3.6k3.7M3.4k](/packages/craftcms-cms)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k20](/packages/tempest-framework)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

12017.1k](/packages/rcsofttech-audit-trail-bundle)[friendsoftypo3/content-blocks

TYPO3 CMS Content Blocks - Content Types API | Define reusable components via YAML

103574.3k68](/packages/friendsoftypo3-content-blocks)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1617.3k16](/packages/2lenet-crudit-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
