gentle-pi 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +51 -80
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-proposal.md +2 -0
  8. package/assets/agents/sdd-research.md +54 -0
  9. package/assets/agents/sdd-status.md +4 -4
  10. package/assets/agents/sdd-tasks.md +2 -3
  11. package/assets/agents/sdd-verify.md +21 -1
  12. package/assets/chains/sdd-full.chain.md +1 -1
  13. package/assets/chains/sdd-verify.chain.md +1 -1
  14. package/assets/orchestrator-delegation.md +54 -248
  15. package/assets/orchestrator-memory.md +2 -0
  16. package/assets/orchestrator.md +22 -38
  17. package/assets/sdd-orchestrator-workflow.md +30 -37
  18. package/assets/support/sdd-status-contract.md +7 -7
  19. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  20. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  21. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  22. package/docs/native-authority-architecture.md +9 -11
  23. package/docs/review-integration.md +27 -373
  24. package/extensions/ask-user-choice.ts +151 -0
  25. package/extensions/gentle-ai.ts +1778 -3673
  26. package/extensions/quiet-tools.ts +515 -32
  27. package/extensions/sdd-init.ts +4 -8
  28. package/lib/gentle-ai-renderer.ts +70 -0
  29. package/lib/model-routing-authority.ts +133 -0
  30. package/lib/native-review-cli.ts +372 -940
  31. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  32. package/lib/review-candidate-view.ts +341 -132
  33. package/lib/review-host-relay.ts +210 -68
  34. package/lib/review-integration-v2.ts +839 -307
  35. package/lib/review-last-event-controller.ts +35 -0
  36. package/lib/sdd-preflight.ts +177 -66
  37. package/lib/sdd-status.ts +66 -111
  38. package/lib/terminal-theme.ts +1 -1
  39. package/package.json +83 -82
  40. package/runtime/gentle-ai-binary.mjs +1 -1
  41. package/runtime/native-review-cli.mjs +322 -890
  42. package/runtime/review-integration-v2.mjs +789 -257
  43. package/runtime/review-relay-contract.mjs +1 -1
  44. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  45. package/scripts/gentle-ai-installer.mjs +75 -22
  46. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  47. package/scripts/test-packed-runner.mjs +4 -7
  48. package/scripts/verify-package-files.mjs +12 -13
  49. package/skills/_shared/review-ledger-contract.md +8 -14
  50. package/skills/chained-pr/SKILL.md +3 -0
  51. package/skills/cognitive-doc-design/SKILL.md +1 -1
  52. package/skills/comment-writer/SKILL.md +1 -1
  53. package/skills/gentle-ai/SKILL.md +7 -74
  54. package/skills/judgment-day/SKILL.md +5 -7
  55. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  56. package/skills/release/SKILL.md +3 -3
  57. package/skills/skill-registry/SKILL.md +1 -1
  58. package/skills/work-unit-commits/SKILL.md +3 -1
  59. package/tests/artifact-language.test.ts +24 -11
  60. package/tests/ask-user-choice.test.ts +264 -0
  61. package/tests/codegraph-tools.test.ts +3 -3
  62. package/tests/crosslane/cross-lane.mjs +15 -1168
  63. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  64. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  65. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  66. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  67. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  68. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  69. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  70. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  71. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  72. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  73. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  74. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  75. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  76. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  77. package/tests/gentle-ai-binary.test.ts +83 -13
  78. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  79. package/tests/gentle-ai-installer.test.ts +137 -47
  80. package/tests/gentle-ai.test.ts +820 -131
  81. package/tests/gentle-theme.test.ts +133 -0
  82. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  83. package/tests/model-routing-authority.test.ts +257 -0
  84. package/tests/native-review-capability-contract.test.ts +28 -1
  85. package/tests/native-review-cli.test.ts +586 -962
  86. package/tests/native-review-consent.test.ts +150 -66
  87. package/tests/native-review-parity-runtime.test.ts +80 -359
  88. package/tests/native-review-parity.test.ts +674 -925
  89. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  90. package/tests/orchestrator-budget.test.ts +132 -88
  91. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  92. package/tests/package-manifest.test.ts +37 -35
  93. package/tests/provider-defect-handoff.test.ts +15 -118
  94. package/tests/quiet-tool-rendering.test.ts +1055 -28
  95. package/tests/review-authority-recovery-docs.test.ts +1 -2
  96. package/tests/review-candidate-view.test.ts +409 -3
  97. package/tests/review-compact-contract.test.ts +4 -30
  98. package/tests/review-controller-lock-status.test.ts +2 -2
  99. package/tests/review-controller-native-recovery.test.ts +363 -1450
  100. package/tests/review-controller-native-routing.test.ts +1251 -5642
  101. package/tests/review-controller-retired-ops.test.ts +1 -1
  102. package/tests/review-controller-workspace-root.test.ts +222 -71
  103. package/tests/review-controller.test.ts +26 -816
  104. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  105. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  106. package/tests/review-gate.test.ts +0 -45
  107. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  108. package/tests/review-host-relay-routing.test.ts +117 -82
  109. package/tests/review-host-relay.test.ts +242 -8
  110. package/tests/review-integration-v2-forward.test.ts +576 -553
  111. package/tests/review-integration-v2.test.ts +239 -165
  112. package/tests/review-last-event-closure.test.ts +408 -0
  113. package/tests/review-ledger-contract.test.ts +97 -35
  114. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  115. package/tests/review-relay-transport-agent.test.ts +119 -46
  116. package/tests/review-snapshot.test.ts +3 -2
  117. package/tests/runtime-harness.mjs +361 -196
  118. package/tests/sdd-agent-tools.test.ts +36 -0
  119. package/tests/sdd-preflight.test.ts +81 -15
  120. package/tests/sdd-status.test.ts +109 -110
  121. package/tests/skill-collision-prefixes.test.ts +5 -8
  122. package/tests/writer-edit-surface-scope.test.ts +230 -0
  123. package/themes/Gentleman-Cute.json +94 -0
  124. package/themes/Gentleman-Sexy.json +92 -0
  125. package/lib/git-commit-transaction.ts +0 -861
  126. package/runtime/git-commit-transaction.mjs +0 -862
  127. package/scripts/run-git-commit-transaction.mjs +0 -35
  128. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  129. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  130. package/tests/git-commit-transaction.test.ts +0 -530
@@ -1,10 +1,14 @@
1
1
  import assert from "node:assert/strict";
2
+ import { realpathSync } from "node:fs";
2
3
  import test from "node:test";
4
+ import { initTheme, keyHint } from "@earendil-works/pi-coding-agent";
5
+ import { imageFallback, visibleWidth } from "@earendil-works/pi-tui";
3
6
  import piPretty from "../extensions/pi-pretty.ts";
4
7
  import quietTools, {
5
8
  countNonEmptyLines,
6
9
  extractTextContent,
7
10
  formatToolResultOutput,
11
+ gentleAiRoutineCommand,
8
12
  tailLines,
9
13
  } from "../extensions/quiet-tools.ts";
10
14
 
@@ -17,13 +21,46 @@ const passthroughTheme = {
17
21
  },
18
22
  };
19
23
 
20
- function renderToString(component: { render(width: number): string[] }): string {
21
- return component.render(120).join("\n");
24
+ initTheme("dark");
25
+
26
+ const statusTheme = {
27
+ bold(value: string) {
28
+ return value;
29
+ },
30
+ fg(color: string, value: string) {
31
+ return `<${color}>${value}</${color}>`;
32
+ },
33
+ };
34
+
35
+ function routineRenderContext(overrides: Record<string, unknown> = {}) {
36
+ return {
37
+ args: {},
38
+ toolCallId: "tool-call",
39
+ invalidate() {},
40
+ lastComponent: undefined,
41
+ cwd: "/repo",
42
+ executionStarted: false,
43
+ argsComplete: true,
44
+ isPartial: true,
45
+ expanded: false,
46
+ showImages: false,
47
+ isError: false,
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ function renderLines(component: { render(width: number): string[] }, width = 120): string[] {
53
+ return component.render(width).map((line) => line.trimEnd());
22
54
  }
23
55
 
24
- function textResult(text: string) {
56
+ function renderToString(component: { render(width: number): string[] }, width = 120): string {
57
+ return renderLines(component, width).join("\n");
58
+ }
59
+
60
+ function textResult(text: string, details?: unknown) {
25
61
  return {
26
62
  content: [{ type: "text", text }],
63
+ details,
27
64
  };
28
65
  }
29
66
 
@@ -102,6 +139,30 @@ async function withEnvAsync<T>(updates: Record<string, string | undefined>, run:
102
139
  }
103
140
  }
104
141
 
142
+ function registeredQuietTools() {
143
+ const { pi, tools } = createPi();
144
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
145
+ return tools;
146
+ }
147
+
148
+ function registeredQuietToolsWithResolver(resolveOverride: () => unknown) {
149
+ const { pi, tools } = createPi();
150
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any, resolveOverride as any));
151
+ return tools;
152
+ }
153
+
154
+ function renderToolResult(tool: any, result: any, options: any, context: Record<string, unknown> = {}): string {
155
+ return renderToString(tool.renderResult(result, options, passthroughTheme, context));
156
+ }
157
+
158
+ function assertGenericBash(tool: any, command: string): void {
159
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
160
+ const output = renderToolResult(tool, textResult("original command output"), { expanded: true, isPartial: false }, { args: { command } });
161
+ assert.equal(call.trimEnd(), `$ ${command}`, command);
162
+ assert.doesNotMatch(call, /🌹︎ Gentle AI/);
163
+ assert.match(output, /original command output/);
164
+ }
165
+
105
166
  test("quiet tool rendering registers noisy built-in tools", () => {
106
167
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => {
107
168
  const { pi, tools } = createPi();
@@ -117,6 +178,22 @@ test("quiet tool rendering registers noisy built-in tools", () => {
117
178
  });
118
179
  });
119
180
 
181
+ test("quiet tool execution uses the tool-call cwd", async () => {
182
+ const tool = registeredQuietTools().get("bash");
183
+ const output = extractTextContent(await tool.execute("tool-call", { command: "pwd" }, new AbortController().signal, undefined, { cwd: "/tmp" })).trim();
184
+ // pwd prints the physical directory: on macOS /tmp is a symlink to /private/tmp.
185
+ assert.equal(output, realpathSync("/tmp"));
186
+ assert.notEqual(output, process.cwd());
187
+ });
188
+
189
+ test("quiet Bash rendering leaves configured shellPath outside its scope (#107)", () => {
190
+ const tool = registeredQuietTools().get("bash");
191
+ const render = (context: Record<string, unknown> = {}) => renderToString(tool.renderCall({ command: "printf output" }, passthroughTheme, routineRenderContext(context)));
192
+ const configured = render({ shellPath: "/configured/bash" });
193
+ assert.equal(configured, render());
194
+ assert.doesNotMatch(configured, /shellPath|configured\/bash/);
195
+ });
196
+
120
197
  test("quiet tool rendering can be disabled by env", () => {
121
198
  withEnv({ GENTLE_PI_QUIET_TOOLS: "0" }, () => {
122
199
  const { pi, tools } = createPi();
@@ -161,29 +238,36 @@ test("pi-pretty suppression is skipped when quiet tools are disabled", async ()
161
238
  );
162
239
  });
163
240
 
164
- test("quiet tool rendering hides noisy result bodies while collapsed and restores them when expanded", () => {
241
+ test("quiet tool rendering uses bounded previews while preserving search summaries", () => {
165
242
  const { pi, tools } = createPi();
166
243
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
167
244
 
168
- const cases = [
169
- { tool: "read", text: "first line\nsecond line", hidden: "first line", expanded: "second line" },
170
- { tool: "bash", text: "stdout line\nstderr line", hidden: "stdout line", expanded: "stderr line" },
171
- { tool: "grep", text: "src/a.ts:1:match\nsrc/b.ts:2:match", hidden: "src/a.ts", expanded: "src/b.ts" },
172
- { tool: "find", text: "src/a.ts\nsrc/b.ts", hidden: "src/a.ts", expanded: "src/b.ts" },
173
- { tool: "ls", text: "file-a.ts\nfile-b.ts", hidden: "file-a.ts", expanded: "file-b.ts" },
174
- ];
245
+ const read = renderToString(
246
+ tools.get("read").renderResult(textResult("first line\nsecond line"), { expanded: false, isPartial: false }, passthroughTheme, {}),
247
+ );
248
+ const bash = renderToString(
249
+ tools.get("bash").renderResult(textResult("stdout line\nstderr line"), { expanded: false, isPartial: false }, passthroughTheme, { args: { command: "printf output" } }),
250
+ );
251
+ assert.match(read, /first line\nsecond line/);
252
+ assert.match(bash, /stdout line\nstderr line/);
253
+ const configuredHint = keyHint("app.tools.expand", "to expand");
254
+ assert.ok(read.includes(configuredHint));
255
+ assert.ok(bash.includes(configuredHint));
175
256
 
176
- for (const entry of cases) {
177
- const tool = tools.get(entry.tool);
257
+ for (const [tool, text, label] of [
258
+ ["grep", "src/a.ts:1:match\nsrc/b.ts:2:match", "2 matches"],
259
+ ["find", "src/a.ts\nsrc/b.ts", "2 files"],
260
+ ["ls", "file-a.ts\nfile-b.ts", "2 entries"],
261
+ ] as const) {
178
262
  const collapsed = renderToString(
179
- tool.renderResult(textResult(entry.text), { expanded: false, isPartial: false }, passthroughTheme, {}),
263
+ tools.get(tool).renderResult(textResult(text), { expanded: false, isPartial: false }, passthroughTheme, {}),
180
264
  );
181
265
  const expanded = renderToString(
182
- tool.renderResult(textResult(entry.text), { expanded: true, isPartial: false }, passthroughTheme, {}),
266
+ tools.get(tool).renderResult(textResult(text), { expanded: true, isPartial: false }, passthroughTheme, {}),
183
267
  );
184
-
185
- assert.doesNotMatch(collapsed, new RegExp(entry.hidden.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), `${entry.tool} collapsed output must not include result body`);
186
- assert.match(expanded, new RegExp(entry.expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), `${entry.tool} expanded output must include full result body`);
268
+ assert.match(collapsed, new RegExp(label));
269
+ assert.doesNotMatch(collapsed, /src\/a\.ts|file-a\.ts/);
270
+ assert.match(expanded, new RegExp(text.split("\n")[1]!));
187
271
  }
188
272
  });
189
273
 
@@ -196,17 +280,673 @@ test("quiet tool rendering keeps compact collapsed summaries for search and list
196
280
  assert.equal(formatToolResultOutput("grep", textResult("No matches found") as any, { expanded: false }), "");
197
281
  assert.equal(formatToolResultOutput("find", textResult("No files found matching pattern") as any, { expanded: false }), "");
198
282
  assert.equal(formatToolResultOutput("ls", textResult("Directory is empty") as any, { expanded: false }), "");
199
- assert.equal(formatToolResultOutput("read", textResult("a\nb\n") as any, { expanded: false }), "");
200
- assert.equal(formatToolResultOutput("bash", textResult("a\nb\n") as any, { expanded: false }), "");
283
+ assert.equal(formatToolResultOutput("read", textResult("a\nb\n") as any, { expanded: false }), "\na\nb");
284
+ assert.equal(formatToolResultOutput("bash", textResult("a\nb\n") as any, { expanded: false }), "\na\nb");
201
285
  assert.equal(formatToolResultOutput("bash", textResult("a\nb\n") as any, { expanded: false, args: { command: "git diff" } }), "\na\nb\n");
202
286
  assert.equal(formatToolResultOutput("bash", textResult("a\nb\n") as any, { expanded: false, args: { command: "git -C repo status" } }), "\na\nb\n");
203
- assert.equal(formatToolResultOutput("bash", textResult("a\nb\n") as any, { expanded: false, args: { command: "echo git diff" } }), "");
204
- assert.equal(formatToolResultOutput("edit", textResult("updated") as any, { expanded: false }), "\nupdated");
205
- assert.equal(formatToolResultOutput("write", textResult("wrote") as any, { expanded: false }), "\nwrote");
287
+ assert.equal(formatToolResultOutput("bash", textResult("a\nb\n") as any, { expanded: false, args: { command: "echo git diff" } }), "\na\nb");
288
+ assert.equal(formatToolResultOutput("edit", textResult("updated") as any, { expanded: false }), "\n✓ applied");
289
+ assert.equal(formatToolResultOutput("write", textResult("wrote") as any, { expanded: false }), "\n✓ written");
206
290
  assert.equal(formatToolResultOutput("grep", textResult("a\nb\n") as any, { expanded: true }), "\na\nb\n");
207
291
  assert.equal(formatToolResultOutput("read", textResult("ENOENT: missing file") as any, { expanded: false, isError: true }), "\nENOENT: missing file");
208
292
  });
209
293
 
294
+ test("quiet search and placeholder summaries preserve exact-result behavior", async (t) => {
295
+ const grepRows = [["one", 12, "first"], ["two", 20, "second"], ["three", 28, "third"], ["four", 36, "fourth"], ["five", 44, "fifth"]] as const;
296
+ const contextText = grepRows.flatMap(([file, line, ordinal]) => [
297
+ `src/${file}.ts:${line}: ${ordinal} match`,
298
+ `src/${file}.ts-${line - 1}- context before`,
299
+ `src/${file}.ts-${line + 1}- context after`,
300
+ ]).join("\n");
301
+ assert.deepEqual([grepRows.length, contextText.split("\n").length, contextText.split("\n").filter((line) => /:\d+:/.test(line)).length, contextText.split("\n").filter((line) => /-\d+-/.test(line)).length], [5, 15, 5, 10]);
302
+
303
+ const lsCollisionText = "(empty directory)\nreal-entry.txt";
304
+ const bashCollisionText = "(no output)\nreal output";
305
+ const tools = registeredQuietTools();
306
+ const hint = keyHint("app.tools.expand", "to expand");
307
+ const cases = [
308
+ ["grep", "context rows count only match rows", "grep", contextText, { context: 1 }, " → 5 matches", undefined, /5 matches/, /15 matches/, 1],
309
+ ["grep", "no-context output counts every non-empty row", "grep", "src/a.ts:1:match\nsrc/b.ts:2:match", {}, " → 2 matches", undefined, /2 matches/, undefined, 1],
310
+ ["exact", "current empty-directory marker", "ls", "(empty directory)", {}, "", undefined, /^$/, /entries|to expand/, 0],
311
+ ["exact", "legacy empty-directory marker", "ls", "Directory is empty", {}, "", undefined, /^$/, /entries|to expand/, 0],
312
+ ["exact", "no-output marker", "bash", "(no output)", { command: "true" }, "\n(no output)", undefined, /^\(no output\)$/, /to expand/, 0],
313
+ ["collision", "empty-directory marker with a real entry", "ls", lsCollisionText, {}, " → 2 entries", `\n${lsCollisionText}`, /2 entries/, /empty directory|real-entry\.txt/, 1],
314
+ ["collision", "no-output marker with real output", "bash", bashCollisionText, { command: "printf output" }, `\n${bashCollisionText}`, `\n${bashCollisionText}`, /\(no output\)\nreal output/, undefined, 1],
315
+ ] as const;
316
+ assert.equal(cases.length, 7);
317
+
318
+ const assertSummaryCase = (current: (typeof cases)[number]) => {
319
+ const [, name, toolName, text, args, collapsed, expanded, rendered, forbidden, hintCount] = current;
320
+ const result = textResult(text) as any;
321
+ const tool = tools.get(toolName);
322
+ assert.ok(tool, `${name}: missing ${toolName} renderer`);
323
+ assert.equal(formatToolResultOutput(toolName, result, { expanded: false, args }), collapsed, name);
324
+ if (expanded !== undefined) assert.equal(formatToolResultOutput(toolName, result, { expanded: true, args }), expanded, `${name} expanded output`);
325
+ const output = renderToolResult(tool, result, { expanded: false, isPartial: false }, { args });
326
+ assert.match(output, rendered, name);
327
+ if (forbidden) assert.doesNotMatch(output, forbidden, name);
328
+ assert.equal(output.split(hint).length - 1, hintCount, `${name}: hint count`);
329
+ };
330
+
331
+ for (const [section, count] of [["grep", 2], ["exact", 3], ["collision", 2]] as const) await t.test(section, () => {
332
+ const selected = cases.filter(([currentSection]) => currentSection === section);
333
+ assert.equal(selected.length, count, `${section}: case count`);
334
+ for (const current of selected) assertSummaryCase(current);
335
+ });
336
+ });
337
+
338
+ test("quiet Bash errors preserve meaningful rows for both public output orderings", () => {
339
+ const tool = registeredQuietTools().get("bash");
340
+ const hint = keyHint("app.tools.expand", "to expand");
341
+ const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;]*m/g, "");
342
+ const cases = [
343
+ ["Pi session event", [
344
+ [" B3 stdout before failure", []],
345
+ [" B3 stderr detail that should require expansion", [" ", " "]],
346
+ [" Command exited with code 7", []],
347
+ ]],
348
+ ["registered execution probe", [
349
+ ["probe stdout before failure", ["", ""]],
350
+ ["probe stderr detail \x1b[31mwith terminal color\x1b[0m", ["", ""]],
351
+ ["probe exit status: 7", []],
352
+ ]],
353
+ ] as const;
354
+ assert.equal(cases.length, 2);
355
+ const args = { command: "false" };
356
+ const renderResult = (result: any, expanded: boolean) => tool.renderResult(result, { expanded, isPartial: false, isError: true }, passthroughTheme, { args, isError: true });
357
+
358
+ for (const [name, rows] of cases) {
359
+ assert.equal(rows.length, 3, `${name}: three meaningful source rows`);
360
+ const text = rows.flatMap(([row, gaps]) => [row, ...gaps]).join("\n");
361
+ const previewRows = rows.map(([row]) => stripAnsi(row));
362
+ assert.ok(previewRows.every((row) => row.trim().length > 0), `${name}: preview rows are meaningful`);
363
+ const expandedText = stripAnsi(text);
364
+ const renderedExpandedText = expandedText.split("\n").map((line) => line.trimEnd()).join("\n");
365
+ const result = textResult(text) as any;
366
+ assert.equal(formatToolResultOutput("bash", result, { expanded: false, isError: true, args }), `\n${previewRows.join("\n")}`, name);
367
+ assert.equal(formatToolResultOutput("bash", result, { expanded: true, isError: true, args }), `\n${expandedText}`, `${name} expanded output`);
368
+
369
+ const collapsedLines = renderLines(renderResult(result, false), 120);
370
+ assert.ok(collapsedLines.length <= 4, `${name}: expected at most 4 visual rows, got ${collapsedLines.length}`);
371
+ assert.deepEqual(collapsedLines.slice(0, 3), previewRows, `${name}: preview rows`);
372
+ assert.equal(collapsedLines.filter((line) => line.trim().length === 0).length, 0, `${name}: no blank preview rows`);
373
+ assert.equal(collapsedLines.filter((line) => line.includes(hint)).length, 1, `${name}: one expansion hint`);
374
+ const expanded = renderToString(renderResult(result, true), 1000);
375
+ assert.equal(expanded, `\n${renderedExpandedText}`, `${name}: complete expanded sanitized text`);
376
+ assert.doesNotMatch(expanded, /\x1b\[/, `${name}: expanded output is ANSI-free`);
377
+ }
378
+ });
379
+
380
+ test("quiet Bash normalizes compact JSON for bounded previews while keeping expanded text complete", () => {
381
+ const compactJson = JSON.stringify({
382
+ schema: "review",
383
+ contract: "compact",
384
+ nested: { longTail: "this must stay out of the collapsed preview", deeper: { value: true } },
385
+ });
386
+ const result = textResult(compactJson) as any;
387
+ const commandArgs = { command: "printf json" };
388
+ const previewRows = [
389
+ ' "schema": "review",',
390
+ ' "contract": "compact",',
391
+ ' "nested": {',
392
+ ];
393
+ const expectedPreview = `\n${previewRows.join("\n")}`;
394
+ const tool = registeredQuietTools().get("bash");
395
+
396
+ assert.equal(formatToolResultOutput("bash", result, { expanded: false, args: commandArgs }), expectedPreview);
397
+ assert.equal(formatToolResultOutput("bash", result, { expanded: true, args: commandArgs }), `\n${compactJson}`);
398
+
399
+ const collapsedLines = renderLines(tool.renderResult(result, { expanded: false, isPartial: false }, passthroughTheme, { args: commandArgs }), 120);
400
+ const collapsed = collapsedLines.join("\n");
401
+ assert.equal(previewRows.length, 3);
402
+ assert.ok(collapsedLines.length <= 4, `expected at most 4 visual rows, got ${collapsedLines.length}`);
403
+ assert.deepEqual(collapsedLines.slice(0, 3), previewRows);
404
+ assert.equal(collapsedLines.filter((line) => line.includes(keyHint("app.tools.expand", "to expand"))).length, 1);
405
+ assert.doesNotMatch(collapsed, /longTail|this must stay out of the collapsed preview|deeper/);
406
+
407
+ const expanded = renderToString(tool.renderResult(result, { expanded: true, isPartial: false }, passthroughTheme, { args: commandArgs }), 1000);
408
+ assert.equal(expanded, `\n${compactJson}`);
409
+ });
410
+
411
+ test("quiet tool rendering identifies only routine Gentle AI SDD and RDD commands", () => {
412
+ assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-status fix-rose --json" }), "sdd-status");
413
+ assert.equal(gentleAiRoutineCommand({ command: "env FOO=bar gentle-ai sdd-continue fix-rose" }), "sdd-continue");
414
+ assert.equal(gentleAiRoutineCommand({ command: "/package/.gentle-ai/v2.2.0/gentle-ai review status --next-transition" }), "review");
415
+ assert.equal(gentleAiRoutineCommand({ command: "./.gentle-ai/v2.2.0/gentle-ai sdd-status rose --json" }), "sdd-status");
416
+ assert.equal(gentleAiRoutineCommand({ command: ".\\.gentle-ai\\v2.2.0\\gentle-ai.exe review status --next-transition" }), "review");
417
+ assert.equal(gentleAiRoutineCommand({ command: "C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe sdd-continue rose" }), "sdd-continue");
418
+ assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-attempt acquire --change fix-rose" }), "sdd-attempt");
419
+ assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-attempt settle --change fix-rose" }), "sdd-attempt");
420
+ assert.equal(gentleAiRoutineCommand({ command: "gentle-ai review status --next-transition" }), "review");
421
+ assert.equal(gentleAiRoutineCommand({ command: "gentle-ai version" }), undefined);
422
+ assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-attempt inspect" }), undefined);
423
+ assert.equal(gentleAiRoutineCommand({ command: "/package/.gentle-ai/v2.2.0/gentle-ai-copy review status" }), undefined);
424
+ assert.equal(gentleAiRoutineCommand({ command: "echo gentle-ai review status" }), undefined);
425
+ });
426
+
427
+ test("quiet tool rendering refuses to compact composed Gentle AI shell commands", () => {
428
+ const { pi, tools } = createPi();
429
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
430
+ const commands = [
431
+ "gentle-ai review status --next-transition && rm -rf target",
432
+ "gentle-ai review status; rm -rf target",
433
+ "gentle-ai review status || rm -rf target",
434
+ "gentle-ai review status | tee status.json",
435
+ "gentle-ai review status & rm -rf target",
436
+ "gentle-ai review status\nrm -rf target",
437
+ "gentle-ai review status --next-transition $(rm -rf target)",
438
+ "gentle-ai review status --next-transition `rm -rf target`",
439
+ "gentle-ai review status --next-transition <(cat target)",
440
+ "gentle-ai review status --next-transition >(tee target)",
441
+ "gentle-ai review status --next-transition > status.json",
442
+ "gentle-ai review status --next-transition < status.json",
443
+ "gentle-ai review status $HOME",
444
+ "gentle-ai review status ${HOME}",
445
+ "gentle-ai review status\n",
446
+ ];
447
+
448
+ for (const command of commands) {
449
+ assert.equal(gentleAiRoutineCommand({ command }), undefined, command);
450
+ const call = renderToString(tools.get("bash").renderCall({ command }, passthroughTheme, {}));
451
+ const output = renderToString(tools.get("bash").renderResult(textResult("command output"), { expanded: true, isPartial: false }, passthroughTheme, { args: { command } }));
452
+ assert.equal(call.replace(/[ \t]+$/gm, "").trimEnd(), `$ ${command.trimEnd()}`);
453
+ assert.match(output, /command output/);
454
+ }
455
+ });
456
+
457
+ test("quiet tool rendering hides every collapsed direct Gentle AI result and preserves expanded errors", () => {
458
+ const { pi, tools } = createPi();
459
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
460
+ const command = "gentle-ai review status --next-transition";
461
+ const textRose = "\u{1F339}\uFE0E";
462
+ const tool = tools.get("bash");
463
+ const failureText = "review status failed: authority unavailable\x1b[31m\nlineage=secret";
464
+ const expandHint = keyHint("app.tools.expand", "to expand");
465
+
466
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, {}));
467
+ const collapsed = renderToString(tool.renderResult(textResult('{"next_transition":"stop"}'), { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
468
+ const expanded = renderToString(tool.renderResult(textResult('{"next_transition":"stop"}'), { expanded: true, isPartial: false }, passthroughTheme, { args: { command } }));
469
+ const failure = renderToString(tool.renderResult(textResult(failureText), { expanded: false, isPartial: false, isError: true }, passthroughTheme, { args: { command } }));
470
+ const expandedFailure = renderToString(tool.renderResult(textResult(failureText), { expanded: true, isPartial: false, isError: true }, passthroughTheme, { args: { command } }));
471
+ const empty = renderToString(tool.renderResult(textResult(""), { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
472
+ const nonText = renderToString(tool.renderResult({ content: [{ type: "image", data: "opaque", mimeType: "image/png" }] }, { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
473
+
474
+ assert.equal([...textRose].length, 2);
475
+ assert.equal(call.trimEnd(), `${textRose} Gentle AI · running · review status`);
476
+ assert.equal(collapsed, expandHint);
477
+ assert.equal(collapsed.split("\n")[0], expandHint);
478
+ assert.doesNotMatch(collapsed, /next_transition|stop/);
479
+ assert.equal(expanded.split("\n")[0], '{"next_transition":"stop"}');
480
+ assert.match(expanded, /"next_transition":"stop"/);
481
+ assert.doesNotMatch(expanded, /to expand/);
482
+ assert.equal(failure, expandHint);
483
+ assert.doesNotMatch(failure, /review status failed|authority unavailable|lineage=secret/);
484
+ assert.match(expandedFailure, /review status failed: authority unavailable/);
485
+ assert.match(expandedFailure, /lineage=secret/);
486
+ assert.doesNotMatch(expandedFailure, /to expand/);
487
+ assert.doesNotMatch(expandedFailure, /\x1b\[/);
488
+ assert.equal(empty, "");
489
+ assert.equal(nonText, "");
490
+ });
491
+
492
+ test("quiet tool rendering transitions one Gentle AI header through lifecycle states", () => {
493
+ const { pi, tools } = createPi();
494
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
495
+ const tool = tools.get("bash");
496
+ const command = "gentle-ai review status --cwd /repo/private-change";
497
+
498
+ const initial = tool.renderCall({ command }, statusTheme, routineRenderContext());
499
+ const initialText = renderToString(initial).trimEnd();
500
+ const running = tool.renderCall(
501
+ { command },
502
+ statusTheme,
503
+ routineRenderContext({ executionStarted: true, lastComponent: initial }),
504
+ );
505
+ const runningText = renderToString(running).trimEnd();
506
+ const completed = tool.renderCall(
507
+ { command },
508
+ statusTheme,
509
+ routineRenderContext({
510
+ executionStarted: true,
511
+ isPartial: false,
512
+ lastComponent: running,
513
+ }),
514
+ );
515
+ const completedText = renderToString(completed).trimEnd();
516
+ const failed = tool.renderCall(
517
+ { command },
518
+ statusTheme,
519
+ routineRenderContext({
520
+ executionStarted: true,
521
+ isPartial: false,
522
+ isError: true,
523
+ lastComponent: completed,
524
+ }),
525
+ );
526
+ const failedText = renderToString(failed).trimEnd();
527
+
528
+ assert.strictEqual(initial, running);
529
+ assert.strictEqual(running, completed);
530
+ assert.strictEqual(completed, failed);
531
+ assert.equal(initialText, "<warning>🌹︎ Gentle AI · running · review status</warning>");
532
+ assert.equal(runningText, "<warning>🌹︎ Gentle AI · running · review status</warning>");
533
+ assert.equal(completedText, "<success>🌹︎ Gentle AI · completed · review status</success>");
534
+ assert.equal(failedText, "<error>🌹︎ Gentle AI · failed · review status</error>");
535
+ assert.doesNotMatch(failedText, /private-change/);
536
+ });
537
+
538
+ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
539
+ const tool = registeredQuietTools().get("bash");
540
+ const rose = /🌹︎/;
541
+ const render = (command: string, state: Record<string, unknown>, overrides: Record<string, unknown> = {}) => {
542
+ const component = tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, state, argsComplete: false, ...overrides }));
543
+ return [component, renderToString(component)] as const;
544
+ };
545
+ const command = "gentle-ai review status --token 'lineage-secret";
546
+ const state = {};
547
+ const [preparing, collapsed] = render(command, state);
548
+ assert.equal(collapsed, "🌹︎ Gentle AI · preparing · review status");
549
+ assert.doesNotMatch(collapsed, /token|lineage-secret/);
550
+ const expanded = render(command, state, { expanded: true, lastComponent: preparing })[1];
551
+ assert.match(expanded, /^🌹︎ Gentle AI · preparing · review status\n\$ gentle-ai review status --token 'lineage-secret$/);
552
+
553
+ const direct = "gentle-ai review status";
554
+ const [running, runningText] = render(direct, state, { argsComplete: true, executionStarted: true, lastComponent: preparing });
555
+ assert.strictEqual(running, preparing); assert.equal(runningText, "🌹︎ Gentle AI · running · review status");
556
+ const genericState = {};
557
+ const [generic, genericText] = render("gentle-ai review status | cat", genericState);
558
+ assert.doesNotMatch(genericText, rose); assert.match(genericText, /\| cat/);
559
+ assert.doesNotMatch(render(direct, genericState, { argsComplete: true, lastComponent: generic })[1], rose);
560
+ assert.match(render(direct, {}, { argsComplete: true })[1], rose);
561
+
562
+ for (const malformed of ["gentle-ai\x1b[31m review status", 'gentle-ai review status "unfinished']) {
563
+ const text = render(malformed, {}, { argsComplete: true })[1];
564
+ assert.doesNotMatch(text, rose); assert.match(text, /\$ gentle-ai.*review status/);
565
+ }
566
+ for (const bracket of ["[", "]"]) assert.match(render(`${direct} ${bracket}`, {})[1], rose);
567
+
568
+ const genericResult = renderToolResult(tool, textResult("generic result"), { expanded: false, isPartial: false }, { args: { command: direct }, state: genericState, argsComplete: true });
569
+ assert.equal(genericResult.split("\n")[0], "generic result");
570
+ const directState = {}; render(direct, directState, { argsComplete: true });
571
+ const directResult = renderToolResult(tool, textResult("direct result"), { expanded: false, isPartial: false }, { args: { command: direct }, state: directState, argsComplete: true });
572
+ assert.equal(directResult, keyHint("app.tools.expand", "to expand"));
573
+ });
574
+
575
+ test("quiet tool rendering displays only finite safe Gentle AI operation paths", () => {
576
+ const { pi, tools } = createPi();
577
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
578
+ const tool = tools.get("bash");
579
+ const rose = "🌹︎";
580
+ const cases = [
581
+ ["gentle-ai sdd-status change-123 --cwd /repo/private", "sdd status"],
582
+ ["gentle-ai sdd-continue change-123 --json", "sdd continue"],
583
+ ["gentle-ai sdd-attempt acquire --change change-123", "sdd attempt acquire"],
584
+ ["gentle-ai sdd-attempt settle --change change-123 --actor maintainer", "sdd attempt settle"],
585
+ ["gentle-ai review capabilities --cwd /repo/private", "review capabilities"],
586
+ ["gentle-ai review start --target sha256:secret --path src/private.ts", "review start"],
587
+ ["gentle-ai review finalize --lineage lineage-secret --payload '{\"secret\":true}'", "review finalize"],
588
+ ["gentle-ai review status --lineage lineage-secret", "review status"],
589
+ ["gentle-ai review repair --reason private-reason", "review repair"],
590
+ ["gentle-ai review invalidate --lineage lineage-secret", "review invalidate"],
591
+ ["gentle-ai review abandon --lineage lineage-secret", "review abandon"],
592
+ ["gentle-ai review recover --lineage lineage-secret", "review recover"],
593
+ ["gentle-ai review reclaim --lineage lineage-secret", "review reclaim"],
594
+ ["gentle-ai review validate --cwd /repo/private", "review validate"],
595
+ ["gentle-ai review capture-result --input result.json", "review capture result"],
596
+ ["gentle-ai review capture-refuter --input result.json", "review capture refuter"],
597
+ ["gentle-ai review capture-validation --input result.json", "review capture validation"],
598
+ ["gentle-ai review capture-evidence --input evidence.json", "review capture evidence"],
599
+ ["gentle-ai review preserve-result --input result.json", "review preserve result"],
600
+ ["gentle-ai review lens-context --lens reviewer", "review lens context"],
601
+ ["gentle-ai review retry-final-verification --incident incident.json", "review retry final verification"],
602
+ ["gentle-ai review store-reset --authorization secret", "review store reset"],
603
+ ["gentle-ai review inspect-authority --path /repo/private", "review inspect authority"],
604
+ ["gentle-ai review inspect-candidate --path /repo/private", "review inspect candidate"],
605
+ ["gentle-ai review dispose-result --reference result-secret", "review dispose result"],
606
+ ["gentle-ai review reopen-results --reference result-secret", "review reopen results"],
607
+ ["gentle-ai review opencode-transport --payload secret", "review opencode transport"],
608
+ ["gentle-ai review bind-sdd --change change-123 --lineage lineage-secret", "review bind sdd"],
609
+ ["gentle-ai review mode enable --actor maintainer", "review mode enable"],
610
+ ["gentle-ai review mode disable --actor maintainer", "review mode disable"],
611
+ ["gentle-ai review mode status --json", "review mode status"],
612
+ ["gentle-ai review validate --gate post-apply --cwd /repo/private", "review validate post apply"],
613
+ ["gentle-ai review validate --gate pre-commit --cwd /repo/private", "review validate pre commit"],
614
+ ["gentle-ai review validate --gate pre-push --cwd /repo/private", "review validate pre push"],
615
+ ["gentle-ai review validate --gate pre-pr --cwd /repo/private", "review validate pre pr"],
616
+ ["gentle-ai review validate --gate release --cwd /repo/private", "review validate release"],
617
+ ["gentle-ai review schema capture-result-dry-run --path /tmp/private.json", "review schema capture result dry run"],
618
+ ["gentle-ai review schema final-verification-incident --path /tmp/private.json", "review schema final verification incident"],
619
+ ["gentle-ai review schema refuter --path /tmp/private.json", "review schema refuter"],
620
+ ["gentle-ai review schema reviewer --path /tmp/private.json", "review schema reviewer"],
621
+ ["gentle-ai review schema validator --path /tmp/private.json", "review schema validator"],
622
+ ["gentle-ai review schema verification-evidence --path /tmp/private.json", "review schema verification evidence"],
623
+ ["gentle-ai review schema verification-evidence-record --path /tmp/private.json", "review schema verification evidence record"],
624
+ ["gentle-ai review unknown-operation --change change-123 --reason private-reason", "review"],
625
+ ["gentle-ai review mode restart --actor maintainer", "review"],
626
+ ["gentle-ai review validate --gate unknown-gate --cwd /repo/private", "review"],
627
+ ["gentle-ai review schema unknown-schema --path /tmp/private.json", "review schema"],
628
+ ];
629
+
630
+ for (const [command, path] of cases) {
631
+ const rendered = renderToString(
632
+ tool.renderCall(
633
+ { command },
634
+ passthroughTheme,
635
+ routineRenderContext({ args: { command } }),
636
+ ),
637
+ );
638
+ assert.equal(rendered.trimEnd(), `${rose} Gentle AI · running · ${path}`, command);
639
+ assert.doesNotMatch(rendered, /change-123|private|secret|lineage|sha256|result\.json|incident\.json/);
640
+ }
641
+ });
642
+
643
+ test("quiet tool rendering covers version and future standalone Gentle AI commands", () => {
644
+ const { pi, tools } = createPi();
645
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
646
+ const tool = tools.get("bash");
647
+ const rose = "🌹︎";
648
+ const cases = [
649
+ ["gentle-ai version", "version"],
650
+ ["gentle-ai future-command --authorization-root /repo/private", "command"],
651
+ ["gentle-ai sdd-attempt future-verb --change secret-change", "sdd attempt"],
652
+ ["gentle-ai review future-operation --path /repo/private", "review"],
653
+ ["/package/.gentle-ai/v2.2.0/gentle-ai version", "version"],
654
+ ["C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe future-command --root C:\\private", "command"],
655
+ ] as const;
656
+
657
+ for (const [command, path] of cases) {
658
+ const rendered = renderToString(
659
+ tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command } })),
660
+ );
661
+ assert.equal(rendered.trimEnd(), `${rose} Gentle AI · running · ${path}`, command);
662
+ assert.doesNotMatch(rendered, /authorization-root|secret-change|private|C:\\\\private/);
663
+ }
664
+ });
665
+
666
+ test("quiet tool rendering recognizes exact quoted, escaped, Windows, and command-prefixed Gentle AI executables", () => {
667
+ const { pi, tools } = createPi();
668
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
669
+ const tool = tools.get("bash");
670
+ const routineCommands = [
671
+ "gentle-ai.exe version",
672
+ "'gentle-ai' version",
673
+ '"gentle-ai" version',
674
+ "'gentle-ai.exe' version",
675
+ '"gentle-ai.exe" version',
676
+ "gentle\\-ai version",
677
+ "gentle\\-ai\\.exe version",
678
+ "command -- gentle-ai version",
679
+ "command -- 'gentle-ai.exe' version",
680
+ "env gentle-ai version",
681
+ "FOO='a b' gentle-ai version",
682
+ 'FOO="a b" gentle-ai version',
683
+ "env FOO='a b' gentle-ai version",
684
+ 'env FOO="a b" gentle-ai version',
685
+ ] as const;
686
+
687
+ for (const command of routineCommands) {
688
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
689
+ assert.equal(call.trimEnd(), "🌹︎ Gentle AI · running · version", command);
690
+ }
691
+
692
+ for (const command of ["'gentle-ai-copy' version", '"gentle-ai-copy.exe" version', "'gentle-ai' version && echo done"] as const) {
693
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
694
+ assert.equal(call.trimEnd(), `$ ${command}`);
695
+ assert.doesNotMatch(call, /🌹︎ Gentle AI/);
696
+ }
697
+ });
698
+
699
+ test("quiet tool rendering recognizes only the exact resolved dev binary", () => {
700
+ let devPath = "/home/devel/projects/gentle-ai/dist/gentle-ai-main";
701
+ let resolutions = 0;
702
+ const tools = registeredQuietToolsWithResolver(() => {
703
+ resolutions += 1;
704
+ return { source: "registration", origin: "test", path: devPath, sha256: "test" };
705
+ });
706
+ const tool = tools.get("bash");
707
+ const cases = [
708
+ [`${devPath} review validate --gate pre-commit --cwd /repo/private`, "review validate pre commit"],
709
+ [`'${devPath}' review status --lineage secret`, "review status"],
710
+ [`"${devPath}" version`, "version"],
711
+ [`env FOO='a b' ${devPath} review capabilities`, "review capabilities"],
712
+ [`command -- "${devPath}" sdd-status hidden`, "sdd status"],
713
+ ] as const;
714
+ for (const [command, operationPath] of cases) {
715
+ const call = renderToString(tool.renderCall({ command }, statusTheme, routineRenderContext({ args: { command } })));
716
+ assert.equal(call.trimEnd(), `<warning>🌹︎ Gentle AI · running · ${operationPath}</warning>`, command);
717
+ assert.doesNotMatch(call, /gentle-ai-main|private|secret|hidden/);
718
+ }
719
+ const command = `${devPath} review status --prompt hidden-prompt --lineage lineage-secret --body private-body`;
720
+ const lifecycleContext = routineRenderContext({ args: { command }, state: {} });
721
+ assert.match(renderToString(tool.renderCall({ command }, passthroughTheme, lifecycleContext)), /Gentle AI · running · review status/);
722
+ devPath = "/new/dev/gentle-ai-main";
723
+ const refreshedCommand = `${devPath} review status --token token-secret`;
724
+ assert.match(renderToString(tool.renderCall({ command: refreshedCommand }, passthroughTheme, routineRenderContext({ args: { command: refreshedCommand } }))), /Gentle AI · running · review status/);
725
+ assertGenericBash(tool, cases[2][0]);
726
+ const text = "error: private failure\nlineage=secret body=hidden\x1b[31m";
727
+ const collapsed = renderToolResult(tool, textResult(text), { expanded: false, isPartial: false, isError: true }, lifecycleContext);
728
+ const expanded = renderToolResult(tool, textResult(text), { expanded: true, isPartial: false, isError: true }, lifecycleContext);
729
+ const refreshed = renderToolResult(tool, textResult("result-secret"), { expanded: false, isPartial: false }, { args: { command: refreshedCommand } });
730
+ const hint = keyHint("app.tools.expand", "to expand");
731
+ assert.equal(collapsed, hint);
732
+ assert.equal(collapsed.split(hint).length - 1, 1);
733
+ assert.doesNotMatch(collapsed, /private|lineage|secret|hidden|error/);
734
+ assert.match(expanded, /private failure|lineage=secret body=hidden/);
735
+ assert.doesNotMatch(expanded, /to expand|\x1b\[/);
736
+ assert.doesNotMatch(refreshed, /result-secret/);
737
+ assert.ok(resolutions > 1);
738
+ });
739
+ test("quiet tool rendering keeps unregistered dev lookalikes and composed calls generic", () => {
740
+ const devPath = "/home/devel/projects/gentle-ai/dist/gentle-ai-main";
741
+ const active = registeredQuietToolsWithResolver(() => ({ source: "registration", origin: "test", path: devPath, sha256: "test" }));
742
+ for (const command of [
743
+ "gentle-ai-main review status",
744
+ "/tmp/alias/gentle-ai review status",
745
+ `${devPath}-sibling review status`, `${devPath}.bak review status`, `${devPath}x review status`,
746
+ `${devPath} review status | tee output`, `${devPath} review status && echo done`,
747
+ `'${devPath}' review status $(printf nested)`,
748
+ ]) assertGenericBash(active.get("bash"), command);
749
+
750
+ const unresolved = `${devPath} review status`;
751
+ assertGenericBash(registeredQuietToolsWithResolver(() => undefined).get("bash"), unresolved);
752
+ let throwing: Map<string, any> | undefined;
753
+ assert.doesNotThrow(() => { throwing = registeredQuietToolsWithResolver(() => { throw new Error("invalid override"); }); });
754
+ assertGenericBash(throwing!.get("bash"), unresolved);
755
+ assertGenericBash(registeredQuietToolsWithResolver(() => ({ path: "relative/gentle-ai-main" })).get("bash"), unresolved);
756
+ });
757
+ test("quiet tool rendering hides the routine partial result because the header owns state", () => {
758
+ const { pi, tools } = createPi();
759
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
760
+ const tool = tools.get("bash");
761
+ const command = "gentle-ai review status --cwd /repo/private";
762
+
763
+ const expandHint = keyHint("app.tools.expand", "to expand");
764
+ const partial = renderToString(
765
+ tool.renderResult(
766
+ textResult("{\"status\":\"running\"}"),
767
+ { expanded: false, isPartial: true },
768
+ passthroughTheme,
769
+ { ...routineRenderContext({ args: { command } }), isError: false },
770
+ ),
771
+ );
772
+ const partialExpanded = renderToString(
773
+ tool.renderResult(
774
+ textResult("{\"status\":\"running\"}"),
775
+ { expanded: true, isPartial: true },
776
+ passthroughTheme,
777
+ { ...routineRenderContext({ args: { command } }), isError: false },
778
+ ),
779
+ );
780
+ const partialFailure = renderToString(
781
+ tool.renderResult(
782
+ textResult("review status failed: authority unavailable"),
783
+ { expanded: false, isPartial: true },
784
+ passthroughTheme,
785
+ { ...routineRenderContext({ args: { command } }), isError: true },
786
+ ),
787
+ );
788
+ const completed = renderToString(
789
+ tool.renderResult(
790
+ textResult("{\"status\":\"running\"}"),
791
+ { expanded: false, isPartial: false },
792
+ passthroughTheme,
793
+ { ...routineRenderContext({ args: { command } }), isError: false },
794
+ ),
795
+ );
796
+
797
+ assert.equal(partial, expandHint);
798
+ assert.equal(partial.split(expandHint).length - 1, 1);
799
+ assert.match(partialExpanded, /"status":"running"/);
800
+ assert.doesNotMatch(partialExpanded, /to expand/);
801
+ assert.equal(partialFailure, expandHint);
802
+ assert.equal(completed, expandHint);
803
+
804
+ const partialExpandedFailure = renderToString(
805
+ tool.renderResult(
806
+ textResult("review status failed: authority unavailable\x1b[31m"),
807
+ { expanded: true, isPartial: true },
808
+ passthroughTheme,
809
+ { ...routineRenderContext({ args: { command } }), isError: true },
810
+ ),
811
+ );
812
+ assert.match(partialExpandedFailure, /review status failed: authority unavailable/);
813
+ assert.doesNotMatch(partialExpandedFailure, /\x1b\[/);
814
+ });
815
+
816
+ test("quiet tool rendering collapses grant calls to action and authorization-root cardinality", () => {
817
+ const { pi, tools } = createPi();
818
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
819
+ const tool = tools.get("bash");
820
+ const command = "gentle-ai sdd-attempt grant --authorization-root /repo/root --authorization-root /other/root --change secret-change\x1b[31m";
821
+
822
+ const initial = tool.renderCall(
823
+ { command },
824
+ statusTheme,
825
+ routineRenderContext({ args: { command } }),
826
+ );
827
+ const running = tool.renderCall(
828
+ { command },
829
+ statusTheme,
830
+ routineRenderContext({ args: { command }, executionStarted: true, lastComponent: initial }),
831
+ );
832
+ const completed = tool.renderCall(
833
+ { command },
834
+ statusTheme,
835
+ routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, lastComponent: running }),
836
+ );
837
+ const failed = tool.renderCall(
838
+ { command },
839
+ statusTheme,
840
+ routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, isError: true, lastComponent: completed }),
841
+ );
842
+
843
+ assert.strictEqual(initial, running);
844
+ assert.strictEqual(running, completed);
845
+ assert.strictEqual(completed, failed);
846
+ const rendered = renderToString(failed);
847
+ assert.equal(rendered, "<error>🌹︎ Gentle AI · failed · sdd attempt grant · 2 roots</error>");
848
+ assert.doesNotMatch(rendered, /authorization-root|secret-change|repo\/root|other\/root|audit:|\x1b\[/);
849
+ });
850
+
851
+ test("quiet tool rendering counts grant authorization roots without rendering values", () => {
852
+ const { pi, tools } = createPi();
853
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
854
+ const tool = tools.get("bash");
855
+ const cases = [
856
+ ["gentle-ai sdd-attempt grant --change change", "sdd attempt grant"],
857
+ ["gentle-ai sdd-attempt grant --authorization-root /repo/root", "sdd attempt grant · 1 root"],
858
+ ["gentle-ai sdd-attempt grant --authorization-root=/repo/root", "sdd attempt grant · 1 root"],
859
+ ["gentle-ai sdd-attempt grant --authorization-root --change change", "sdd attempt grant"],
860
+ ["gentle-ai sdd-attempt grant --authorization-root /one --authorization-root=/two --authorization-root /three", "sdd attempt grant · 3 roots"],
861
+ ["gentle-ai sdd-attempt grant --authorization-root= --authorization-root /one", "sdd attempt grant · 1 root"],
862
+ ] as const;
863
+
864
+ for (const [command, expected] of cases) {
865
+ const rendered = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
866
+ assert.equal(rendered.trimEnd(), `🌹︎ Gentle AI · running · ${expected}`, command);
867
+ assert.doesNotMatch(rendered, /authorization-root|\/repo\/root|\/one|\/two|\/three|change/);
868
+ }
869
+ });
870
+
871
+ test("quiet tool rendering hides invocation secrets from collapsed Gentle AI calls and results", () => {
872
+ const { pi, tools } = createPi();
873
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
874
+ const tool = tools.get("bash");
875
+ const command = "gentle-ai review finalize --prompt hidden-prompt --lineage lineage-secret --body private-body --authorization-root /private/root";
876
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
877
+ const collapsed = renderToString(
878
+ tool.renderResult(
879
+ textResult("audit: lineage-secret body=private-body prompt=hidden-prompt root=/private/root"),
880
+ { expanded: false, isPartial: false, isError: true },
881
+ passthroughTheme,
882
+ { args: { command }, isError: true },
883
+ ),
884
+ );
885
+
886
+ assert.equal(call.trimEnd(), "🌹︎ Gentle AI · running · review finalize");
887
+ assert.equal(collapsed, keyHint("app.tools.expand", "to expand"));
888
+ for (const forbidden of ["hidden-prompt", "lineage-secret", "private-body", "/private/root", "audit:"]) {
889
+ assert.doesNotMatch(call, new RegExp(forbidden));
890
+ assert.doesNotMatch(collapsed, new RegExp(forbidden));
891
+ }
892
+ });
893
+
894
+ test("quiet tool rendering keeps shell compositions, expansions, and lookalikes generic", () => {
895
+ const { pi, tools } = createPi();
896
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
897
+ const tool = tools.get("bash");
898
+ const commands = [
899
+ "gentle-ai version && echo done",
900
+ "gentle-ai version; echo done",
901
+ "gentle-ai version | tee output",
902
+ "gentle-ai version $HOME",
903
+ "gentle-ai version $(printf nested)",
904
+ "gentle-ai sdd-attempt grant --change secret-change # ignored comment",
905
+ "gentle-ai sdd-attempt grant --authorization-root /private/root && echo done",
906
+ "gentle-ai sdd-attempt grant --authorization-root=/private/root | tee output",
907
+ "/package/.gentle-ai/v2.2.0/gentle-ai-copy version",
908
+ "echo gentle-ai version",
909
+ ];
910
+
911
+ for (const command of commands) {
912
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
913
+ const output = renderToString(
914
+ tool.renderResult(textResult("original command output"), { expanded: true, isPartial: false }, passthroughTheme, { args: { command } }),
915
+ );
916
+ assert.equal(call.trimEnd(), `$ ${command}`);
917
+ assert.doesNotMatch(call, /🌹︎ Gentle AI/);
918
+ assert.match(output, /original command output/);
919
+ }
920
+ });
921
+
922
+ test("quiet Bash previews semantic lines for generic JSON output", () => {
923
+ const tools = registeredQuietTools();
924
+ const tool = tools.get("bash");
925
+ const command = "gentle-ai review capabilities --contract gentle-ai.review-integration/v2 | python3 -m json.tool";
926
+ const json = (value: unknown) => JSON.stringify(value, null, 2);
927
+ const object = json({ schema: "gentle-ai.review-integration/v2", contract: "review", protocol: { version: 2 }, payload: { items: [{ value: true }] } });
928
+ const error = json({ error: "failed", payload: { items: [{ code: 1 }] } });
929
+ const cases = [
930
+ [object, ' "schema": "gentle-ai.review-integration/v2",\n "contract": "review",\n "protocol": {'],
931
+ [json(["alpha", "beta", { entry: true }]), ' "alpha",\n "beta",\n "entry": true'],
932
+ ['{\n "schema": "broken",\n "contract": "partial",\n "payload": [\n "value"\n ]', ' "payload": [\n "value"\n ]'],
933
+ ] as const;
934
+ for (const [text, expected] of cases) {
935
+ assert.equal(formatToolResultOutput("bash", textResult(text) as any, { expanded: false, args: { command } }), `\n${expected}`);
936
+ }
937
+ assert.equal(formatToolResultOutput("bash", textResult(error) as any, { expanded: false, isError: true, args: { command } }), `\n${tailLines(error, 3)}`);
938
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
939
+ const collapsed = renderToolResult(tool, textResult(object), { expanded: false, isPartial: false }, { args: { command } });
940
+ const expanded = renderToolResult(tool, textResult(object), { expanded: true, isPartial: false }, { args: { command } });
941
+ assert.equal(call.trimEnd(), `$ ${command}`);
942
+ assert.doesNotMatch(call, /🌹︎ Gentle AI/);
943
+ assert.doesNotMatch(collapsed, /^\n\s*[}\]]/);
944
+ const expandHint = keyHint("app.tools.expand", "to expand");
945
+ assert.equal(collapsed.split(expandHint).length - 1, 1);
946
+ assert.equal(expanded, `\n${object}`);
947
+ assert.doesNotMatch(expanded, /to expand/);
948
+ });
949
+
210
950
  test("quiet tool rendering keeps collapsed git bash result tails", () => {
211
951
  const text = Array.from({ length: 12 }, (_, index) => `git line ${index + 1}`).join("\n");
212
952
 
@@ -214,12 +954,12 @@ test("quiet tool rendering keeps collapsed git bash result tails", () => {
214
954
  assert.equal(formatToolResultOutput("bash", textResult(text) as any, { expanded: false, args: { command: "git status --short" } }), `\n${tailLines(text, 10)}`);
215
955
  });
216
956
 
217
- test("quiet tool rendering keeps collapsed edit and write result tails", () => {
957
+ test("quiet tool rendering keeps concise collapsed edit and write summaries", () => {
218
958
  const text = Array.from({ length: 12 }, (_, index) => `line ${index + 1}`).join("\n");
219
959
 
220
960
  assert.equal(tailLines(text, 10), Array.from({ length: 10 }, (_, index) => `line ${index + 3}`).join("\n"));
221
- assert.equal(formatToolResultOutput("edit", textResult(text) as any, { expanded: false }), `\n${tailLines(text, 10)}`);
222
- assert.equal(formatToolResultOutput("write", textResult(text) as any, { expanded: false }), `\n${tailLines(text, 10)}`);
961
+ assert.equal(formatToolResultOutput("edit", textResult(text, { diff: "@@\n-old\n+new" }) as any, { expanded: false }), "\n +1 / -1");
962
+ assert.equal(formatToolResultOutput("write", textResult("Successfully wrote 12 bytes\n" + text) as any, { expanded: false }), "\n wrote 12 bytes");
223
963
  });
224
964
 
225
965
  test("quiet tool rendering sanitizes collapsed output and call rows", () => {
@@ -227,12 +967,26 @@ test("quiet tool rendering sanitizes collapsed output and call rows", () => {
227
967
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
228
968
 
229
969
  const collapsed = renderToString(
230
- tools.get("write").renderResult(textResult("safe\x1b[31mred\x1b[0m"), { expanded: false, isPartial: false }, passthroughTheme, {}),
970
+ tools.get("bash").renderResult(textResult("safe\x1b[31mred\x1b[0m"), { expanded: false, isPartial: false }, passthroughTheme, { args: { command: "printf output" } }),
231
971
  );
232
972
  const call = renderToString(tools.get("bash").renderCall({ command: "echo \x1b[31mred\x1b[0m" }, passthroughTheme, {}));
233
973
 
234
- assert.equal(collapsed.replace(/[ \t]+$/gm, ""), "\nsafered");
974
+ const carriageReturn = renderToolResult(tools.get("bash"), textResult("prefix\rSECRET\r\nnext"), { expanded: false, isPartial: false }, { args: { command: "printf output" } });
975
+ assert.match(collapsed, /safered/);
976
+ assert.doesNotMatch(collapsed, /\x1b\[31m|\x1b\[0m/);
235
977
  assert.equal(call.trimEnd(), "$ echo red");
978
+ assert.match(carriageReturn, /prefixSECRET\nnext/);
979
+ assert.doesNotMatch(carriageReturn, /\r/);
980
+ });
981
+
982
+ test("quiet tool rendering sanitizes only rendered fields", () => {
983
+ let argsReads = 0, detailReads = 0;
984
+ const counted = (record: Record<string, unknown>, increment: () => void) => Object.defineProperties(record, Object.fromEntries(Array.from({ length: 1000 }, (_, index) => [`unused${index}`, { enumerable: true, get() { increment(); return "ignored"; } }])));
985
+ const args = counted({ command: "printf output" }, () => argsReads++);
986
+ const details = counted({}, () => detailReads++);
987
+ renderToolResult(registeredQuietTools().get("bash"), textResult("first\nsecond", details), { expanded: false, isPartial: false }, { args });
988
+ assert.equal(argsReads, 0);
989
+ assert.equal(detailReads, 0);
236
990
  });
237
991
 
238
992
  test("quiet tool rendering call rows show tool calls without result output", () => {
@@ -246,4 +1000,277 @@ test("quiet tool rendering call rows show tool calls without result output", ()
246
1000
  assert.match(readCall, /read .*example\.ts:2-4/);
247
1001
  assert.match(bashCall, /\$ printf noisy \(timeout 5s\)/);
248
1002
  assert.match(grepCall, /grep \/needle\/ in src \(\*\.ts\)/);
1003
+ });
1004
+
1005
+ test("quiet tool rendering limits collapsed visual rows at the actual width", () => {
1006
+ const tools = registeredQuietTools();
1007
+ const bash = tools.get("bash");
1008
+ const hint = keyHint("app.tools.expand", "to expand");
1009
+ const narrowWidth = 12;
1010
+ const cases = [
1011
+ [textResult("x".repeat(80)), { expanded: false, isPartial: true }, { args: { command: "printf output" } }, 4],
1012
+ [textResult("界".repeat(40)), { expanded: false, isPartial: false }, { args: { command: "printf output" } }, 4],
1013
+ [textResult("e\u0301".repeat(40)), { expanded: false, isPartial: false, isError: true }, { args: { command: "false" }, isError: true }, 4],
1014
+ ] as const;
1015
+ for (const [result, options, context, maxRows] of cases) {
1016
+ const lines = renderLines(bash.renderResult(result, options, passthroughTheme, context), narrowWidth);
1017
+ assert.ok(lines.length <= maxRows, `expected at most ${maxRows} rows, got ${lines.length}`);
1018
+ assert.ok(lines.every((line) => visibleWidth(line) <= narrowWidth));
1019
+ }
1020
+ const completed = renderLines(bash.renderResult(textResult("😀".repeat(40)), { expanded: false, isPartial: false }, passthroughTheme, { args: { command: "printf output" } }), narrowWidth);
1021
+ assert.equal(completed.filter((line) => line.includes(hint)).length, 1);
1022
+ assert.ok(completed.every((line) => visibleWidth(line) <= narrowWidth));
1023
+ });
1024
+
1025
+ test("quiet tool rendering classifies quoted and escaped literal shell metacharacters as direct Gentle AI calls", () => {
1026
+ const tool = registeredQuietTools().get("bash");
1027
+ for (const command of [
1028
+ "gentle-ai review status '--note=$|#;'",
1029
+ '"gentle-ai" review status "literal \\$|#;"',
1030
+ "gentle\\-ai review status escaped\\ space",
1031
+ ]) {
1032
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
1033
+ const collapsed = renderToolResult(tool, textResult("private output"), { expanded: false, isPartial: false }, { args: { command } });
1034
+ assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1035
+ assert.doesNotMatch(collapsed, /private output/, command);
1036
+ }
1037
+ });
1038
+
1039
+ test("quiet tool rendering keeps unescaped expansions inside double quotes generic", () => {
1040
+ const tool = registeredQuietTools().get("bash");
1041
+ const commands = [
1042
+ 'gentle-ai review status "$VALUE"',
1043
+ 'gentle-ai review status "$(printf nested)"',
1044
+ 'gentle-ai review status "`printf nested`"',
1045
+ ] as const;
1046
+
1047
+ for (const command of commands) {
1048
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
1049
+ const collapsed = renderToolResult(
1050
+ tool,
1051
+ textResult("original command output"),
1052
+ { expanded: false, isPartial: false },
1053
+ { args: { command } },
1054
+ );
1055
+ assert.equal(gentleAiRoutineCommand({ command }), undefined, command);
1056
+ assert.equal(call.trimEnd(), `$ ${command}`, command);
1057
+ assert.match(collapsed, /original command output/, command);
1058
+ }
1059
+ });
1060
+
1061
+ test("quiet tool rendering recognizes a quoted exact dev override path containing spaces", () => {
1062
+ const devPath = "/opt/Gentle AI/gentle-ai";
1063
+ const tool = registeredQuietToolsWithResolver(() => ({ path: devPath })).get("bash");
1064
+ const command = `"${devPath}" review status "literal \\$|#;"`;
1065
+ const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
1066
+ const collapsed = renderToolResult(tool, textResult("private result"), { expanded: false, isPartial: false }, { args: { command } });
1067
+ assert.equal(call.trimEnd(), "🌹︎ Gentle AI · running · review status");
1068
+ assert.doesNotMatch(call, /\/opt\/Gentle AI\/gentle-ai|literal/);
1069
+ assert.doesNotMatch(collapsed, /private result/);
1070
+ });
1071
+
1072
+ test("quiet tool rendering bounds and sanitizes partial text without a completion hint", () => {
1073
+ const tool = registeredQuietTools().get("bash");
1074
+ const result = textResult("first\nsecond\nthird\nfourth");
1075
+ const collapsed = renderToolResult(tool, result, { expanded: false, isPartial: true }, { args: { command: "printf output" } });
1076
+ const expanded = renderToolResult(tool, result, { expanded: true, isPartial: true }, { args: { command: "printf output" } });
1077
+
1078
+ assert.match(collapsed, /… bash · 4 lines/);
1079
+ assert.doesNotMatch(collapsed, /first/);
1080
+ assert.match(collapsed, /second\nthird\nfourth/);
1081
+ assert.doesNotMatch(collapsed, /to expand|Ctrl\+O/);
1082
+ assert.match(expanded, /first\nsecond\nthird\nfourth/);
1083
+ assert.doesNotMatch(expanded, /to expand|Ctrl\+O/);
1084
+ assert.match(renderToolResult(tool, textResult("first\n\nsecond\n\n"), { expanded: false, isPartial: true }, { args: { command: "printf output" } }), /… bash · 2 lines/);
1085
+
1086
+ const sanitized = renderToolResult(tool, textResult("safe\x1b]52;c;Y2xpcGJvYXJk\x07\x1b[2Jdone"), { expanded: true, isPartial: true }, { args: { command: "echo safe" } });
1087
+ assert.match(sanitized, /safedone/);
1088
+ assert.doesNotMatch(sanitized, /\x1b/);
1089
+
1090
+ for (const [value, options] of [
1091
+ [textResult(""), { expanded: false, isPartial: true }],
1092
+ [{ content: [{ type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" }] }, { expanded: true, isPartial: true }],
1093
+ ] as const) {
1094
+ assert.equal(renderToolResult(tool, value, options, { args: { command: "true" } }).trimEnd(), "… bash");
1095
+ }
1096
+ });
1097
+
1098
+ test("quiet tool rendering bounds completed previews, errors, and edit/write summaries", () => {
1099
+ const tools = registeredQuietTools();
1100
+ for (const [toolName, text, hidden, visible, context] of [
1101
+ ["read", "read one\nread two\nread three\nread four", "read four", "read one\nread two\nread three", {}],
1102
+ ["bash", "bash one\nbash two\nbash three\nbash four", "bash one", "bash two\nbash three\nbash four", { args: { command: "printf output" } }],
1103
+ ] as const) {
1104
+ const rendered = renderToolResult(tools.get(toolName), textResult(text), { expanded: false, isPartial: false }, context);
1105
+ assert.doesNotMatch(rendered, new RegExp(hidden));
1106
+ assert.match(rendered, new RegExp(visible));
1107
+ assert.match(rendered, /to expand/);
1108
+ }
1109
+
1110
+ for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"] as const) {
1111
+ const rendered = renderToolResult(tools.get(toolName), textResult("error one\nerror two\nerror three\nerror four"), { expanded: false, isPartial: false, isError: true }, { args: toolName === "bash" ? { command: "false" } : {} });
1112
+ assert.doesNotMatch(rendered, /error one/);
1113
+ assert.match(rendered, /error two\nerror three\nerror four/);
1114
+ assert.match(rendered, /to expand/);
1115
+ }
1116
+
1117
+ const edit = renderToolResult(tools.get("edit"), textResult("Applied", { diff: "@@\n-old\n+new" }), { expanded: false, isPartial: false }, { args: { path: "file.ts", edits: [] } });
1118
+ const write = renderToolResult(tools.get("write"), textResult("Successfully wrote 4 bytes\nbody"), { expanded: false, isPartial: false }, { args: { path: "file.ts", content: "body" } });
1119
+ assert.match(edit, /\+1 \/ -1/);
1120
+ assert.doesNotMatch(edit, /Applied|old|new/);
1121
+ assert.match(write, /wrote 4 bytes/);
1122
+ assert.doesNotMatch(write, /body/);
1123
+ });
1124
+
1125
+ test("quiet tool rendering preserves complete expanded text and delegates sanitized image reads", () => {
1126
+ const tools = registeredQuietTools();
1127
+ const expanded = renderToolResult(tools.get("bash"), textResult("first\nsecond\nthird\nfourth\nsafe\x1b]52;c;Y2xpcGJvYXJk\x07done"), { expanded: true, isPartial: false }, { args: { command: "printf safe" } });
1128
+ assert.match(expanded, /first\nsecond\nthird\nfourth\nsafedone/);
1129
+ assert.doesNotMatch(expanded, /\x1b/);
1130
+
1131
+ const edit = renderToolResult(tools.get("edit"), textResult("Applied", { diff: "@@\n-old\x1b]52;c;Y2xpcGJvYXJk\x07\n+new" }), { expanded: true, isPartial: false }, { args: { path: "file\x1b[2J.ts", edits: [{ oldText: "old\x1b[31m", newText: "new" }] } });
1132
+ assert.match(edit, /-old\n\+new/);
1133
+ assert.doesNotMatch(edit, /\x1b/);
1134
+
1135
+ const image = renderToolResult(
1136
+ tools.get("read"),
1137
+ { content: [{ type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" }], details: { path: "image\x1b[2J.png", note: "safe\x1b]2;title\x07" } },
1138
+ { expanded: true, isPartial: false },
1139
+ { args: { path: "image\x1b[2J.png", offset: 1, limit: 2 }, cwd: "/repo\x1b[2J", showImages: false, isError: false },
1140
+ );
1141
+ assert.ok(image.includes(imageFallback("image/png")));
1142
+ assert.doesNotMatch(image, /\x1b/);
1143
+ });
1144
+
1145
+ test("quiet tool rendering preserves directional visual preview rows at narrow widths", () => {
1146
+ const tools = registeredQuietTools();
1147
+ const bash = tools.get("bash");
1148
+ const readTool = tools.get("read");
1149
+ const narrowWidth = 12;
1150
+ const renderCollapsed = (
1151
+ text: string,
1152
+ options: { expanded: false; isPartial: boolean; isError?: boolean },
1153
+ context: Record<string, unknown>,
1154
+ ) => renderLines(bash.renderResult(textResult(text), options, passthroughTheme, context), narrowWidth);
1155
+ const renderRead = (text: string) => renderLines(
1156
+ readTool.renderResult(textResult(text), { expanded: false, isPartial: false }, passthroughTheme, {}),
1157
+ narrowWidth,
1158
+ );
1159
+ const oldText = "older wrapped text ".repeat(16);
1160
+
1161
+ const partial = renderCollapsed(`${oldText}\nPARTIAL-NEW`, { expanded: false, isPartial: true }, { args: { command: "printf output" } });
1162
+ const growingPartial = renderCollapsed(`${oldText}GROW-SUFFIX`, { expanded: false, isPartial: true }, { args: { command: "printf output" } });
1163
+ const completedBash = renderCollapsed(`${oldText}\nBASH-NEW`, { expanded: false, isPartial: false }, { args: { command: "printf output" } });
1164
+ const error = renderCollapsed(`${oldText}\nERROR-NEW`, { expanded: false, isPartial: false, isError: true }, { args: { command: "false" }, isError: true });
1165
+ const git = renderCollapsed(`${oldText}\nGIT-NEW`, { expanded: false, isPartial: false }, { args: { command: "git diff" } });
1166
+
1167
+ for (const [lines, marker] of [
1168
+ [partial, "PARTIAL-NEW"],
1169
+ [growingPartial, "GROW-SUFFIX"],
1170
+ [completedBash, "BASH-NEW"],
1171
+ [error, "ERROR-NEW"],
1172
+ [git, "GIT-NEW"],
1173
+ ] as const) {
1174
+ assert.ok(lines.length <= 4, `expected fixed preview budget, got ${lines.length}`);
1175
+ assert.match(lines.join("\n"), new RegExp(marker));
1176
+ }
1177
+ assert.match(partial[0] ?? "", /… bash/);
1178
+ assert.match(completedBash.join("\n"), /to expand/);
1179
+
1180
+ const read = renderRead(`READ-FIRST\n${oldText}\nREAD-NEW`);
1181
+ const semanticJson = renderCollapsed(
1182
+ JSON.stringify({ first: "x".repeat(80), newest: "JSON-NEW" }, null, 2),
1183
+ { expanded: false, isPartial: false },
1184
+ { args: { command: "printf output" } },
1185
+ );
1186
+ assert.match(read.join("\n"), /READ-FIRST/);
1187
+ assert.doesNotMatch(read.join("\n"), /READ-NEW/);
1188
+ assert.match(semanticJson.join("\n"), /first/);
1189
+ assert.doesNotMatch(semanticJson.join("\n"), /JSON-NEW/);
1190
+ assert.match(
1191
+ renderToolResult(bash, textResult(`${oldText}\nBASH-NEW`), { expanded: true, isPartial: false }, { args: { command: "printf output" } }),
1192
+ /BASH-NEW/,
1193
+ );
1194
+ });
1195
+
1196
+ test("quiet tool rendering fails closed on unquoted shell expansions", () => {
1197
+ const bash = registeredQuietTools().get("bash");
1198
+ for (const command of [
1199
+ "gentle-ai review status *",
1200
+ "gentle-ai review status ?",
1201
+ "gentle-ai review status [a-z]",
1202
+ "gentle-ai review status ~",
1203
+ "gentle-ai review status {one,two}",
1204
+ ]) {
1205
+ assert.equal(gentleAiRoutineCommand({ command }), undefined, command);
1206
+ assertGenericBash(bash, command);
1207
+ }
1208
+
1209
+ for (const command of [
1210
+ "gentle-ai review status '*'",
1211
+ "gentle-ai review status \"?\"",
1212
+ "gentle-ai review status '[a-z]'",
1213
+ "gentle-ai review status \"~\"",
1214
+ "gentle-ai review status '{one,two}'",
1215
+ "gentle-ai review status \\*",
1216
+ "gentle-ai review status \\?",
1217
+ "gentle-ai review status \\[a-z\\]",
1218
+ "gentle-ai review status \\~",
1219
+ "gentle-ai review status \\{one,two\\}",
1220
+ ]) {
1221
+ const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
1222
+ const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
1223
+ assert.equal(gentleAiRoutineCommand({ command }), "review", command);
1224
+ assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1225
+ assert.doesNotMatch(collapsed, /dummy-secret/, command);
1226
+ }
1227
+ });
1228
+
1229
+ test("quiet tool rendering redacts only exact package-local executable paths with spaces", () => {
1230
+ const bash = registeredQuietTools().get("bash");
1231
+ const validCommands = [
1232
+ "'/opt/Gentle Package/.gentle-ai/v2.2.0/gentle-ai' review status --token single-secret",
1233
+ '"/opt/Gentle Package/.gentle-ai/v2.2.0/gentle-ai" review status --token double-secret',
1234
+ "/opt/Gentle\\ Package/.gentle-ai/v2.2.0/gentle-ai review status --token escaped-secret",
1235
+ ] as const;
1236
+ for (const command of validCommands) {
1237
+ const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
1238
+ const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
1239
+ assert.equal(gentleAiRoutineCommand({ command }), "review", command);
1240
+ assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1241
+ assert.doesNotMatch(call, /Gentle Package|secret/, command);
1242
+ assert.doesNotMatch(collapsed, /dummy-secret/, command);
1243
+ }
1244
+
1245
+ for (const command of [
1246
+ "'/opt/Gentle Package/.gentle-ai/v2.2.0/gentle-ai-copy' review status --token copy-secret",
1247
+ '"/opt/Gentle Package/.gentle-ai/v2.2.0/gentle-ai.exe.bak" review status --token backup-secret',
1248
+ "/opt/Gentle\\ Package/.gentle-ai/v2.2.0/not-gentle-ai review status --token sibling-secret",
1249
+ ]) {
1250
+ const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
1251
+ const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
1252
+ assert.equal(gentleAiRoutineCommand({ command }), undefined, command);
1253
+ assert.equal(call.trimEnd(), `$ ${command}`, command);
1254
+ assert.match(collapsed, /dummy-secret/, command);
1255
+ }
1256
+ });
1257
+
1258
+ test("quiet tool rendering respects command and env wrapper order", () => {
1259
+ const bash = registeredQuietTools().get("bash");
1260
+ for (const command of [
1261
+ "FOO=bar command -- env BAR=baz gentle-ai review status",
1262
+ "command -- env FOO=bar gentle-ai review status",
1263
+ "env FOO=bar gentle-ai review status",
1264
+ "command -- gentle-ai review status",
1265
+ ]) {
1266
+ const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
1267
+ const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
1268
+ assert.equal(gentleAiRoutineCommand({ command }), "review", command);
1269
+ assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1270
+ assert.doesNotMatch(collapsed, /dummy-secret/, command);
1271
+ }
1272
+
1273
+ const command = "env FOO=bar command -- gentle-ai review status";
1274
+ assert.equal(gentleAiRoutineCommand({ command }), undefined);
1275
+ assertGenericBash(bash, command);
249
1276
  });