0.9.0: tooling that fits your project – roles, ratchets, Packs

· Release · written for 0.9.0

0.9.0 is about which tools the suite proposes. The tooling tree is rebuilt so that it understands tools that do the same job, other languages, and tools of your own. It is not a breaking release. A project on 0.7 or 0.8 has nothing to do before it calls the suite again; what you will notice is listed at the end, in What changes for your project.

Why the tooling tree had to change

Up to 0.8 the tooling tree was a graph of adoption steps, built for PHP. Using it on real projects showed where it did not hold:

  • A tooling tree of your own was hard to build. Adding a tool meant changes in several places, sometimes in the parser itself. A project could not bring its own tools without changing the suite.
  • A tool could not be tightened later. A tool came in with the Safety Net, and from then on its node was either done or not. There was no way to introduce it early and tighten it step by step afterwards, which is what a ratchet does: no new violations, the old ones shrink, in one direction only.

The second point led to a contradiction. Structural work, the Investigation Track, always waits for the Safety Net. But some ratchets could only be tightened with a restructuring. So the Safety Net waited for structural work, and structural work waited for the Safety Net.

Two smaller problems came on top:

  • Tools that do the same job. PHPStan or Psalm took several helper nodes and special cases, and Laravel Pint had no place at all.
  • Anything that is not PHP. On a project in another language, the Safety Net could never be fulfilled, because nearly all of its tools were PHP tools.

Roles, Tools, Stages and Packs

0.9.0 replaces the graph with four terms:

  • A Role is what a tool is for: Static Analysis, Code Style, Testing.
  • A Tool plays one or more Roles. It is either recommended (proposed when its Role is empty) or recognized (counted when it is there, never proposed).
  • A Stage is one step of a Tool, such as a PHPStan level or a Rector set. Tickets are filed per Stage, as before per node, and their names stay the same.
  • A Pack is a set of Tools with a sentence saying when it applies. There is a generic Pack, a PHP Pack and, new, a Twig Pack.

Beside these, the ratchet is now a property of a Tool: the PHPStan baseline, the test coverage floor, PHPMD's limits.

The graph itself is gone. With every Stage as a node and every dependency as an edge, it would have become too complicated to understand. Each Pack is now a JSON file, pack.json, which the suite reads, and the prose for each Tool sits beside it in a Markdown file.

What this changes in practice:

  • Introduce early, tighten later. A Tool comes in with the Safety Net; tightening it is a later Stage in Guardrails, which may need structural work and no longer holds the Safety Net back. Psalm Taint Analysis moved to Guardrails for exactly this reason: its findings can need a restructuring to fix. PHP CS Fixer and PHPMD were split into two Stages the same way (see below).
  • A filled Role is respected. A project on Psalm has Static Analysis filled and is not offered PHPStan. A project on Laravel Pint, Easy Coding Standard or PHP_CodeSniffer has Code Style filled and is not offered PHP CS Fixer.
  • You can decline more precisely. A rejection can name a single Stage, a whole Tool, or a whole Role. Declining a Tool leaves its Role open for another one.
  • A project that is not PHP can now have a fulfilled Safety Net from the generic Tools: a CI pipeline and .editorconfig.
  • Twig templates in a PHP project get their own Tools: Twig-CS-Fixer for code style and TwigStan for static analysis. They stand beside the PHP Tools, because a Role is filled per language: a code-style tool for Twig does not take the place of one for PHP.

For humans, pages are generated from the Packs: the tooling overview lists the Roles and Packs, and each Pack has a page with a diagram, for example PHP.

A Pack of your own

A tool the suite does not know no longer needs a change to the suite. Your project can keep a Pack of its own: a pack.json with the Tool and its Stages, and a Markdown file per Tool saying when a Stage is fulfilled. Name its folder in a new optional Tooling Pack bullet in your ## Refactoring operations section, and the suite proposes your Stages like its own.

Your Pack can add Tools, Stages and Roles, and wait for the suite's Stages. It cannot redefine them: a clash ends the run's tooling step with an error that names both places. A Tool of yours that does the job of one of the suite's stands beside it; decline the suite's if you prefer yours. The playbook has a worked example with Deptrac.

The format of pack.json is described by a JSON Schema, pack.schema.json, which ships with the suite. Point your file's "$schema" line at it in your installed skills, and your editor checks and completes the file as you write it.

Note that the Pack format is still in development and will change. A Pack of your own may need adjusting after a later release; the changelog will say what changes.

Large projects: PHP CS Fixer and PHPMD in two Stages

On a project with thousands of files, one formatting pass is a merge request nobody can review. So PHP CS Fixer now comes in two Stages:

  • The adoption pins a PER Coding Style version, proposes extra rules you may decline, and takes in as many rules as one merge request can carry. The others are switched off with a marker. Rector waits only for this first Stage.
  • The rollout, a Guardrails Stage, takes in the remaining rules one at a time, path by path, through a second config file.

PHPMD gets the same split, and loses its baseline. A baseline only lists rule, file and method, with no value and no reason, so a method that gets worse after it was baselined stays quiet. Instead, each metric limit is set to the worst value the code has today, with a comment, and lowered whenever the work removes that value. The limits themselves are the record of the debt. A deliberate exception is suppressed at the code, with its reason beside it.

Other agents, and marking a ticket ready

  • The suite no longer names Claude Code's question tool. It asks for the multiple-choice question tool of whatever agent runs it, and falls back to numbered options in chat. So questions now show up properly in Cursor and OpenCode too.
  • A new optional Ready after plan bullet in your tracker file says how your project marks a ticket ready for implementation, for example a label or a board column. The suite runs it right after it posts a plan. Without the bullet, nothing changes. The suite still never sets a status when work starts.

What changes for your project

Nothing you have to do. What you will notice:

  • Your existing tickets and rejections stay valid. Stages keep the names of the old nodes.
  • The next scan may propose new things: Stages it has not judged on your project before, such as the PHP CS Fixer and PHPMD rollouts, or the Twig Tools if your project uses Twig.
  • A project on Psalm, Pint, ECS or PHP_CodeSniffer is no longer offered the competing tool.
  • A project with a PHPMD baseline gets one merge request that replaces it with limits and suppressions.

Optional, if you want them: a Tooling Pack for your own tools, and Ready after plan for your tracker's workflow.

The FAQ explains the reasoning, for example why the suite thinks in roles and tools and why PHPMD has no baseline.

If you build a Pack of your own, or improve something with what 0.9 makes possible, I would love to read about it in GitHub Discussions (Show and tell).

Changelog 0.9.0

The upstream changelog entries for 0.9.0, verbatim. Also on GitHub: release 0.9.0.

Show changelog entries
  • The tooling tree is now Roles, Tools and Stages delivered in Packs: a project on Psalm is no longer offered PHPStan, a project that is not PHP can have a fulfilled Safety Net from the language-neutral tools, a rejection can name a whole tool or role, and a tool the suite learns about later is picked up on the next run.
  • The tools the suite knows are now drawn for humans: docs/tooling/ lists the Roles and, per Pack, the Tools and Stages with a diagram of what requires what. The pages are generated from the Packs, and the validator now fails on a Pack that breaks its schema, is not in the fixed form, has two recommended tools for one Role, names a missing details file, or whose page is out of date.
  • The prose for a tool now sits beside its Pack: the generic Pack's tools, the suite's two prerequisites and the smaller PHP tools moved to packs/<pack>/<tool>.md, without the history of how they used to be wired. The PHP Pack also knows Laravel Pint and Easy Coding Standard now: a project on either has its code style settled, and PHP CS Fixer is not proposed to it.
  • The last tool files (PHPStan, Psalm, Rector) moved into the PHP Pack and lost the description of how the tools used to be wired; the two tooling-tree documents are gone. Every tool now has one file beside its Pack, and the Pack pages under docs/tooling/ link to it.
  • A project can keep a Pack of its own for tools the suite does not know: a pack.json and a file per tool, named in a new optional Tooling Pack operation of its tracker file. Its steps are proposed like the suite's own; it can add tools, steps and roles, never redefine the suite's, and a clash ends the run's tooling step with an error that names both places. A Pack now names a scope (PHP, Twig, …): tools take each other's place only within a scope, so a code-style tool for Twig no longer competes with one for PHP. See the new playbook on adding a tool of your own.
  • A new Pack for Twig in PHP projects: it applies when the PHP project requires twig/twig (the file extension of the templates does not decide) and brings Twig-CS-Fixer for the Code Style of the templates and TwigStan for their Static Analysis. Both stand beside the PHP Tools and do not take their place.
  • PHP_CodeSniffer is now a recognized Tool for the Role Code Style: a target that runs phpcs/phpcbf has the Role filled, so PHP CS Fixer is no longer proposed on top of it.
  • The PHP Pack Stage "PHP Minimum Version" is now called "PHP Version Floor", matching its Role Runtime Floor and the terms Floor correction and Floor raise; its slug php-minimal-version is unchanged.
  • Psalm Taint Analysis is now a Guardrails Stage instead of a Safety Net one: its findings can need a restructuring to fix, and that restructuring waits for the Safety Net, so the Stage no longer holds the Safety Net back and no longer has to be skipped to pass it.
  • Twig-CS-Fixer is now a Guardrails Stage instead of a Safety Net one: a style pass over Twig templates no longer holds the Safety Net, and with it every PHP Guardrails Stage, back. TwigStan still waits for it.
  • In the PHP Pack diagram, the Static Analysis Role now sits inside the Safety Net group instead of outside it.
  • PHP CS Fixer now comes in two Stages, so a project with thousands of files gets its code style in merge requests a person can review. The adoption (php-cs-fixer-adoption, renamed from php-cs-fixer) pins a PER Coding Style version, proposes a list of extra rules you may decline, checks the project's lowest PHP version, and takes in as many rules as one merge request can carry; it alone releases Rector. The new Guardrails Stage php-cs-fixer-rollout takes in the rules left Not yet: one at a time, path by path, through a second config file, as the ticket "PHP CS Fixer: take in the remaining rules". A Housekeeping line looks for a newer PER Coding Style version.
  • PHPMD no longer starts with a baseline: the limits of its metric rules are set to the worst value the code has today, with a comment, and fall whenever a refactoring removes that worst value. Deliberate exceptions are suppressed at the code with a reason, the five rule sets (now with design) are the default, and a project that already has a PHPMD baseline gets one merge request that replaces it.
  • PHPMD now comes in two Stages, like PHP CS Fixer. The adoption (phpmd-adoption, renamed from phpmd) installs the tool, sets the limits, takes in the rules one merge request can carry and adds the CI job; rules left Not yet: no longer keep it unfulfilled. The new Guardrails Stage phpmd-rollout takes in those rules one at a time, as the ticket "PHPMD: take in the remaining rules". A rule too large for one merge request is taken in directory by directory through a second ruleset, phpmd.rollout.xml, which CI runs over the directories already done, so they cannot slip back before the rule is in everywhere.
  • A Stage's after may now name a Role as well as a Stage. The Rector Stages wait for the Role Code Style instead of PHP CS Fixer's adoption, so the tooling diagram shows PHP CS Fixer, Laravel Pint, Easy Coding Standard and PHP_CodeSniffer feeding one Code Style node that Rector waits on. When Rector is released does not change: a Role counts as decided once it is filled, or once every Tool that could fill it is decided. A target's own Pack can use the same form.
  • The suite no longer names one agent's question tool: it asks for the environment's multiple-choice question tool (Claude Code, Cursor and OpenCode each have one under a different name) and falls back to numbered options in chat when there is none, so questions are shown in Cursor and OpenCode too. A tool reply that no human gave counts as unanswered.
  • A new optional operation, Ready after plan, lets a target mark a ticket ready for implementation (for example a label or a board column) right after the suite posts a plan on it. Without the bullet nothing changes.

Questions or comments on this post? Welcome in GitHub Discussions (Q&A).