pi-tool-discipline 0.1.16 → 0.2.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.
package/README.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # pi-tool-discipline
2
2
 
3
- <p align="center">
4
- <img src="assets/banner.png" alt="pi-tool-discipline" width="600">
5
- </p>
6
-
7
3
  pi extension that enforces a **search-tools-first discipline** and removes the
8
4
  conflicting "use bash for file operations" guidance pi injects by default.
5
+ **Prompt-level only: it activates the right tools and edits the system prompt;
6
+ it does not intercept or parse bash commands.**
9
7
 
10
8
  ## Why
11
9
 
@@ -19,18 +17,14 @@ tool, not bash").
19
17
 
20
18
  ## How it works
21
19
 
22
- Three mechanisms, applied automatically in every session:
20
+ Two mechanisms, applied automatically in every session:
23
21
 
24
- 1. **Activate search tools (root fix).** pi 0.84+ ships real `grep` / `find` /
22
+ 1. **Activate search tools (root fix).** pi >= 0.84 ships real `grep` / `find` /
25
23
  `ls` built-in tool definitions but only activates `read`/`bash`/`edit`/
26
24
  `write` by default. This extension activates the built-ins, so pi's
27
25
  `hasGrep`/`hasFind`/`hasLs` check passes and the conflicting
28
26
  `Use bash for file operations like ls, rg, find` guideline is **never
29
- generated** — the model is never told to use bash for searching. On older
30
- pi versions without these built-ins, fs-based fallbacks
31
- (`extensions/search.ts`) are registered instead, with their text snippet
32
- suppressed while `ffgrep`/`fffind` from pi-fff are active (the registered
33
- tool schema remains present either way).
27
+ generated** — the model is never told to use bash for searching.
34
28
  2. **System-prompt injection (rules).** On every agent start, appends an
35
29
  idempotent "Tool Discipline" section to the system prompt: content search
36
30
  with `ffgrep`, path search with `fffind`, file reads with `read`
@@ -38,13 +32,22 @@ Three mechanisms, applied automatically in every session:
38
32
  `which` for searching or reading, bash reserved for pipelines/git/npm/network, `rg`
39
33
  (never `grep`) as the last resort in pipelines. Also strips the bash guideline text as
40
34
  a belt-and-suspenders fallback.
41
- 3. **Runtime Guardrail (interception).** Intercepts `tool_call` events for
42
- `bash` and `powershell`. If the model attempts to invoke prohibited file
43
- operations (`ls`, `cat`, `grep`, `find`, `sed`, `which`, or unpiped
44
- `head`/`tail` directly on files), execution is blocked at runtime with
45
- actionable feedback guiding the model to use the proper tool (`read`,
46
- `ffgrep`, `fffind`, `ls`, etc.), while safely preserving legitimate builds,
47
- tests, git operations, and pipelines.
35
+
36
+ ## Why no bash interception?
37
+
38
+ Earlier versions (<= 0.1.x) added a runtime guard: a hand-written
39
+ bash/PowerShell command parser that blocked prohibited file operations at
40
+ `tool_call` time. Maintaining a parser that precisely recognizes nested
41
+ subshells, here-docs, wrappers and redirects — without misjudging legitimate
42
+ pipelines — grew to ~1000 lines and became a hang/freeze risk (a stray `)` in
43
+ a command could spin the event loop at 100% CPU, freezing the whole session).
44
+
45
+ Behavior guidance at the prompt level is probabilistic but safe; code-level
46
+ enforcement would require near-complete shell parsing — impractical and
47
+ hazardous. The activation + injection mechanisms above already remove the
48
+ root cause (pi no longer tells the model to use bash), so interception added
49
+ little value. This version drops it entirely: no command parsing, no
50
+ interception, no hang surface.
48
51
 
49
52
  ## Install
50
53
 
@@ -54,14 +57,14 @@ pi install npm:pi-tool-discipline
54
57
  pi -e npm:pi-tool-discipline
55
58
  ```
56
59
 
57
- Requires nothing extra. With `@ff-labs/pi-fff` installed, the model prefers
58
- `ffgrep`/`fffind`; without it, Pi's built-in `grep`/`find`/`ls` tools (activated
59
- by this extension, or fs-based fallbacks on older pi versions) are used.
60
+ Requires **pi >= 0.84** (built-in `grep`/`find`/`ls` definitions). With
61
+ `@ff-labs/pi-fff` installed, the model prefers `ffgrep`/`fffind`; without it,
62
+ Pi's built-in `grep`/`find`/`ls` tools (activated by this extension) are used.
60
63
 
61
64
  ## Verify
62
65
 
63
- Run `/tool-discipline` in a pi session — it reports whether the placeholder
64
- tools are registered and whether the discipline section is injected.
66
+ Run `/tool-discipline` in a pi session — it reports whether the search tools
67
+ are registered/active and whether the discipline section is injected.
65
68
 
66
69
  You can also inspect the system prompt: the string
67
70
  `Use bash for file operations like ls, rg, find` should no longer appear, and
@@ -72,18 +75,8 @@ You can also inspect the system prompt: the string
72
75
  This extension runs with full system access like any pi extension. What it does:
73
76
  - Activates pi's built-in `grep`/`find`/`ls` tools (read-only file search).
74
77
  - Injects text into the system prompt (discipline rules).
75
- - On pi versions without built-in search tools, registers read-only fs-based
76
- fallback implementations that read file contents under the searched path.
77
-
78
- **Disclosure:** pi's built-in `grep`/`find` tools execute the `rg`/`fd`
79
- binaries, and pi may auto-download those binaries from GitHub on first use
80
- (`ensureTool`). This extension itself does not execute commands, write files,
81
- or touch the network — that claim covers only its own fs-based fallback
82
- implementations, not the pi built-ins it activates.
83
-
84
- The fallback search tools only read. Note that any installed tool, including
85
- this one, can be invoked by the model. Review the source in `extensions/`
86
- before installing.
78
+
79
+ It executes no commands, writes no files, and touches no network.
87
80
 
88
81
  ## License
89
82
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * pi-tool-discipline
2
+ * pi-tool-discipline (slim)
3
3
  *
4
4
  * Enforces a "search tools first" file-search discipline in pi sessions and
5
5
  * neutralizes the built-in "Use bash for file operations like ls, rg, find"
@@ -12,29 +12,27 @@
12
12
  * search tool exists and tells the model to use bash — fighting AGENTS.md
13
13
  * rules that say to use the grep tool instead.
14
14
  *
15
- * How it works:
16
- * A. Ensures `grep` / `find` / `ls` tools are ACTIVE in the session. pi 0.84+
17
- * ships real built-in definitions (createAllToolDefinitions) but only
18
- * activates read/bash/edit/write by default this extension activates
19
- * the built-ins, so pi's hasGrep/hasFind/hasLs check passes and the
20
- * conflicting bash guideline is never generated, and the model always has
21
- * search tools. On older pi versions without the built-ins, fs-based
22
- * fallbacks (search.ts) are registered instead (visible only when the
23
- * FFF counterpart ffgrep/fffind is absent).
15
+ * How it works (prompt-level only — no bash interception):
16
+ * A. Ensures pi's built-in `grep` / `find` / `ls` tools are ACTIVE in the
17
+ * session (pi 0.84+ ships real definitions via createAllToolDefinitions
18
+ * but only activates read/bash/edit/write by default). Activating them
19
+ * flips pi's hasGrep/hasFind/hasLs check, so the conflicting bash
20
+ * guideline is never generated and the model always has search tools.
24
21
  * B. On before_agent_start, appends the tool-discipline rules to the system
25
22
  * prompt (idempotent) and strips the bash guideline text as a fallback.
23
+ *
24
+ * No runtime tool_call interception: behavior is guided at the prompt level,
25
+ * which means no shell-command parsing and no risk of parser hangs. Requires
26
+ * pi >= 0.84 for the built-in grep/find/ls definitions.
26
27
  */
27
28
 
28
29
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
29
- import { Type } from "typebox";
30
- import { grepFiles, findFiles, listDir, grepSchema, findSchema, lsSchema } from "./search.js";
31
30
  import { stripBashGuidelines } from "./strip.js";
32
- import { checkDisciplineViolation } from "./guard.js";
33
31
 
34
32
  const MARK = "<!-- pi-tool-discipline:v1 -->";
35
33
 
36
34
  /** Tool names that flip pi's hasGrep/hasFind/hasLs checks. */
37
- const PLACEHOLDER_NAMES = ["grep", "find", "ls"] as const;
35
+ const SEARCH_TOOLS = ["grep", "find", "ls"] as const;
38
36
 
39
37
  /** FFF search tools that indicate pi-fff (or equivalent) is installed. */
40
38
  const FFF_TOOLS = ["ffgrep", "fffind"];
@@ -45,103 +43,25 @@ const DISCIPLINE = `
45
43
  Search tool priority:
46
44
 
47
45
  1. ffgrep / fffind (from @ff-labs/pi-fff) — always preferred. They work with absolute paths outside the workspace and support regex / path / exclude filters.
48
- 2. Without pi-fff, use the grep / find / ls TOOLS (Pi built-ins on pi 0.84+, fs fallbacks on older versions). Fill their parameters according to each tool's declared schema — built-in grep uses ignoreCase/limit, built-in find uses a glob pattern. Never fall back to bash's grep or find.
46
+ 2. Without pi-fff, use the grep / find / ls TOOLS (Pi built-ins on pi 0.84+). Fill their parameters according to each tool's declared schema — built-in grep uses ignoreCase/limit, built-in find uses a glob pattern. Never fall back to bash's grep or find.
49
47
  3. Never run bash \`grep\` or \`find\`. Never use bash \`ls\`/\`cat\`/\`head\`/\`tail\`/\`sed\`/\`which\` directly for searching or reading — use ffgrep / fffind / read (or the grep/find/ls tools) instead.
50
48
  4. Read files with \`read\` (offset/limit for large files).
51
49
  5. Bash stays allowed only when dedicated tools cannot do the job: pipelines, git, npm, running programs, network requests, file mutations.
52
50
  6. If bash searching is truly unavoidable, use \`rg\` (never \`grep\`).`;
53
51
 
54
- interface FallbackTool {
55
- label: string;
56
- description: string;
57
- snippet: string;
58
- parameters: ReturnType<typeof Type.Object>;
59
- execute: (params: any, cwd: string, signal?: AbortSignal) => Promise<string>;
60
- }
61
-
62
- const FALLBACK_TOOLS: Record<string, FallbackTool> = {
63
- grep: {
64
- label: "grep (fallback)",
65
- description:
66
- "Search file contents for a text pattern. Fallback for environments without ffgrep; prefer ffgrep when available.",
67
- snippet: "Search file contents (fallback when ffgrep is unavailable)",
68
- parameters: grepSchema,
69
- execute: async (params, cwd, signal) => grepFiles({ ...params, cwd, signal }),
70
- },
71
- find: {
72
- label: "find (fallback)",
73
- description:
74
- "Find files by path/name substring. Fallback for environments without fffind; prefer fffind when available.",
75
- snippet: "Find files by path/name (fallback when fffind is unavailable)",
76
- parameters: findSchema,
77
- execute: async (params, cwd, signal) => findFiles({ ...params, cwd, signal }),
78
- },
79
- ls: {
80
- label: "ls (fallback)",
81
- description:
82
- "List directory entries. Fallback for environments without fffind; prefer fffind when available.",
83
- snippet: "List directory entries (fallback when fffind is unavailable)",
84
- parameters: lsSchema,
85
- execute: async (params, cwd, signal) => listDir({ ...params, cwd, signal }),
86
- },
87
- };
88
-
89
52
  export default function toolDiscipline(pi: ExtensionAPI) {
90
- // A. Register search-tool names so pi stops generating the bash guideline.
91
- // Done in session_start: action methods (getAllTools/registerTool) are not
92
- // available during extension loading, and tools registered here are
53
+ // A. Activate pi's built-in search tools so pi stops generating the bash
54
+ // guideline. Done in session_start: action methods (setActiveTools) are
55
+ // not available during extension loading, and tools activated here are
93
56
  // refreshed into the session (and system prompt) immediately.
94
57
  pi.on("session_start", () => {
95
58
  const all = new Set(pi.getAllTools().map((t) => t.name));
96
59
  const active = new Set(pi.getActiveTools());
97
- const hasFfgrep = active.has("ffgrep");
98
- const hasFffind = active.has("fffind");
99
-
100
- // 1) Activate built-in grep/find/ls when they exist (pi 0.84+ ships real
101
- // definitions via createAllToolDefinitions but only activates
102
- // read/bash/edit/write by default). Activating them flips
103
- // hasGrep/hasFind/hasLs so the bash guideline is never generated, and
104
- // gives the model real built-in search tools.
105
- const toActivate = PLACEHOLDER_NAMES.filter((name) => all.has(name) && !active.has(name));
60
+ const toActivate = SEARCH_TOOLS.filter((name) => all.has(name) && !active.has(name));
106
61
  if (toActivate.length > 0) {
107
62
  // setActiveTools rebuilds the system prompt immediately.
108
63
  pi.setActiveTools([...active, ...toActivate]);
109
64
  }
110
-
111
- // 2) Older pi without built-in grep/find/ls: register working fs-based
112
- // fallbacks (search.ts). Visibility is per-capability: hidden while
113
- // the FFF counterpart is active, visible when it is not.
114
- let registeredAny = false;
115
- const registerSearchTool = (name: string, fffActive: boolean) => {
116
- if (all.has(name)) return; // built-in exists — handled above
117
- const fallback = FALLBACK_TOOLS[name];
118
- pi.registerTool({
119
- name,
120
- label: fallback.label,
121
- description: fallback.description,
122
- promptSnippet: fffActive ? undefined : fallback.snippet,
123
- promptGuidelines: [
124
- "Use ffgrep/fffind when they are available; grep/find/ls are fallbacks only for environments without pi-fff.",
125
- ],
126
- parameters: fallback.parameters,
127
- async execute(_toolCallId, params, signal, _onUpdate, execCtx) {
128
- const text = await fallback.execute(params ?? {}, execCtx.cwd, signal);
129
- return {
130
- content: [{ type: "text", text }],
131
- details: { fallback: true },
132
- };
133
- },
134
- });
135
- registeredAny = true;
136
- };
137
- registerSearchTool("grep", hasFfgrep);
138
- registerSearchTool("find", hasFffind);
139
- registerSearchTool("ls", hasFffind);
140
- // Tools registered in session_start do not enter selectedTools until the
141
- // registry is refreshed. refreshTools exists at runtime (ExtensionActions)
142
- // but is not declared on ExtensionAPI's type. Known limitation: with peer
143
- // version "*", other pi versions may differ.
144
- if (registeredAny) (pi as unknown as { refreshTools: () => void }).refreshTools();
145
65
  });
146
66
 
147
67
  // B. Inject the discipline into the system prompt (idempotent per turn).
@@ -152,24 +72,6 @@ export default function toolDiscipline(pi: ExtensionAPI) {
152
72
  return { systemPrompt: `${prompt}\n${MARK}\n${DISCIPLINE}` };
153
73
  });
154
74
 
155
- // C. Runtime Guardrail: intercept and block prohibited bash file operations.
156
- pi.on("tool_call", async (event) => {
157
- if (event.toolName === "bash" || event.toolName === "powershell") {
158
- const command = (event.input as { command?: string })?.command;
159
- if (typeof command === "string") {
160
- const dialect = event.toolName === "powershell" ? "powershell" : "bash";
161
- const check = checkDisciplineViolation(command, dialect);
162
- if (check.block) {
163
- return {
164
- block: true,
165
- reason: check.reason,
166
- };
167
- }
168
- }
169
- }
170
- return undefined;
171
- });
172
-
173
75
  // Status command: /tool-discipline — verify tool activation and injection.
174
76
  pi.registerCommand("tool-discipline", {
175
77
  description: "Show pi-tool-discipline status (tools + injected guideline)",
@@ -177,8 +79,8 @@ export default function toolDiscipline(pi: ExtensionAPI) {
177
79
  const all = new Set(pi.getAllTools().map((t) => t.name));
178
80
  const active = new Set(pi.getActiveTools());
179
81
  const fff = FFF_TOOLS.filter((name) => active.has(name));
180
- const registered = PLACEHOLDER_NAMES.filter((n) => all.has(n));
181
- const activeTools = PLACEHOLDER_NAMES.filter((n) => active.has(n));
82
+ const registered = SEARCH_TOOLS.filter((n) => all.has(n));
83
+ const activeTools = SEARCH_TOOLS.filter((n) => active.has(n));
182
84
  const injected = ctx.getSystemPrompt().includes(MARK);
183
85
  ctx.ui.notify(
184
86
  `pi-tool-discipline\n` +
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-tool-discipline",
3
- "version": "0.1.16",
4
- "description": "pi extension: enforce ffgrep/fffind-first search discipline and neutralize the default bash file-operation guideline",
3
+ "version": "0.2.0",
4
+ "description": "pi extension (prompt-level): enforce ffgrep/fffind-first search discipline and neutralize the default bash file-operation guideline",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -38,16 +38,14 @@
38
38
  },
39
39
  "scripts": {
40
40
  "typecheck": "tsc --noEmit",
41
- "test": "node --experimental-strip-types test/fallback.test.mjs && node --experimental-strip-types test/guard.test.mjs"
41
+ "test": "node --experimental-strip-types test/strip.test.mjs"
42
42
  },
43
43
  "peerDependencies": {
44
- "@earendil-works/pi-coding-agent": "*",
45
- "typebox": "*"
44
+ "@earendil-works/pi-coding-agent": ">=0.84"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@earendil-works/pi-coding-agent": "*",
49
48
  "@types/node": "^22.0.0",
50
- "typebox": "*",
51
49
  "typescript": "^5.0.0"
52
50
  }
53
51
  }