0.10.0: tools on request, your BC promise, and an agent that drives

· Release · written for 0.10.0

0.10.0 is about proposing the right things, and fewer of them. Some tools are now set up only when you ask, the suite learns what your project promises to the code outside it, and a run can be driven by another agent. It has one breaking change, for a Pack of your own only: a Stage's after is now waits_for. Everything else needs nothing from you; what you will notice is listed at the end, in What changes for your project.

Tools on request

Up to 0.9, every scan proposed every tool that was neither in place nor declined. For some tools that was noise. Semgrep finds a lot in code that grew for years without guardrails, and almost nothing in a young project that had them from the start. Mutation testing runs your tests hundreds of times, which only pays off once they are good enough to judge.

So a few tools are now on request: Semgrep, Psalm's taint analysis, Secret Detection, and the new mutation testing with Infection. They are never offered unasked and never block a Track. Every scan report names what is available on request; name one in the call and it is proposed like any other tool:

/continuous-refactoring set up Semgrep
/continuous-refactoring scan including the tools on request

Two tools changed their place rather than their status:

  • Composer Audit is gone as a tool. A pipeline check only fails on advisories known when the code changes; one published next month against the same lockfile is never seen. The first Housekeeping template now carries a recurring dependency audit, for any package manager, and audits on every cycle.
  • The secret scan no longer waits for a scanner in your project. Housekeeping uses the one you have, or runs one of its own (gitleaks, for instance) without adding anything to your project. A forge's push protection counts as Secret Detection being in place.

The diagrams on the Pack pages now show a tool's status as the outline of its steps, dashed for on request, dotted for recognized, with a legend below. See Pack php.

Your BC promise

Whether the suite may delete an old API depends on who else uses it. Up to 0.9 it guessed: where it could not tell, it assumed someone depends on it. Now it asks. The BC promise is your project's own document saying what stays stable for code outside the project, your package's users or your application's addons, and how a breaking change is allowed: a new major version under SemVer, or a deprecation period counted in releases and months, as some applications promise their addons. The suite does not model either; it reads your document.

  • Onboarding asks for it, and a project onboarded before is asked once at the start of its next run. The answer, a path or none, goes into docs/agents/issue-tracker.md.
  • Code the promise covers is restructured with old and new side by side, and the old path is deprecated the way your promise says. Its removal is proposed later as an ordinary ticket, never made by a refactoring.
  • Under a SemVer promise with a tagged release, the Safety Net gains Roave's Backward Compatibility Check, run in CI before the suite's restructurings start. Rector's type coverage waits for it.
  • Where your promise covers extending non-final classes, or is not settled yet, Rector's type coverage takes a BC-safe path. Adding a parameter or return type to a method breaks every class outside your project that overrides it. So the suite switches off Rector's own type-declaration set and uses art4/rector-bc-library instead, a library I maintain: the same rules, applied only where the change is not a BC break. It comes in as a dev dependency and supports the same adoption in levels.

The FAQ has the reasoning.

Tests that say what they cover

A coverage report counts a class as covered when any test runs it, also a test of another class that merely passes through. The suite reads those numbers to find code without tests, so such a class looked tested when nobody tested it. After the coverage floor, two more steps follow for PHP projects:

  • Every test names the class it covers.
  • Then, on PHPUnit 10 or newer, also what it only uses, so a test that runs code it does not name fails. Integration and functional tests are marked as medium and large tests and need no such list. Pest has no way to name what a test uses, so it stops at the first step.

Counting only what a test means to test can lower the measured coverage. The merge request writes the missing tests; where that does not fit, it sets the floor once to the newly measured value and says so.

Two related changes: a restructuring that creates a new class now gives it a test of its own, written first, instead of relying on the old code's test staying green. And before opening a merge request, a run executes every gate your CI runs over the whole project, such as the coverage floor, in full, and names any it cannot run locally.

Rector before PHPStan, and never above your PHP

  • PHPStan Level 4 waits for Rector's Dead Code set, Level 5 for Type Coverage. Rector clears part of those levels' baselines mechanically, and every finding it clears is one nobody fixes by hand. Declining the Rector set releases the level.
  • Rector's PHP set stops at the PHP version your composer.json declares. Rewriting the code for a newer PHP is the same promise as raising the minimum, and that is a product decision. The "PHP Version Floor" step is gone with it; the suite no longer raises your declared minimum.
  • Rector's PHPUnit set is a Guardrails Stage and no longer holds the Safety Net back.

An agent that drives the runs

/continuous-refactoring can now be started by an agent, as /continuous-housekeeping already could. The new Driver playbook describes such an agent: it calls one autonomous run after another, each in a subagent of its own so every run starts with a fresh context, reviews each merge request in a second subagent, merges what passes (or only comments, if you choose "review only"), and asks you only where a decision is yours. The playbook ends with a prompt to copy into your project.

Three changes make that work, and help any run:

  • A named ticket narrows the run. "ticket 42" no longer searches every open ticket first, and reconcile checks only that ticket's merge request. A run started for one ticket writes nothing to the others.
  • An open question is answered by content, not by author. The wait ends at the first comment that is neither the suite's plan nor another of its questions, whichever account wrote it, since agents often post with your own. An answer that declines the ticket is recorded as a rejection.
  • A tool whose judgement stays open is filed with its question on the new ticket, so an autonomous run no longer skips it and raises it again on every scan.

Known limitations names the two catches: an agent's comment on a waiting ticket counts as your answer, and a run inside a subagent does its review steps in its own context.

What changes for your project

One thing to do, only if you have a Pack of your own: rename each Stage's after key to waits_for. A Pack that still says after is refused with a message saying so. While you are there: a Tool of yours that gains a second Stage gets a slug of its own; rename the Tool, not the Stage, so your tickets and rejections keep their names.

What you will notice:

  • The next run asks about your BC promise, once, and goes on.
  • An open "Composer Audit" or "PHP Version Floor" ticket is offered for closing, with a note saying what took its place. A recorded rejection of the PHP Version Floor is simply ignored.
  • Your Housekeeping template gains the dependency audit on its next cycle.
  • Open tickets for Semgrep, Psalm Taint Analysis or Secret Detection stay and are worked as before; only new scans stop proposing them.
  • The next scan may propose new steps: the coverage-metadata Stages, and the Backward Compatibility Check if your promise follows SemVer.

The FAQ explains the reasoning, for example why some tools are only set up on request and why Rector stops at your PHP version.

If you let a Driver loose on a project of yours, I would love to read how it went in GitHub Discussions (Show and tell).

Changelog 0.10.0

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

Show changelog entries
  • Semgrep and Psalm's taint analysis are no longer proposed to every project: they are now set up only when you ask for them, by naming one in the call ("set up Semgrep") or by asking for a scan including the tools on request, and each scan report names what is available. Composer Audit is no longer a tool to set up: the first Housekeeping template now carries a recurring dependency audit for any package manager, an existing template gains it on its next cycle, and an open "Composer Audit" or "PHP Version Floor" ticket is offered for closing with a note saying what took its place. The diagrams under docs/tooling now show a tool's status as the outline of its steps — dashed for on request, dotted for recognized — with a legend below.
  • Secret Detection is now set up only on request, and the secret scan over the Git history no longer waits for it: Housekeeping uses your project's own secret scanner, or runs one of its own (gitleaks, for instance) without adding anything to your project. A forge's push protection for secrets now counts as Secret Detection being in place, and the scanner's findings no longer feed the Security signal of restructuring candidates.
  • Mutation testing joins the tooling, on request: ask for Infection and the suite sets it up after your coverage floor, with a minimum score that only rises, a pipeline check on the changed files and a full run in each Housekeeping cycle; the faults your tests miss then point the search for restructurings at code that is run but not really tested. A project that already runs Pest's own mutation testing has it in place. A tool on request that needs a newer PHP than your project allows is no longer reported as out of reach until you ask for it.
  • The suite now asks whether your project promises stability to code outside it — your package's users, your application's addons — and records where that promise is written (or that there is none) in docs/agents/issue-tracker.md. Onboarding asks it; a project onboarded before is asked once at the start of its next run. Code the promise covers is restructured with old and new side by side and the old path deprecated the way your promise says; its removal is proposed as an ordinary ticket, never made by a refactoring.
  • A project whose BC promise follows SemVer and that has tagged a release now gets Roave's Backward Compatibility Check in its Safety Net, run in CI from its Docker image, before the suite's restructurings start. Rector's type coverage waits for it, and where your promise covers extending non-final classes it takes the BC-safe art4/rector-bc-library set instead of Rector's own, so an added type does not break a subclass in someone else's code.
  • After the coverage floor, the suite now proposes two more steps for PHP projects: every test says which class it covers, and then — on PHPUnit 10 or newer, not on Pest — also what it only uses, so coverage counts only what a test means to test and a class without a test of its own shows up as untested. Integration and functional tests are marked as medium and large tests and need no list of what they use. The merge request writes the tests a class lacks; where that does not fit, it sets the coverage floor once to the newly measured value and says so. A Pack of your own whose Tool gains a second Stage is now told to rename the Tool, not the Stage, so tickets and rejections on record keep their name.
  • Rector's PHP set now goes up to the PHP version your composer.json declares and never above it, and the "PHP Version Floor" step is gone: the suite no longer raises your declared minimum PHP after rewriting the code for a newer one. A rejection of that step you recorded earlier is ignored.
  • PHPStan Level 4 now waits until Rector Dead Code is decided, and Level 5 until Rector Type Coverage is, so the rewrites shrink those levels' baselines before they are fixed by hand; Rector's PHPUnit Set is now a Guardrails Stage and no longer holds the Safety Net back.
  • A Stage's after field in a Pack is now named waits_for ("B waits for A"), and the generated tooling tables call the column "Waits for". A Pack of your own that still says after is refused with a message asking you to rename the key.
  • A structural change that creates a new class or other type reachable from outside now gives it a test of its own, written first, instead of relying on the old code's test staying green; and before opening a merge request a run now executes every gate your CI applies to the whole project, such as a coverage floor, in full, naming any it cannot run locally.
  • A run whose call names a ticket now looks at that ticket alone: it no longer searches every open ticket first, and reconcile checks only that ticket's merge request, so it writes nothing to tickets you did not name.
  • An open question on a ticket now ends at the first comment that is neither the suite's plan nor another of its questions, whichever account wrote it, and an answer that declines the ticket is recorded as a rejection.
  • A proposed tool whose judgement stays open is now filed with its question as an open question on the new ticket, so an autonomous run no longer leaves it unfiled and raises it again on every scan.
  • An agent may now start continuous-refactoring by itself, as it already could continuous-housekeeping — for example one that hands each run to a subagent and reviews its merge request before calling the next.
  • A new Driver playbook describes an agent that calls runs one after another, each in a subagent of its own, reviews and merges each merge request in between, and asks you only where a decision is yours — with a prompt to copy into your project.
  • Known limitations now name two effects of letting an agent drive runs: its comments on a waiting ticket count as your answer, and a run inside a subagent does every step in its own context.
  • docs/adr/README.md lists every decision with its status — in force, amended, superseded in part or superseded — generated from the ADRs' banners, which the validator now keeps consistent on both sides of every relation.

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