Walkthrough

Step 04 — Do: build the fix

03 Plan · Index · next: 05 Check →

Beat: Do. Fully automated — no human touch point. The builder leaf reads the PLANNED bundle's brief.md (and nothing else) and produces the change. When patch.diff lands, the bundle becomes BUILT.

How Do runs

The builder leaf is headless (interactive = false) — it runs unattended as part of make flow, so there's nothing to type. From gramps' pdca.toml:

[leaves.builder]
mode = "command"
interactive = false
argv = ["claude", "-p", "--agent", "builder", "--permission-mode", "acceptEdits",
        "--allowedTools", "Read,Edit,Bash(git *),Bash(python3 *)"]

The narrow --allowedTools is deliberate: the builder may read, edit, and run git/python — it cannot, say, open a PR. STOP discipline (step 03) is enforced by what the leaf can't do, not just by instruction.

What Do produces

Three artifacts land in the bundle:

Artifact Purpose Who sees it
patch.diff The change itself Check gates, reviewer, you
the test file The red→green proof, shipped at the brief's Test file path C4 gate runs it
build-notes.md The builder's rationale — why this approach, trade-offs considered You at sign-off — withheld from the reviewer

That last withholding is a real design decision, not an accident: the Check reviewer (step 05) sees only {patch.diff, test, brief.md, check-gates.json}. If the reviewer could read the builder's self-justification, it would anchor on the builder's framing instead of judging the diff cold. The human signing off does get build-notes.md, because the human is adjudicating, not independently re-deriving.

The real patch

Here's the heart of the actual results/issue_11589/patch.diff. The brief said "delete only the selected plugin's own files when the directory is shared"; the builder added two helpers and rerouted __uninstall:

--- a/PluginManager/PluginManager.py
+++ b/PluginManager/PluginManager.py
@@ -336,11 +336,52 @@ class PluginStatus(tool.Tool, ManagedWindow):
+    def __plugins_sharing_dir(self, pdata):
+        """Return the registered plugins, other than *pdata*, whose files live
+        in the same directory (``fpath``). ... (bug 11589)."""
+        shared = {}
+        for ptype in PTYPE_STR:
+            for other in self._preg.type_plugins(ptype):
+                if other.id != pdata.id and other.fpath == pdata.fpath:
+                    shared[other.id] = other
+        return list(shared.values())
+
+    def __remove_plugin_files(self, pdata, siblings):
+        """Delete only the files belonging to *pdata* ... leaving the shared
+        directory and everything else in it intact."""
+        if pdata.fname in {other.fname for other in siblings}:
+            return
+        base = os.path.splitext(pdata.fname)[0]
+        for fname in (base + ".gpr.py", pdata.fname):
+            target = os.path.join(pdata.fpath, fname)
+            if os.path.isfile(target):
+                os.remove(target)
+
     def __uninstall(self, pid, path):
         """Uninstall the plugin"""
         pdata = self._pmgr.get_plugin(pid)
+        siblings = self.__plugins_sharing_dir(pdata)
         try:
-            if os.path.islink(pdata.fpath):  # linux link
+            if siblings:
+                # The directory is shared by other registered plugins (e.g. the
+                # multi-rule FilterRules pack). Removing the whole directory
+                # would destroy those siblings ... so remove only this plugin's
+                # own files (bug 11589).
+                self.__remove_plugin_files(pdata, siblings)
+            elif os.path.islink(pdata.fpath):  # linux link

Two things to note, both traceable straight back to the brief:

  • The success criterion's two halves are both honoured. Shared directory → remove only own files (new branch); sole occupant → fall through to the old rmtree (the elif). The brief explicitly demanded the sole-occupant case "still remove the directory, preserving today's behaviour" — so the builder kept the old path instead of replacing it.
  • The fix targets the root cause named in the brief (shutil.rmtree on a shared fpath), not the symptom. That's what Check's C5 "causal adequacy" will probe.

The companion test ships at exactly the path the brief named — PluginManager/tests/test_uninstall_shared_dir.py — so the C4 gate has something to run.

The bundle is now BUILT. The driver moves straight into Check — step 05 — with no pause.

03 Plan · Index · next: 05 Check →