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.
- package/README.en.md +136 -0
- package/README.md +136 -0
- package/index.ts +229 -0
- package/node_modules/@pi-atelier/shared-utils/README.en.md +182 -0
- package/node_modules/@pi-atelier/shared-utils/README.md +182 -0
- package/node_modules/@pi-atelier/shared-utils/package.json +51 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +120 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +100 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +152 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +187 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +170 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +126 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +138 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +257 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +189 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +204 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +269 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +267 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +520 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +116 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +119 -0
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +145 -0
- package/node_modules/@pi-atelier/shared-utils/src/agents.ts +39 -0
- package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +42 -0
- package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +62 -0
- package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +100 -0
- package/node_modules/@pi-atelier/shared-utils/src/index.ts +71 -0
- package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +96 -0
- package/node_modules/@pi-atelier/shared-utils/src/paths.ts +23 -0
- package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +241 -0
- package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +191 -0
- package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +73 -0
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +104 -0
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +75 -0
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +172 -0
- package/node_modules/@pi-atelier/shared-utils/src/settings.ts +104 -0
- package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +149 -0
- package/node_modules/@pi-atelier/shared-utils/tsconfig.json +9 -0
- package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +24 -0
- package/package.json +49 -0
- package/rules.json +516 -0
- package/shepherd/ephemeral-shared.ts +14 -0
- package/shepherd/ephemeral.ts +52 -0
- package/shepherd/index.ts +39 -0
- package/shepherd/line-count.ts +86 -0
- package/shepherd/rules-editor.ts +135 -0
- package/shepherd/rules-tool.ts +99 -0
- package/shepherd/rules-validate.ts +44 -0
- package/shepherd/rules.ts +283 -0
- package/shepherd/state-tracker.ts +119 -0
- package/shepherd/tool-event-types.ts +31 -0
- package/shepherd/tool-hooks.ts +176 -0
- package/shepherd/worktree-check.ts +130 -0
- package/tsconfig.json +14 -0
- 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
|