pi-shepherd 0.1.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 (55) hide show
  1. package/README.en.md +136 -0
  2. package/README.md +136 -0
  3. package/index.ts +229 -0
  4. package/node_modules/@pi-atelier/shared-utils/README.en.md +182 -0
  5. package/node_modules/@pi-atelier/shared-utils/README.md +182 -0
  6. package/node_modules/@pi-atelier/shared-utils/package.json +51 -0
  7. package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +120 -0
  8. package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +100 -0
  9. package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +152 -0
  10. package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +187 -0
  11. package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +170 -0
  12. package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +126 -0
  13. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +138 -0
  14. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +257 -0
  15. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +189 -0
  16. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +204 -0
  17. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +269 -0
  18. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +267 -0
  19. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +520 -0
  20. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +116 -0
  21. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +119 -0
  22. package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +145 -0
  23. package/node_modules/@pi-atelier/shared-utils/src/agents.ts +39 -0
  24. package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +42 -0
  25. package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +62 -0
  26. package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +100 -0
  27. package/node_modules/@pi-atelier/shared-utils/src/index.ts +71 -0
  28. package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +96 -0
  29. package/node_modules/@pi-atelier/shared-utils/src/paths.ts +23 -0
  30. package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +241 -0
  31. package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +191 -0
  32. package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +73 -0
  33. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +104 -0
  34. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +75 -0
  35. package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +172 -0
  36. package/node_modules/@pi-atelier/shared-utils/src/settings.ts +104 -0
  37. package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +149 -0
  38. package/node_modules/@pi-atelier/shared-utils/tsconfig.json +9 -0
  39. package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +24 -0
  40. package/package.json +49 -0
  41. package/rules.json +516 -0
  42. package/shepherd/ephemeral-shared.ts +14 -0
  43. package/shepherd/ephemeral.ts +52 -0
  44. package/shepherd/index.ts +39 -0
  45. package/shepherd/line-count.ts +86 -0
  46. package/shepherd/rules-editor.ts +135 -0
  47. package/shepherd/rules-tool.ts +99 -0
  48. package/shepherd/rules-validate.ts +44 -0
  49. package/shepherd/rules.ts +283 -0
  50. package/shepherd/state-tracker.ts +119 -0
  51. package/shepherd/tool-event-types.ts +31 -0
  52. package/shepherd/tool-hooks.ts +176 -0
  53. package/shepherd/worktree-check.ts +130 -0
  54. package/tsconfig.json +14 -0
  55. package/vitest.config.ts +13 -0
package/README.en.md ADDED
@@ -0,0 +1,136 @@
1
+ [中文文档](README.md) | English
2
+
3
+ # pi-shepherd
4
+
5
+ Line count guard and behavior rules extension for [pi-coding-agent](https://github.com/earendil-works/pi-coding-agent) — rule-driven hooks for tool calls, agent end, and session events.
6
+
7
+ ## What It Does
8
+
9
+ AI agents can go off the rails — generate too much code, forget to commit, ignore coding standards, or produce outputs that are too large. pi-shepherd acts as a **guardrail system** that monitors and enforces behavioral rules:
10
+
11
+ - **Tool call interception** — Inspect and modify tool calls before execution (e.g., enforce line limits)
12
+ - **Tool result inspection** — Check tool results after execution (e.g., flag overly large outputs)
13
+ - **Agent end hooks** — Enforce commit/message rules when the agent finishes
14
+ - **Session lifecycle** — Reset state between sessions
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pi install git:github.com/catlain/pi-shepherd
20
+ ```
21
+
22
+ ## How It Works
23
+
24
+ pi-shepherd uses a **rules engine** that evaluates configurable patterns against tool calls and results:
25
+
26
+ ```
27
+ Tool Call → Rules Engine → Pass/Block/Modify
28
+ Tool Result → Rules Engine → Pass/Flag/Truncate
29
+ Agent End → Rules Engine → Enforce (commit, summarize, etc.)
30
+ ```
31
+
32
+ ### Rules Format
33
+
34
+ Rules are defined in `rules.json` (or the `shepherd` section of settings):
35
+
36
+ ```json
37
+ [
38
+ {
39
+ "name": "block-grep-for-code-graph",
40
+ "pattern": "^grep\\s+.*\\b[A-Z][a-zA-Z]+\\(",
41
+ "type": "tool_call",
42
+ "action": "block",
43
+ "message": "Use code-graph search_symbols instead of grep for symbol names"
44
+ },
45
+ {
46
+ "name": "warn-large-edit",
47
+ "pattern": "edit",
48
+ "type": "tool_result",
49
+ "maxLines": 500,
50
+ "action": "warn",
51
+ "message": "Edit result is large, consider breaking into smaller changes"
52
+ }
53
+ ]
54
+ ```
55
+
56
+ ### Rule Types
57
+
58
+ | Type | When Evaluated | Actions |
59
+ |------|---------------|---------|
60
+ | `tool_call` | Before tool execution | `pass`, `block`, `modify` |
61
+ | `tool_result` | After tool execution | `pass`, `warn`, `truncate` |
62
+ | `agent_end` | When agent finishes | `enforce` |
63
+
64
+ ## Built-in Rules
65
+
66
+ pi-shepherd ships with default rules for common anti-patterns:
67
+
68
+ - Redirect `grep` to `code-graph` for symbol searches
69
+ - Warn on overly large tool results
70
+ - Enforce git commit on agent end
71
+ - Block redundant file reads
72
+
73
+ ## Configuration
74
+
75
+ ```json
76
+ {
77
+ "shepherd": {
78
+ "enabled": true,
79
+ "rulesDir": "~/.pi/agent/shepherd-rules"
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Use Cases
85
+
86
+ | Scenario | Rule Type | Action |
87
+ |----------|-----------|--------|
88
+ | **Enforce coding standards** | `tool_call` | Block tools that don't follow conventions |
89
+ | **Prevent context bloat** | `tool_result` | Truncate large results |
90
+ | **Git discipline** | `agent_end` | Force commit at session end |
91
+ | **Redirect to better tools** | `tool_call` | Block grep, suggest code-graph |
92
+ | **Custom team rules** | All types | Project-specific guardrails |
93
+
94
+ ## Best Practices
95
+
96
+ ### ✅ Recommended
97
+ - Start with built-in rules, then add project-specific ones
98
+ - Use `warn` before `block` — give the agent a chance to learn
99
+ - Keep rule patterns simple and specific — regex is evaluated on every tool call
100
+ - Put project rules in `.pi/shepherd-rules/` for version control
101
+
102
+ ### ❌ Not Recommended
103
+ - Don't use overly broad patterns — they'll match too many calls and slow things down
104
+ - Don't create contradictory rules (block + allow the same pattern)
105
+ - Don't rely on shepherd for security — it's a guide, not a sandbox
106
+
107
+ ## Limitations
108
+
109
+ | Limitation | Detail |
110
+ |------------|--------|
111
+ | Regex only | Patterns use regex, not semantic understanding |
112
+ | No async rules | Rules must evaluate synchronously |
113
+ | Agent can bypass | Determined agents can ignore warnings |
114
+ | No persistence | Rule state resets between sessions |
115
+
116
+ ## Architecture
117
+
118
+ ```
119
+ pi-shepherd/
120
+ ├── index.ts # Entry: register hooks + rules engine
121
+ ├── rules-engine.ts # Pattern matching + action dispatch
122
+ ├── rules/ # Built-in rule definitions
123
+ │ ├── grep.ts # Redirect grep → code-graph
124
+ │ ├── line-limit.ts # Warn on large outputs
125
+ │ └── agent-end.ts # Enforce git commit
126
+ ├── types.ts # Rule type definitions
127
+ └── package.json
128
+ ```
129
+
130
+ **Dependencies**:
131
+ - `@pi-atelier/shared-utils` (bundled) — settings management
132
+ - `@earendil-works/pi-coding-agent` — ExtensionAPI (peer)
133
+
134
+ ## License
135
+
136
+ MIT
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ [English](README.en.md) | 程序中文文档
2
+
3
+ # pi-shepherd
4
+
5
+ Line count guard and behavior rules extension for [pi-coding-agent](https://github.com/earendil-works/pi-coding-agent) — rule-driven hooks for tool calls, agent end, and session events.
6
+
7
+ ## What It Does
8
+
9
+ AI agents can go off the rails — generate too much code, forget to commit, ignore coding standards, or produce outputs that are too large. pi-shepherd acts as a **guardrail system** that monitors and enforces behavioral rules:
10
+
11
+ - **Tool call interception** — Inspect and modify tool calls before execution (e.g., enforce line limits)
12
+ - **Tool result inspection** — Check tool results after execution (e.g., flag overly large outputs)
13
+ - **Agent end hooks** — Enforce commit/message rules when the agent finishes
14
+ - **Session lifecycle** — Reset state between sessions
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pi install git:github.com/catlain/pi-shepherd
20
+ ```
21
+
22
+ ## How It Works
23
+
24
+ pi-shepherd uses a **rules engine** that evaluates configurable patterns against tool calls and results:
25
+
26
+ ```
27
+ Tool Call → Rules Engine → Pass/Block/Modify
28
+ Tool Result → Rules Engine → Pass/Flag/Truncate
29
+ Agent End → Rules Engine → Enforce (commit, summarize, etc.)
30
+ ```
31
+
32
+ ### Rules Format
33
+
34
+ Rules are defined in `rules.json` (or the `shepherd` section of settings):
35
+
36
+ ```json
37
+ [
38
+ {
39
+ "name": "block-grep-for-code-graph",
40
+ "pattern": "^grep\\s+.*\\b[A-Z][a-zA-Z]+\\(",
41
+ "type": "tool_call",
42
+ "action": "block",
43
+ "message": "Use code-graph search_symbols instead of grep for symbol names"
44
+ },
45
+ {
46
+ "name": "warn-large-edit",
47
+ "pattern": "edit",
48
+ "type": "tool_result",
49
+ "maxLines": 500,
50
+ "action": "warn",
51
+ "message": "Edit result is large, consider breaking into smaller changes"
52
+ }
53
+ ]
54
+ ```
55
+
56
+ ### Rule Types
57
+
58
+ | Type | When Evaluated | Actions |
59
+ |------|---------------|---------|
60
+ | `tool_call` | Before tool execution | `pass`, `block`, `modify` |
61
+ | `tool_result` | After tool execution | `pass`, `warn`, `truncate` |
62
+ | `agent_end` | When agent finishes | `enforce` |
63
+
64
+ ## Built-in Rules
65
+
66
+ pi-shepherd ships with default rules for common anti-patterns:
67
+
68
+ - Redirect `grep` to `code-graph` for symbol searches
69
+ - Warn on overly large tool results
70
+ - Enforce git commit on agent end
71
+ - Block redundant file reads
72
+
73
+ ## Configuration
74
+
75
+ ```json
76
+ {
77
+ "shepherd": {
78
+ "enabled": true,
79
+ "rulesDir": "~/.pi/agent/shepherd-rules"
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Use Cases
85
+
86
+ | Scenario | Rule Type | Action |
87
+ |----------|-----------|--------|
88
+ | **Enforce coding standards** | `tool_call` | Block tools that don't follow conventions |
89
+ | **Prevent context bloat** | `tool_result` | Truncate large results |
90
+ | **Git discipline** | `agent_end` | Force commit at session end |
91
+ | **Redirect to better tools** | `tool_call` | Block grep, suggest code-graph |
92
+ | **Custom team rules** | All types | Project-specific guardrails |
93
+
94
+ ## Best Practices
95
+
96
+ ### ✅ Recommended
97
+ - Start with built-in rules, then add project-specific ones
98
+ - Use `warn` before `block` — give the agent a chance to learn
99
+ - Keep rule patterns simple and specific — regex is evaluated on every tool call
100
+ - Put project rules in `.pi/shepherd-rules/` for version control
101
+
102
+ ### ❌ Not Recommended
103
+ - Don't use overly broad patterns — they'll match too many calls and slow things down
104
+ - Don't create contradictory rules (block + allow the same pattern)
105
+ - Don't rely on shepherd for security — it's a guide, not a sandbox
106
+
107
+ ## Limitations
108
+
109
+ | Limitation | Detail |
110
+ |------------|--------|
111
+ | Regex only | Patterns use regex, not semantic understanding |
112
+ | No async rules | Rules must evaluate synchronously |
113
+ | Agent can bypass | Determined agents can ignore warnings |
114
+ | No persistence | Rule state resets between sessions |
115
+
116
+ ## Architecture
117
+
118
+ ```
119
+ pi-shepherd/
120
+ ├── index.ts # Entry: register hooks + rules engine
121
+ ├── rules-engine.ts # Pattern matching + action dispatch
122
+ ├── rules/ # Built-in rule definitions
123
+ │ ├── grep.ts # Redirect grep → code-graph
124
+ │ ├── line-limit.ts # Warn on large outputs
125
+ │ └── agent-end.ts # Enforce git commit
126
+ ├── types.ts # Rule type definitions
127
+ └── package.json
128
+ ```
129
+
130
+ **Dependencies**:
131
+ - `@pi-atelier/shared-utils` (bundled) — settings management
132
+ - `@earendil-works/pi-coding-agent` — ExtensionAPI (peer)
133
+
134
+ ## License
135
+
136
+ MIT
package/index.ts ADDED
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Shepherd — 通用 Hook 规则引擎
3
+ *
4
+ * 规则驱动的事件 hook,支持多种动作:
5
+ * - tool_call: 工具调用前(可 block 拦截 / notify 提醒 / rewrite 重写)
6
+ * - tool_result: 工具执行后(可 notify 提醒 / steer 向 LLM 注入 + 行数检查)
7
+ * - agent_end: AI 正常完成时(可 notify 提醒,支持 stopReason 过滤)
8
+ * - session_shutdown: 会话结束时(可 notify 提醒)
9
+ *
10
+ * steer/notify 提示通过 before_provider_request 临时注入到 LLM payload,
11
+ * 不写入 session 历史,不占用后续上下文。
12
+ *
13
+ * 规则配置文件:
14
+ * 全局: ~/.pi/agent/extensions/shepherd/rules.json
15
+ * 项目级: <cwd>/.pi/extensions/shepherd-rules-*.json(自动扫描,叠加加载)
16
+ *
17
+ * 修改规则文件后 /reload 即可生效,无需重启 pi。
18
+ */
19
+
20
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
+ import { dirname, join } from "path";
22
+ import { fileURLToPath } from "url";
23
+
24
+ /** pi payload 消息结构的最小类型 */
25
+ interface PayloadMessage {
26
+ role: string;
27
+ content?: unknown;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ /** pi provider payload 的最小类型 */
32
+ interface ProviderPayload {
33
+ messages?: PayloadMessage[];
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ const __dirname = dirname(fileURLToPath(import.meta.url));
38
+ const RULES_DIR = __dirname;
39
+
40
+ import { getEffectiveConfig } from "@pi-atelier/shared-utils";
41
+ import {
42
+ checkWorktrees,
43
+ drainHints,
44
+ hasGitUncommittedChanges,
45
+ hasWarnings,
46
+ isSubagent,
47
+ loadRules,
48
+ notifySummary,
49
+ pushWarning,
50
+ registerToolCall,
51
+ registerToolResult,
52
+ StateTracker,
53
+ type ToolState,
54
+ } from "./shepherd";
55
+ import { registerRulesEditorTool } from "./shepherd/rules-tool";
56
+
57
+ /** 本地 hints 缓冲区(收集 pi.events.emit("ephemeral:hint") 的数据) */
58
+ const _localHints: { text: string; short?: string }[] = [];
59
+
60
+ /** 可变状态:跨 hook 共享 */
61
+ let _aborted = false;
62
+ let _wasDirty = false;
63
+ const _agentEndFired = new Set<string>();
64
+ const _toolState: ToolState = {
65
+ hasEdits: false,
66
+ tracker: new StateTracker(),
67
+ cachedTools: null,
68
+ };
69
+
70
+ export default function shepherdExtension(pi: ExtensionAPI) {
71
+ // ── 读取配置(三层合并:defaults → 全局 settings → 项目 settings)──
72
+ const shepherdConfig = getEffectiveConfig<{
73
+ projectRulesPattern: string;
74
+ maxWarnings: number;
75
+ }>(
76
+ "shepherd",
77
+ {
78
+ projectRulesPattern: "shepherd-rules-",
79
+ maxWarnings: 5,
80
+ },
81
+ process.cwd(),
82
+ );
83
+
84
+ // ── 监听跨扩展 hints(通过 pi.events 绕过 jiti 多实例) ──
85
+ pi.events.on("ephemeral:hint", (data) => {
86
+ const { text, short } = data as { text: string; short?: string };
87
+ _localHints.push({ text, short });
88
+ });
89
+
90
+ // ── before_provider_request:注入临时提示 ──────────────────
91
+ pi.on("before_provider_request", async (event, ctx) => {
92
+ // shepherd 规则 hints
93
+ const shepherdText = drainHints();
94
+ if (shepherdText) {
95
+ _localHints.unshift({ text: shepherdText });
96
+ }
97
+
98
+ // 通知摘要:short 优先,fallback 到 notifySummary 截断
99
+ const shortParts = _localHints
100
+ .map((h) => h.short)
101
+ .filter(Boolean) as string[];
102
+ const longParts = _localHints
103
+ .map((h) => (h.short ? null : h.text))
104
+ .filter(Boolean) as string[];
105
+ const notifyText = [...shortParts, ...longParts].join("\n\n");
106
+
107
+ const allHints = _localHints
108
+ .splice(0)
109
+ .map((h) => h.text)
110
+ .join("\n\n");
111
+ let payload = event.payload as ProviderPayload;
112
+
113
+ if (allHints) {
114
+ const text = allHints;
115
+ payload = { ...payload };
116
+ payload.messages = [...(payload.messages ?? [])];
117
+ payload.messages.push({
118
+ role: "user",
119
+ content: [{ type: "text", text }],
120
+ });
121
+ ctx.ui.notify?.(notifySummary(notifyText), "warning");
122
+ }
123
+
124
+ return payload;
125
+ });
126
+
127
+ // ── session_start ──────────────────────────────────────────
128
+ pi.on("session_start", async (_event, ctx) => {
129
+ checkWorktrees(ctx.ui);
130
+ });
131
+
132
+ // ── agent_start ────────────────────────────────────────────
133
+ pi.on("agent_start", async (_event, ctx) => {
134
+ _aborted = ctx.signal?.aborted ?? false;
135
+ _toolState.hasEdits = false;
136
+ _toolState.cachedTools = null;
137
+ _agentEndFired.clear();
138
+ if (ctx.signal && !ctx.signal.aborted) {
139
+ ctx.signal.addEventListener("abort", () => {
140
+ _aborted = true;
141
+ });
142
+ }
143
+ _wasDirty = hasGitUncommittedChanges();
144
+ });
145
+
146
+ pi.on("input", async (_event) => {
147
+ /* 占位:防止 shepherd steer 循环 */
148
+ });
149
+
150
+ // ── agent_end ──────────────────────────────────────────────
151
+ pi.on("agent_end", async (event, _ctx) => {
152
+ if (isSubagent() || _aborted) return;
153
+ const rules = loadRules(RULES_DIR, {
154
+ projectRulesPattern: shepherdConfig.projectRulesPattern,
155
+ }).filter((r) => r.hook === "agent_end");
156
+ if (rules.length === 0) return;
157
+
158
+ const lastAssistant = [...event.messages]
159
+ .reverse()
160
+ .find((m: PayloadMessage) => m.role === "assistant");
161
+ const stopReason: string | undefined = (lastAssistant as PayloadMessage | undefined)?.stopReason as string | undefined;
162
+
163
+ for (const rule of rules) {
164
+ const allowedReasons = rule.stopReason ?? ["stop"];
165
+ if (!allowedReasons.includes(stopReason ?? "")) continue;
166
+ if (_agentEndFired.has(rule.comment)) continue;
167
+
168
+ let shouldNotify = false;
169
+ if (rule.check === "git_uncommitted") {
170
+ const isDirty = hasGitUncommittedChanges();
171
+ shouldNotify = isDirty && _toolState.hasEdits;
172
+ _wasDirty = isDirty;
173
+ } else if (rule.check === "has_edits") {
174
+ // hasEdits:本轮是否调用过 edit/write,用于提醒记忆更新和总结
175
+ shouldNotify = _toolState.hasEdits;
176
+ } else if (rule.check === "always" || !rule.check) {
177
+ shouldNotify = true;
178
+ }
179
+
180
+ if (shouldNotify && rule.action === "notify") {
181
+ _agentEndFired.add(rule.comment);
182
+ pushWarning(rule.reason, rule.comment);
183
+ }
184
+ }
185
+
186
+ // 如有缓冲提示,用极简消息触发新 turn(before_provider_request 会注入实际内容)
187
+ if (hasWarnings()) {
188
+ setTimeout(() => {
189
+ try {
190
+ pi.sendMessage(
191
+ { customType: "shepherd-agent-end", display: false, content: "" },
192
+ { triggerTurn: true },
193
+ );
194
+ } catch {
195
+ /* session 已替换 */
196
+ }
197
+ }, 0);
198
+ }
199
+ });
200
+
201
+ // ── session_shutdown ───────────────────────────────────────
202
+ pi.on("session_shutdown", async (_event, ctx) => {
203
+ const rules = loadRules(RULES_DIR, {
204
+ projectRulesPattern: shepherdConfig.projectRulesPattern,
205
+ }).filter((r) => r.hook === "session_shutdown");
206
+ if (rules.length === 0) return;
207
+ for (const rule of rules) {
208
+ let shouldNotify = false;
209
+ if (rule.check === "git_uncommitted") {
210
+ shouldNotify = hasGitUncommittedChanges();
211
+ } else if (rule.check === "always" || !rule.check) {
212
+ shouldNotify = true;
213
+ }
214
+ if (shouldNotify && rule.action === "notify") {
215
+ ctx.ui.notify?.(`⚠️ shepherd: ${rule.reason}`, "warning");
216
+ }
217
+ }
218
+ });
219
+
220
+ // ── tool_call + tool_result(提取到 tool-hooks.ts)────────
221
+ const _rulesOpts = {
222
+ projectRulesPattern: shepherdConfig.projectRulesPattern,
223
+ };
224
+ registerToolCall(pi, _toolState, RULES_DIR, _rulesOpts);
225
+ registerToolResult(pi, _toolState, RULES_DIR, _rulesOpts);
226
+
227
+ // ── shepherd_rules 工具:规则文件安全编辑 ───────────────────
228
+ registerRulesEditorTool(pi, join(RULES_DIR, "rules.json"));
229
+ }
@@ -0,0 +1,182 @@
1
+ [中文文档](README.md) | English
2
+
3
+ # pi-shared-utils
4
+
5
+ Shared utility library for the [pi](https://github.com/earendil-works/pi-coding-agent) extension ecosystem — memory file parsing, path constants, settings management, tool output truncation, and more. Used by 7+ pi extensions.
6
+
7
+ ## Why You Need It
8
+
9
+ If you're building a pi extension, you'll inevitably need the same building blocks: reading settings, parsing memory files, truncating tool output, finding agent directories. pi-shared-utils provides these as a single dependency so every extension doesn't reinvent the wheel.
10
+
11
+ **Used by**: pi-memory, pi-context, pi-shepherd, pi-roadmap, pi-session-analyzer, pi-workflow, and more.
12
+
13
+ ## How It Works
14
+
15
+ ```
16
+ pi-shared-utils provides 6 independent modules:
17
+
18
+ ┌─────────────────────────────────────────────────┐
19
+ │ memory-parser ── parse topic--kw1,kw2.md file names
20
+ │ paths ── standard pi agent path constants
21
+ │ settings ── read/write extension config sections in settings.json
22
+ │ tool-output ── truncate tool output (prevent context overflow)
23
+ │ agents ── discover sub-agent definition files
24
+ │ ephemeral ── session-scoped hint/label stack
25
+ └─────────────────────────────────────────────────┘
26
+ ```
27
+
28
+ Each module is independently importable — use only what you need.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pi install git:github.com/catlain/pi-atelier
34
+ ```
35
+
36
+ > This is a workspace package inside the pi-atelier monorepo and typically doesn't need to be installed standalone. Other independent extensions include it automatically via `bundledDependencies`.
37
+
38
+ ## Exported Modules
39
+
40
+ ### Memory File Parsing (`memory-parser`)
41
+
42
+ Parses `topic--kw1,kw2,kw3.md`-format memory file names and scans directories to generate an index.
43
+
44
+ ```ts
45
+ import { parseFileName, buildFileName, scanMemoryDir } from "@pi-atelier/shared-utils";
46
+
47
+ // Parse file name → { topic, keywords }
48
+ const { topic, keywords } = parseFileName("coding_standards--编码,git,lint.md");
49
+ // topic = "coding_standards", keywords = ["编码", "git", "lint"]
50
+
51
+ // Build file name from parts
52
+ const name = buildFileName("coding_standards", ["编码", "git", "lint"]);
53
+ // "coding_standards--编码,git,lint.md"
54
+
55
+ // Scan directory, returns MemoryEntry[]
56
+ const entries = await scanMemoryDir("/path/to/memory");
57
+ ```
58
+
59
+ ### Path Constants (`paths`)
60
+
61
+ Standard pi agent paths, so you never hardcode them.
62
+
63
+ | Constant | Path | Description |
64
+ |------|------|------|
65
+ | `AGENT_DIR` | `~/.pi/agent/` | Agent root directory |
66
+ | `SETTINGS_PATH` | `~/.pi/agent/settings.json` | Global settings |
67
+ | `MODELS_CONFIG_PATH` | `~/.pi/agent/models.json` | Model configuration |
68
+ | `MCP_CONFIG_PATH` | `~/.pi/agent/mcp.json` | MCP server configuration |
69
+ | `MCP_CACHE_PATH` | `~/.pi/agent/mcp-cache/` | MCP tool cache |
70
+ | `AGENTS_DIR` | `~/.pi/agent/agents/` | Sub-agent definitions |
71
+ | `GLOBAL_RULES_PATH` | `~/.pi/agent/rules.md` | Global rules |
72
+ | `MEMORY_DIR` | `~/.pi/agent/memory/` | Global memory |
73
+ | `MEMORY_MD_PATH` | `MEMORY.md` | Memory index file name |
74
+
75
+ ### Settings Management (`settings`)
76
+
77
+ Read and write extension-specific config sections in `settings.json`.
78
+
79
+ ```ts
80
+ import { getSettingsSection, patchSettingsSection, getSettingsValue, setSettingsValue } from "@pi-atelier/shared-utils";
81
+
82
+ // Read extension config section
83
+ const config = await getSettingsSection("my-extension");
84
+
85
+ // Update config incrementally
86
+ await patchSettingsSection("my-extension", { enabled: true });
87
+
88
+ // Read/write a single value
89
+ const val = await getSettingsValue("my-extension", "key", "default");
90
+ await setSettingsValue("my-extension", "key", "new-value");
91
+ ```
92
+
93
+ ### Tool Output Truncation (`tool-output`)
94
+
95
+ Prevent large tool results from overflowing the LLM context.
96
+
97
+ ```ts
98
+ import { truncateToolOutput, truncatedResult, TOOL_OUTPUT_MAX_LINES } from "@pi-atelier/shared-utils";
99
+
100
+ // Truncate overly long output
101
+ const result = truncateToolOutput(longText, { maxLines: 200 });
102
+ // { text: "...", truncated: true, originalLines: 1500, keptLines: 200 }
103
+
104
+ // Shortcut: returns pi tool result format
105
+ return truncatedResult(text); // auto-truncates + returns { content: [{ type: "text", text }] }
106
+ ```
107
+
108
+ ### Sub-Agent Discovery (`agents`)
109
+
110
+ Scan the `~/.pi/agent/agents/` directory for sub-agent definition files.
111
+
112
+ ```ts
113
+ import { discoverAgents, getAgentDescription, formatAgentsList } from "@pi-atelier/shared-utils";
114
+
115
+ // Discover all available sub-agents
116
+ const agents = await discoverAgents();
117
+ // [{ name: "pv-executor", description: "...", filePath: "..." }, ...]
118
+
119
+ // Get description for a single agent
120
+ const desc = await getAgentDescription("pv-executor");
121
+
122
+ // Format as a readable list
123
+ const list = formatAgentsList(agents);
124
+ ```
125
+
126
+ ### Session-Scoped Data (`ephemeral`)
127
+
128
+ A hint/label stack for the current session that vanishes when the session ends. Useful for lightweight state passing across tool calls.
129
+
130
+ ```ts
131
+ import { pushHint, hasHints, peekHints, drainHints, peekLabels } from "@pi-atelier/shared-utils";
132
+
133
+ pushHint({ key: "recent-files", values: ["file1.ts", "file2.ts"] });
134
+ const has = hasHints("recent-files");
135
+ const hints = peekHints("recent-files"); // peek without removing
136
+ const all = drainHints(); // retrieve and clear
137
+ ```
138
+
139
+ ## Best Practices
140
+
141
+ ### ✅ Recommended
142
+ - Import only the modules you need to keep bundle size small
143
+ - Use `truncatedResult()` for all tool outputs — prevents context overflow
144
+ - Use `paths` constants instead of hardcoding `~/.pi/agent/...`
145
+ - Use `settings` module for any persistent configuration
146
+
147
+ ### ❌ Not Recommended
148
+ - Don't hardcode pi paths — they may change between versions
149
+ - Don't return raw tool output without truncation
150
+ - Don't use `ephemeral` for persistent data — it's session-scoped only
151
+
152
+ ## Limitations
153
+
154
+ | Limitation | Detail |
155
+ |------------|--------|
156
+ | Memory file format only | Only supports `topic--kw1,kw2.md` naming convention |
157
+ | No validation | Settings reads don't validate schema — caller must handle |
158
+ | Ephemeral is in-memory | Lost on process restart, not persisted to disk |
159
+ | Token estimation | `tool-output` truncates by lines, not by token count |
160
+
161
+ ## Architecture
162
+
163
+ ```
164
+ pi-shared-utils/
165
+ ├── src/
166
+ │ ├── index.ts # Re-exports all modules
167
+ │ ├── memory-parser.ts # Memory file name parsing + directory scanning
168
+ │ ├── paths.ts # Path constants (AGENT_DIR, SETTINGS_PATH, ...)
169
+ │ ├── settings.ts # settings.json section read/write
170
+ │ ├── tool-output.ts # Output truncation + truncatedResult helper
171
+ │ ├── agents.ts # Sub-agent discovery from ~/.pi/agent/agents/
172
+ │ ├── ephemeral.ts # Session-scoped hint/label stack
173
+ │ └── __tests__/ # Unit tests
174
+ ├── package.json
175
+ └── tsconfig.json
176
+ ```
177
+
178
+ **Dependencies**: Zero runtime dependencies (pure Node.js).
179
+
180
+ ## License
181
+
182
+ MIT