Skip to content

bug(quiet-tools): filtering quiet-tools disables rose lifecycle renderer #525

Description

@e-Evolution

Problem

The rose lifecycle renderer currently shares extensions/quiet-tools.ts with the non-Bash quiet-tool overrides. When a Pi package filters that extension to preserve another tool owner, it also loses the Gentle AI Bash renderer even though command execution remains available.

The result is a generic Bash row instead of the established lifecycle presentation:

🌹︎ Gentle AI · running|completed|failed · <safe operation path>

Existing context and duplicate search

This issue is a narrow follow-up: preserve the already-established renderer when extensions/quiet-tools.ts is intentionally filtered.

Reproduction

  1. Load Gentle Pi as a package while filtering extensions/quiet-tools.ts to avoid replacing another registered tool owner.
  2. Invoke a recognized direct Gentle AI command through Pi's Bash tool, such as gentle-ai --version or gentle-ai review status ....
  3. Observe that the command executes, but the rose lifecycle renderer is unavailable.

Expected behavior

Filtering the non-Bash quiet tools should not disable Gentle AI Bash lifecycle rendering. Bash should have exactly one renderer owner, and the filtered extension must not re-register edit or other quiet tools.

Proposed fix

  • Add a standalone extensions/gentle-ai-bash.ts extension.
  • Export dedicated Bash registration from extensions/quiet-tools.ts.
  • Make the default quiet-tools registration skip Bash to prevent duplicate ownership.
  • Add focused coverage proving independent Bash loading, preserved edit ownership, and unchanged default registration.
  • Load the standalone extension in the runtime harness.

Acceptance criteria

  • The rose lifecycle renderer loads when extensions/quiet-tools.ts is filtered.
  • The existing edit tool remains the sole edit owner.
  • Bash is registered exactly once.
  • Default quiet-tools loading still registers bash, edit, find, grep, ls, read, and write.
  • The runtime harness loads the standalone extension.
  • Existing command-safety and redaction behavior remains unchanged.

Validation evidence

Prepared locally as commit c8e21ec55 (fix(quiet-tools): load rose renderer independently).

  • Targeted renderer behavior: 1 passed, 0 failed.
  • Runtime harness: passed using node --experimental-strip-types tests/runtime-harness.mjs.
  • git diff --check origin/main: passed.
  • Scope: 4 files, 55 insertions, 4 deletions.
  • The full quiet-tool test file also collected 47 tests: 46 passed; one unchanged macOS path-alias assertion compared /tmp with /private/tmp. That unrelated baseline hunk is deliberately excluded from this patch.

Proposed code diff

diff --git a/extensions/gentle-ai-bash.ts b/extensions/gentle-ai-bash.ts
new file mode 100644
index 000000000..95ab92822
--- /dev/null
+++ b/extensions/gentle-ai-bash.ts
@@ -0,0 +1,6 @@
+import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
+import { registerGentleAiBashRenderer } from "./quiet-tools.ts";
+
+export default function gentleAiBash(pi: ExtensionAPI): void {
+	registerGentleAiBashRenderer(pi);
+}
diff --git a/extensions/quiet-tools.ts b/extensions/quiet-tools.ts
index a3a99ba60..1cbaa20ee 100644
--- a/extensions/quiet-tools.ts
+++ b/extensions/quiet-tools.ts
@@ -693,9 +693,17 @@ function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArg
 	});
 }
 
+function registerGentleAiBashRendererWithResolver(pi: ExtensionAPI, resolveOverride: GentleAiDevBinaryOverrideResolver): void {
+	registerQuietTool(pi, "bash", () => createGentleAiCommandArguments(resolveQuietToolsDevBinaryPath(resolveOverride)));
+}
+
+export function registerGentleAiBashRenderer(pi: ExtensionAPI, resolveOverride: GentleAiDevBinaryOverrideResolver = () => resolveGentleAiDevBinaryOverride()): void {
+	registerGentleAiBashRendererWithResolver(pi, resolveOverride);
+}
+
 export default function quietTools(pi: ExtensionAPI, resolveOverride: GentleAiDevBinaryOverrideResolver = () => resolveGentleAiDevBinaryOverride()): void {
 	if (!quietToolsEnabled()) return;
 	for (const toolName of Object.keys(TOOL_CREATORS) as QuietToolName[]) {
-		registerQuietTool(pi, toolName, () => createGentleAiCommandArguments(resolveQuietToolsDevBinaryPath(resolveOverride)));
+		if (toolName !== "bash") registerQuietTool(pi, toolName, () => createGentleAiCommandArguments(resolveQuietToolsDevBinaryPath(resolveOverride)));
 	}
 }
diff --git a/tests/quiet-tool-rendering.test.ts b/tests/quiet-tool-rendering.test.ts
index 6d01b2f11..7df478e54 100644
--- a/tests/quiet-tool-rendering.test.ts
+++ b/tests/quiet-tool-rendering.test.ts
@@ -3,11 +3,13 @@ import test from "node:test";
 import { initTheme, keyHint } from "@earendil-works/pi-coding-agent";
 import { imageFallback, visibleWidth } from "@earendil-works/pi-tui";
 import piPretty from "../extensions/pi-pretty.ts";
-import quietTools, {
+import gentleAiBash from "../extensions/gentle-ai-bash.ts";
+import quietToolsExtension, {
 	countNonEmptyLines,
 	extractTextContent,
 	formatToolResultOutput,
 	gentleAiRoutineCommand,
+	registerGentleAiBashRenderer,
 	tailLines,
 } from "../extensions/quiet-tools.ts";
 
@@ -138,15 +140,27 @@ async function withEnvAsync<T>(updates: Record<string, string | undefined>, run:
 	}
 }
 
+function quietTools(pi: any, resolveOverride?: any): void {
+	quietToolsExtension(pi, resolveOverride);
+	if (process.env.GENTLE_PI_QUIET_TOOLS !== "0") gentleAiBash(pi);
+}
+
+function loadQuietTools(pi: ReturnType<typeof createPi>["pi"]): void {
+	quietTools(pi);
+}
+
 function registeredQuietTools() {
 	const { pi, tools } = createPi();
-	withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
+	withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => loadQuietTools(pi));
 	return tools;
 }
 
 function registeredQuietToolsWithResolver(resolveOverride: () => unknown) {
 	const { pi, tools } = createPi();
-	withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any, resolveOverride as any));
+	withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => {
+		quietToolsExtension(pi as any, resolveOverride as any);
+		registerGentleAiBashRenderer(pi as any, resolveOverride as any);
+	});
 	return tools;
 }
 
@@ -162,6 +176,28 @@ function assertGenericBash(tool: any, command: string): void {
 	assert.match(output, /original command output/);
 }
 
+test("Gentle AI Bash rendering loads independently while smart-edit remains the sole edit owner", () => {
+	const smartEdit = createSdkTool("edit");
+	const { pi, tools } = createPi({ throwOnToolConflict: true });
+	pi.registerTool(smartEdit);
+
+	withEnv({ GENTLE_PI_QUIET_TOOLS: "0" }, () => {
+		quietToolsExtension(pi as any);
+		gentleAiBash(pi as any);
+	});
+
+	assert.strictEqual(tools.get("edit"), smartEdit);
+	assert.ok(tools.has("bash"), "independent extension must own the Gentle AI Bash renderer");
+	assert.equal(tools.size, 2, "quiet-tools exclusion must not register another edit owner");
+	const bash = tools.get("bash");
+	const command = "gentle-ai review status --lineage private";
+	assert.match(renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } })), /🌹︎ Gentle AI · running · review status/);
+
+	const defaultLoad = createPi({ throwOnToolConflict: true });
+	withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(defaultLoad.pi as any));
+	assert.deepEqual([...defaultLoad.tools.keys()].sort(), ["bash", "edit", "find", "grep", "ls", "read", "write"]);
+});
+
 test("quiet tool rendering registers noisy built-in tools", () => {
 	withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => {
 		const { pi, tools } = createPi();
diff --git a/tests/runtime-harness.mjs b/tests/runtime-harness.mjs
index 8cf1ae546..5f3a3f83e 100644
--- a/tests/runtime-harness.mjs
+++ b/tests/runtime-harness.mjs
@@ -15,6 +15,7 @@ import { domainHashV1 } from "../lib/review-canonical.ts";
 const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
 const EXTENSIONS = [
 	"extensions/gentle-ai.ts",
+	"extensions/gentle-ai-bash.ts",
 	"extensions/quiet-tools.ts",
 	"extensions/skill-registry.ts",
 	"extensions/sdd-init.ts",

PR authorization request

Please review this bounded patch and apply status:approved if the behavior and scope are accepted. After maintainer approval, the contributor will push the prepared branch and open a PR with Closes #<this-issue> and exactly one type:bug label.

Activity

  1. e-Evolution commented on Sep 2, 2026

    @e-Evolution
    Author

    Closing this because the reproduction came from an optional extension conflict in my configuration, not from the default Gentle Pi setup.

    I had @aboutlo/pi-smart-edit installed, which also registers the edit tool. To avoid duplicate tool ownership, I filtered extensions/quiet-tools.ts; that filter also removed Gentle Pi’s rose renderer. After removing pi-smart-edit and restoring unfiltered gentle-pi@2.3.0, fresh Pi startup, native edit, ordinary Bash, and the rose lifecycle renderer all work as expected.

    No upstream change is required for my setup. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions