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 +28 -35
- package/extensions/index.ts +19 -117
- package/package.json +4 -6
- package/extensions/guard.ts +0 -1040
- package/extensions/search.ts +0 -264
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
|
-
|
|
20
|
+
Two mechanisms, applied automatically in every session:
|
|
23
21
|
|
|
24
|
-
1. **Activate search tools (root fix).** pi 0.84
|
|
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.
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
58
|
-
`ffgrep`/`fffind`; without it,
|
|
59
|
-
|
|
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
|
|
64
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
|
17
|
-
* ships real
|
|
18
|
-
* activates read/bash/edit/write by default
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
91
|
-
// Done in session_start: action methods (
|
|
92
|
-
// available during extension loading, and tools
|
|
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
|
|
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 =
|
|
181
|
-
const activeTools =
|
|
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.
|
|
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/
|
|
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
|
}
|