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(theelif). 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.rmtreeon a sharedfpath), 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 →