pi-shepherd 0.1.1 → 0.1.2
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 +2 -0
- package/README.md +2 -0
- package/index.ts +1 -1
- package/package.json +1 -4
- package/rules.json +22 -206
- package/shepherd/index.ts +1 -0
- package/shepherd/rules-editor.ts +80 -0
- package/shepherd/rules-tool-helpers.ts +120 -0
- package/shepherd/rules-tool-list.ts +126 -0
- package/shepherd/rules-tool.ts +74 -31
- package/shepherd/rules.ts +15 -3
- package/shepherd/tool-hooks.ts +3 -2
- package/node_modules/@pi-atelier/shared-utils/README.en.md +0 -182
- package/node_modules/@pi-atelier/shared-utils/README.md +0 -182
- package/node_modules/@pi-atelier/shared-utils/package.json +0 -51
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +0 -120
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +0 -100
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +0 -152
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +0 -187
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +0 -170
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +0 -126
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +0 -138
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +0 -257
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +0 -189
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +0 -204
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +0 -269
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +0 -267
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +0 -520
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +0 -116
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +0 -119
- package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +0 -145
- package/node_modules/@pi-atelier/shared-utils/src/agents.ts +0 -39
- package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +0 -42
- package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +0 -62
- package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +0 -100
- package/node_modules/@pi-atelier/shared-utils/src/index.ts +0 -71
- package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +0 -96
- package/node_modules/@pi-atelier/shared-utils/src/paths.ts +0 -23
- package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +0 -241
- package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +0 -191
- package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +0 -73
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +0 -104
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +0 -75
- package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +0 -172
- package/node_modules/@pi-atelier/shared-utils/src/settings.ts +0 -104
- package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +0 -149
- package/node_modules/@pi-atelier/shared-utils/tsconfig.json +0 -9
- package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +0 -24
package/README.en.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# pi-shepherd
|
|
4
4
|
|
|
5
|
+
[Source Code](https://github.com/catlain/pi-shepherd) | [npm](https://www.npmjs.com/package/pi-shepherd)
|
|
6
|
+
|
|
5
7
|
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
8
|
|
|
7
9
|
## What It Does
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# pi-shepherd
|
|
6
6
|
|
|
7
|
+
[源码仓库](https://github.com/catlain/pi-shepherd) | [npm](https://www.npmjs.com/package/pi-shepherd)
|
|
8
|
+
|
|
7
9
|
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.
|
|
8
10
|
|
|
9
11
|
## What It Does
|
package/index.ts
CHANGED
|
@@ -225,5 +225,5 @@ export default function shepherdExtension(pi: ExtensionAPI) {
|
|
|
225
225
|
registerToolResult(pi, _toolState, RULES_DIR, _rulesOpts);
|
|
226
226
|
|
|
227
227
|
// ── shepherd_rules 工具:规则文件安全编辑 ───────────────────
|
|
228
|
-
registerRulesEditorTool(pi,
|
|
228
|
+
registerRulesEditorTool(pi, RULES_DIR, process.cwd());
|
|
229
229
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-shepherd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Line count guard and behavior rules extension for pi-coding-agent",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -40,9 +40,6 @@
|
|
|
40
40
|
"vitest.config.ts",
|
|
41
41
|
"package.json"
|
|
42
42
|
],
|
|
43
|
-
"bundleDependencies": [
|
|
44
|
-
"@pi-atelier/shared-utils"
|
|
45
|
-
],
|
|
46
43
|
"keywords": [
|
|
47
44
|
"pi-package"
|
|
48
45
|
],
|
package/rules.json
CHANGED
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"enabled": true
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
"comment": "[Rust]
|
|
41
|
+
"comment": "[Rust] 编辑/写入后必须格式检查 + 跑测试",
|
|
42
42
|
"hook": "tool_result",
|
|
43
|
-
"tool": "edit",
|
|
43
|
+
"tool": "edit|write",
|
|
44
44
|
"action": "notify",
|
|
45
45
|
"conditions": [
|
|
46
46
|
{
|
|
@@ -53,24 +53,9 @@
|
|
|
53
53
|
"enabled": true
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
"comment": "[
|
|
57
|
-
"hook": "tool_result",
|
|
58
|
-
"tool": "write",
|
|
59
|
-
"action": "notify",
|
|
60
|
-
"conditions": [
|
|
61
|
-
{
|
|
62
|
-
"field": "path",
|
|
63
|
-
"pattern": "\\.rs$",
|
|
64
|
-
"flags": ""
|
|
65
|
-
}
|
|
66
|
-
],
|
|
67
|
-
"reason": "写入了 Rust 文件,必须:1) 运行 cargo clippy 做格式和 lint 检查;2) 跑覆盖该代码的单元测试(如无测试则先补充),修复所有测试问题确保通过。",
|
|
68
|
-
"enabled": true
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"comment": "[TypeScript] 编辑后必须跑测试",
|
|
56
|
+
"comment": "[TypeScript] 编辑/写入后必须跑测试",
|
|
72
57
|
"hook": "tool_result",
|
|
73
|
-
"tool": "edit",
|
|
58
|
+
"tool": "edit|write",
|
|
74
59
|
"action": "notify",
|
|
75
60
|
"conditions": [
|
|
76
61
|
{
|
|
@@ -82,21 +67,6 @@
|
|
|
82
67
|
"reason": "编辑了 TypeScript 文件,必须跑覆盖该代码的单元测试(如无测试则先补充),修复所有测试问题确保通过。",
|
|
83
68
|
"enabled": true
|
|
84
69
|
},
|
|
85
|
-
{
|
|
86
|
-
"comment": "[TypeScript][write] 写入后必须跑测试",
|
|
87
|
-
"hook": "tool_result",
|
|
88
|
-
"tool": "write",
|
|
89
|
-
"action": "notify",
|
|
90
|
-
"conditions": [
|
|
91
|
-
{
|
|
92
|
-
"field": "path",
|
|
93
|
-
"pattern": "\\.ts$",
|
|
94
|
-
"flags": ""
|
|
95
|
-
}
|
|
96
|
-
],
|
|
97
|
-
"reason": "写入了 TypeScript 文件,必须跑覆盖该代码的单元测试(如无测试则先补充),修复所有测试问题确保通过。",
|
|
98
|
-
"enabled": true
|
|
99
|
-
},
|
|
100
70
|
{
|
|
101
71
|
"comment": "收尾规则 — agent_end 时提醒 commit + 记忆 + 总结,commit 必须带 session ID",
|
|
102
72
|
"hook": "agent_end",
|
|
@@ -142,9 +112,9 @@
|
|
|
142
112
|
"subagent": false
|
|
143
113
|
},
|
|
144
114
|
{
|
|
145
|
-
"comment": "[dev]
|
|
115
|
+
"comment": "[dev] 编辑/写入扩展代码时提醒参考规范",
|
|
146
116
|
"hook": "tool_result",
|
|
147
|
-
"tool": "edit",
|
|
117
|
+
"tool": "edit|write",
|
|
148
118
|
"conditions": [
|
|
149
119
|
{
|
|
150
120
|
"field": "path",
|
|
@@ -152,15 +122,15 @@
|
|
|
152
122
|
}
|
|
153
123
|
],
|
|
154
124
|
"action": "steer",
|
|
155
|
-
"reason": "📝
|
|
125
|
+
"reason": "📝 编辑/写入了扩展代码。如果尚未阅读扩展开发规范,请先 read 根目录或项目目录下 extensions/README.md;如果已阅读且有新知识,更新到该文件。已阅读且无新知识则无视。",
|
|
156
126
|
"requireSuccess": true,
|
|
157
127
|
"subagent": false,
|
|
158
128
|
"enabled": true
|
|
159
129
|
},
|
|
160
130
|
{
|
|
161
|
-
"comment": "[dev]
|
|
131
|
+
"comment": "[dev] 编辑/写入扩展代码后提醒跑 typecheck",
|
|
162
132
|
"hook": "tool_result",
|
|
163
|
-
"tool": "write",
|
|
133
|
+
"tool": "edit|write",
|
|
164
134
|
"conditions": [
|
|
165
135
|
{
|
|
166
136
|
"field": "path",
|
|
@@ -168,39 +138,7 @@
|
|
|
168
138
|
}
|
|
169
139
|
],
|
|
170
140
|
"action": "steer",
|
|
171
|
-
"reason": "📝
|
|
172
|
-
"requireSuccess": true,
|
|
173
|
-
"subagent": false,
|
|
174
|
-
"enabled": true
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
"comment": "[dev] 编辑扩展代码后提醒跑 typecheck",
|
|
178
|
-
"hook": "tool_result",
|
|
179
|
-
"tool": "edit",
|
|
180
|
-
"conditions": [
|
|
181
|
-
{
|
|
182
|
-
"field": "path",
|
|
183
|
-
"pattern": "extensions/.*\\.ts$"
|
|
184
|
-
}
|
|
185
|
-
],
|
|
186
|
-
"action": "steer",
|
|
187
|
-
"reason": "📝 编辑了扩展 TypeScript 代码。请运行 `cd extensions && bash check.sh <扩展名> --typecheck` 确认无类型错误。如果已跑过则无视。",
|
|
188
|
-
"requireSuccess": true,
|
|
189
|
-
"subagent": false,
|
|
190
|
-
"enabled": true
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
"comment": "[dev] 写入扩展代码后提醒跑 typecheck",
|
|
194
|
-
"hook": "tool_result",
|
|
195
|
-
"tool": "write",
|
|
196
|
-
"conditions": [
|
|
197
|
-
{
|
|
198
|
-
"field": "path",
|
|
199
|
-
"pattern": "extensions/.*\\.ts$"
|
|
200
|
-
}
|
|
201
|
-
],
|
|
202
|
-
"action": "steer",
|
|
203
|
-
"reason": "📝 写入了扩展 TypeScript 代码。请运行 `cd extensions && bash check.sh <扩展名> --typecheck` 确认无类型错误。如果已跑过则无视。",
|
|
141
|
+
"reason": "📝 编辑/写入了扩展 TypeScript 代码。请运行 `cd extensions && bash check.sh <扩展名> --typecheck` 确认无类型错误。如果已跑过则无视。",
|
|
204
142
|
"requireSuccess": true,
|
|
205
143
|
"subagent": false,
|
|
206
144
|
"enabled": true
|
|
@@ -208,25 +146,7 @@
|
|
|
208
146
|
{
|
|
209
147
|
"comment": "[TS] 禁止空格缩进 — TS 文件必须用 Tab",
|
|
210
148
|
"hook": "tool_call",
|
|
211
|
-
"tool": "edit",
|
|
212
|
-
"action": "notify",
|
|
213
|
-
"conditions": [
|
|
214
|
-
{
|
|
215
|
-
"field": "path",
|
|
216
|
-
"pattern": "\\.ts$"
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
"field": "text",
|
|
220
|
-
"pattern": "\\n [\\S ]"
|
|
221
|
-
}
|
|
222
|
-
],
|
|
223
|
-
"reason": "❌ TS 文件要求 Tab 缩进,不是空格。请用 Tab 缩进重写代码。",
|
|
224
|
-
"enabled": true
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
"comment": "[TS][write] 禁止空格缩进 — TS 文件必须用 Tab",
|
|
228
|
-
"hook": "tool_call",
|
|
229
|
-
"tool": "write",
|
|
149
|
+
"tool": "edit|write",
|
|
230
150
|
"action": "notify",
|
|
231
151
|
"conditions": [
|
|
232
152
|
{
|
|
@@ -244,25 +164,7 @@
|
|
|
244
164
|
{
|
|
245
165
|
"comment": "[Python] 禁止 Tab 缩进 — Python 必须用 4 空格",
|
|
246
166
|
"hook": "tool_call",
|
|
247
|
-
"tool": "edit",
|
|
248
|
-
"action": "notify",
|
|
249
|
-
"conditions": [
|
|
250
|
-
{
|
|
251
|
-
"field": "path",
|
|
252
|
-
"pattern": "\\.py$"
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
"field": "text",
|
|
256
|
-
"pattern": "\\n\\t"
|
|
257
|
-
}
|
|
258
|
-
],
|
|
259
|
-
"reason": "❌ Python 文件禁止 Tab 缩进,必须用 4 空格(PEP 8)。",
|
|
260
|
-
"enabled": true
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
"comment": "[Python][write] 禁止 Tab 缩进 — Python 必须用 4 空格",
|
|
264
|
-
"hook": "tool_call",
|
|
265
|
-
"tool": "write",
|
|
167
|
+
"tool": "edit|write",
|
|
266
168
|
"action": "notify",
|
|
267
169
|
"conditions": [
|
|
268
170
|
{
|
|
@@ -280,25 +182,7 @@
|
|
|
280
182
|
{
|
|
281
183
|
"comment": "[Rust] 禁止 Tab 缩进 — Rust 必须用 4 空格",
|
|
282
184
|
"hook": "tool_call",
|
|
283
|
-
"tool": "edit",
|
|
284
|
-
"action": "notify",
|
|
285
|
-
"conditions": [
|
|
286
|
-
{
|
|
287
|
-
"field": "path",
|
|
288
|
-
"pattern": "\\.rs$"
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
"field": "text",
|
|
292
|
-
"pattern": "\\n\\t"
|
|
293
|
-
}
|
|
294
|
-
],
|
|
295
|
-
"reason": "❌ Rust 文件禁止 Tab 缩进,必须用 4 空格(rustfmt)。",
|
|
296
|
-
"enabled": true
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
"comment": "[Rust][write] 禁止 Tab 缩进 — Rust 必须用 4 空格",
|
|
300
|
-
"hook": "tool_call",
|
|
301
|
-
"tool": "write",
|
|
185
|
+
"tool": "edit|write",
|
|
302
186
|
"action": "notify",
|
|
303
187
|
"conditions": [
|
|
304
188
|
{
|
|
@@ -351,9 +235,9 @@
|
|
|
351
235
|
"enabled": true
|
|
352
236
|
},
|
|
353
237
|
{
|
|
354
|
-
"comment": "[deps]
|
|
238
|
+
"comment": "[deps] 编辑/写入 package.json 时提醒依赖合规检查",
|
|
355
239
|
"hook": "tool_result",
|
|
356
|
-
"tool": "edit",
|
|
240
|
+
"tool": "edit|write",
|
|
357
241
|
"action": "notify",
|
|
358
242
|
"conditions": [
|
|
359
243
|
{
|
|
@@ -364,20 +248,6 @@
|
|
|
364
248
|
"reason": "📦 编辑了 package.json 依赖字段。请对照 DEPENDENCY-GUIDE.md 检查合规性:① pi SDK 必须放 peerDependencies + optionalDependencies,禁止放 dependencies/devDependencies;② shared-utils 运行时引用放 dependencies(开发放 devDependencies);③ 检查 files 白名单是否存在;④ 添加新依赖后运行 scan-deps.sh 验证无嵌套/版本冲突。",
|
|
365
249
|
"enabled": true
|
|
366
250
|
},
|
|
367
|
-
{
|
|
368
|
-
"comment": "[deps][write] 写入 package.json 时提醒依赖合规检查",
|
|
369
|
-
"hook": "tool_result",
|
|
370
|
-
"tool": "write",
|
|
371
|
-
"action": "notify",
|
|
372
|
-
"conditions": [
|
|
373
|
-
{
|
|
374
|
-
"field": "path",
|
|
375
|
-
"pattern": "package\\.json$"
|
|
376
|
-
}
|
|
377
|
-
],
|
|
378
|
-
"reason": "📦 写入了 package.json 依赖字段。请对照 DEPENDENCY-GUIDE.md 检查合规性:① pi SDK 必须放 peerDependencies + optionalDependencies,禁止放 dependencies/devDependencies;② shared-utils 运行时引用放 dependencies(开发放 devDependencies);③ 检查 files 白名单是否存在;④ 添加新依赖后运行 scan-deps.sh 验证无嵌套/版本冲突。",
|
|
379
|
-
"enabled": true
|
|
380
|
-
},
|
|
381
251
|
{
|
|
382
252
|
"comment": "[通用] 脚本执行失败时提醒先 --help",
|
|
383
253
|
"hook": "tool_result",
|
|
@@ -394,9 +264,9 @@
|
|
|
394
264
|
"enabled": true
|
|
395
265
|
},
|
|
396
266
|
{
|
|
397
|
-
"comment": "[roadmap]
|
|
267
|
+
"comment": "[roadmap] 禁止直接编辑/写入 roadmap JSON — 必须用 roadmap 工具",
|
|
398
268
|
"hook": "tool_call",
|
|
399
|
-
"tool": "edit",
|
|
269
|
+
"tool": "edit|write",
|
|
400
270
|
"action": "block",
|
|
401
271
|
"conditions": [
|
|
402
272
|
{
|
|
@@ -407,20 +277,6 @@
|
|
|
407
277
|
"reason": "⛔ 禁止直接编辑 roadmap JSON!请用 roadmap 工具操作(自动处理时间戳/ID分配/状态级联/归档/doing同步):\n查看:roadmap_list(列表)、roadmap_show(详情)、roadmap_next(待办)\n创建:roadmap_plan(完整JSON创建/更新)、roadmap_create + roadmap_add_epic/story/task(逐步构建)\n修改:roadmap_update(roadmapId, item_id, {status/title/description/priority}) — 更新单个项\n完成:roadmap_done(roadmapId, taskId) — 标记完成并级联更新\n归档:roadmap_archive(roadmapId) — 归档已完成项\n直接改 JSON 会绕过这些机制导致数据不一致。",
|
|
408
278
|
"enabled": true
|
|
409
279
|
},
|
|
410
|
-
{
|
|
411
|
-
"comment": "[roadmap] 禁止直接写入 roadmap JSON — 必须用 roadmap 工具",
|
|
412
|
-
"hook": "tool_call",
|
|
413
|
-
"tool": "write",
|
|
414
|
-
"action": "block",
|
|
415
|
-
"conditions": [
|
|
416
|
-
{
|
|
417
|
-
"field": "path",
|
|
418
|
-
"pattern": "\\.roadmap\\.json$|\\broadmap/roadmap\\.json$"
|
|
419
|
-
}
|
|
420
|
-
],
|
|
421
|
-
"reason": "⛔ 禁止直接写入 roadmap JSON!请用 roadmap 工具操作(自动处理时间戳/ID分配/状态级联/归档/doing同步):\n查看:roadmap_list(列表)、roadmap_show(详情)、roadmap_next(待办)\n创建:roadmap_plan(完整JSON创建/更新)、roadmap_create + roadmap_add_epic/story/task(逐步构建)\n修改:roadmap_update(roadmapId, item_id, {status/title/description/priority}) — 更新单个项\n完成:roadmap_done(roadmapId, taskId) — 标记完成并级联更新\n归档:roadmap_archive(roadmapId) — 归档已完成项\n直接改 JSON 会绕过这些机制导致数据不一致。",
|
|
422
|
-
"enabled": true
|
|
423
|
-
},
|
|
424
280
|
{
|
|
425
281
|
"comment": "[roadmap] 禁止通过 bash/python 脚本直接读写 roadmap JSON — 必须用 roadmap 工具",
|
|
426
282
|
"hook": "tool_call",
|
|
@@ -436,29 +292,9 @@
|
|
|
436
292
|
"enabled": true
|
|
437
293
|
},
|
|
438
294
|
{
|
|
439
|
-
"comment": "[
|
|
440
|
-
"reason": "跟 Python/Rust/TS 规则一致——编辑代码后提醒补充单元测试。触发条件:edit 工具的 file 参数匹配 .gd 扩展名。提醒内容:是否有对应的 GUT 测试?如果没有,建议补写。如果有,提醒跑 run_unit_tests.sh 验证。",
|
|
441
|
-
"trigger": {
|
|
442
|
-
"tool": "edit",
|
|
443
|
-
"event": "tool_result",
|
|
444
|
-
"match": "\\.gd$"
|
|
445
|
-
},
|
|
446
|
-
"action": "notify"
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
"action": "notify",
|
|
450
|
-
"comment": "[GDScript][write] 写入 .gd 文件后提醒写/跑 GUT 测试",
|
|
451
|
-
"reason": "跟 edit 规则配对。新建 .gd 文件时提醒创建对应的测试文件。",
|
|
452
|
-
"trigger": {
|
|
453
|
-
"event": "tool_result",
|
|
454
|
-
"match": "\\.gd$",
|
|
455
|
-
"tool": "write"
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
{
|
|
459
|
-
"comment": "[settings] 禁止 edit settings.json — 必须用 patchSettingsSectionWithBackup 或 settings_rollback",
|
|
295
|
+
"comment": "[settings] 禁止 edit/write settings.json — 必须用 patchSettingsSectionWithBackup",
|
|
460
296
|
"reason": "直接编辑 settings.json 曾导致配置丢失(119 行变 3 行)。必须使用 patchSettingsSectionWithBackup 或 settings_rollback。",
|
|
461
|
-
"tool": "edit",
|
|
297
|
+
"tool": "edit|write",
|
|
462
298
|
"hook": "tool_call",
|
|
463
299
|
"action": "block",
|
|
464
300
|
"message": "⛔ 禁止直接 edit settings.json!使用 patchSettingsSectionWithBackup() 安全修改,或 settings_rollback 回滚。",
|
|
@@ -469,32 +305,19 @@
|
|
|
469
305
|
}
|
|
470
306
|
]
|
|
471
307
|
},
|
|
472
|
-
{
|
|
473
|
-
"action": "block",
|
|
474
|
-
"comment": "[settings] 禁止 write settings.json — 必须用 patchSettingsSectionWithBackup 或 settings_rollback",
|
|
475
|
-
"conditions": [
|
|
476
|
-
{
|
|
477
|
-
"field": "path",
|
|
478
|
-
"pattern": "settings\\.json$|settings\\.json\\.bak"
|
|
479
|
-
}
|
|
480
|
-
],
|
|
481
|
-
"hook": "tool_call",
|
|
482
|
-
"message": "⛔ 禁止直接 write settings.json!使用 patchSettingsSectionWithBackup() 安全修改,或 settings_rollback 回滚。",
|
|
483
|
-
"reason": "直接编辑 settings.json 曾导致配置丢失(119 行变 3 行)。必须使用 patchSettingsSectionWithBackup 或 settings_rollback。",
|
|
484
|
-
"tool": "write"
|
|
485
|
-
},
|
|
486
308
|
{
|
|
487
309
|
"action": "block",
|
|
488
310
|
"comment": "[settings] 禁止 bash 直接修改 settings.json — 必须用 patchSettingsSectionWithBackup 或 settings_rollback",
|
|
489
311
|
"conditions": [
|
|
490
312
|
{
|
|
491
313
|
"field": "command",
|
|
492
|
-
"pattern": "settings\\.json"
|
|
314
|
+
"pattern": "^(?!.*\\b(git|grep|find|ls|cat|head|tail|diff|test|python3.*json\\.load)\\b).*settings\\.json",
|
|
315
|
+
"flags": "s"
|
|
493
316
|
}
|
|
494
317
|
],
|
|
495
318
|
"hook": "tool_call",
|
|
496
319
|
"message": "⛔ 禁止通过 bash 修改 settings.json!使用 patchSettingsSectionWithBackup() 安全修改,或 settings_rollback 回滚。",
|
|
497
|
-
"reason": "
|
|
320
|
+
"reason": "禁止通过 bash 直接写入 settings.json(如 echo > / sed -i / python 写入),但允许 git 操作(status/diff/reset/checkout/restore/add/rm/log)和只读命令(grep/find/ls/cat/head/tail/diff/test/python3 解析读取)",
|
|
498
321
|
"tool": "bash"
|
|
499
322
|
},
|
|
500
323
|
{
|
|
@@ -505,12 +328,5 @@
|
|
|
505
328
|
"condition": "bash command contains 'git commit' and exit code is 0",
|
|
506
329
|
"action": "notify",
|
|
507
330
|
"message": "commit 成功。检查是否有远程仓库(git remote),有的话立即 git push 同步到远程。"
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
"comment": "UI 布局改动提醒",
|
|
511
|
-
"reason": "编辑 GameScene.gd 或 UI 相关代码时,先查 debug_summary().ui_layout 确认位置,改完用 debug_toggle_borders() 验证无重叠",
|
|
512
|
-
"pattern": "GameScene\\.gd",
|
|
513
|
-
"type": "notify",
|
|
514
|
-
"action": "notify"
|
|
515
331
|
}
|
|
516
332
|
]
|
package/shepherd/index.ts
CHANGED
package/shepherd/rules-editor.ts
CHANGED
|
@@ -32,11 +32,24 @@ export interface ListResult {
|
|
|
32
32
|
error?: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface RuleDetail {
|
|
36
|
+
index: number;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ListDetailResult {
|
|
41
|
+
rules: RuleDetail[];
|
|
42
|
+
count: number;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export interface WriteResult {
|
|
36
47
|
success: boolean;
|
|
37
48
|
error?: string;
|
|
38
49
|
index?: number;
|
|
39
50
|
deleted?: { comment: string; [key: string]: unknown };
|
|
51
|
+
overwritten?: boolean;
|
|
52
|
+
warning?: string;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
// ── 文件操作辅助 ──────────────────────────────────────────
|
|
@@ -77,6 +90,48 @@ function safeWrite(filePath: string, rules: Record<string, unknown>[]): WriteRes
|
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
// ── 去重签名 ─────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/** 规范化 conditions 数组:按 field+pattern 排序,取关键属性 */
|
|
96
|
+
function normalizeConditions(
|
|
97
|
+
conditions: Array<Record<string, unknown>>,
|
|
98
|
+
): string {
|
|
99
|
+
const normalized = conditions
|
|
100
|
+
.map((c) => ({ f: c.field || "", p: c.pattern || "" }))
|
|
101
|
+
.sort((a, b) => (a.f + a.p).localeCompare(b.f + b.p));
|
|
102
|
+
return JSON.stringify(normalized);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 生成规则去重签名 key */
|
|
106
|
+
export function dedupKey(rule: Record<string, unknown>): string {
|
|
107
|
+
const tool = (rule.tool as string) || "";
|
|
108
|
+
const hook = (rule.hook as string) || "";
|
|
109
|
+
const action = (rule.action as string) || "";
|
|
110
|
+
let trigger: string;
|
|
111
|
+
if (Array.isArray(rule.conditions) && rule.conditions.length > 0) {
|
|
112
|
+
trigger = normalizeConditions(rule.conditions as Array<Record<string, unknown>>);
|
|
113
|
+
} else if (rule.pattern) {
|
|
114
|
+
trigger = rule.pattern as string;
|
|
115
|
+
} else if (rule.check) {
|
|
116
|
+
trigger = rule.check as string;
|
|
117
|
+
} else {
|
|
118
|
+
trigger = "";
|
|
119
|
+
}
|
|
120
|
+
return `${tool}|${hook}|${trigger}|${action}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 在规则数组中查找签名相同的规则,返回 index 或 null */
|
|
124
|
+
export function findDuplicateBySignature(
|
|
125
|
+
rules: Record<string, unknown>[],
|
|
126
|
+
newRule: Record<string, unknown>,
|
|
127
|
+
): number | null {
|
|
128
|
+
const newKey = dedupKey(newRule);
|
|
129
|
+
for (let i = 0; i < rules.length; i++) {
|
|
130
|
+
if (dedupKey(rules[i]) === newKey) return i;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
80
135
|
// ── 公开 API ─────────────────────────────────────────────
|
|
81
136
|
|
|
82
137
|
export function listRules(filePath: string): ListResult {
|
|
@@ -96,11 +151,36 @@ export function listRules(filePath: string): ListResult {
|
|
|
96
151
|
};
|
|
97
152
|
}
|
|
98
153
|
|
|
154
|
+
export function listRulesDetail(filePath: string): ListDetailResult {
|
|
155
|
+
const { rules, error } = readFile(filePath);
|
|
156
|
+
if (error) return { rules: [], count: 0, error };
|
|
157
|
+
return {
|
|
158
|
+
rules: rules.map((r, i) => ({ index: i, ...r })),
|
|
159
|
+
count: rules.length,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getRuleDetail(filePath: string, index: number): RuleDetail | { error: string } {
|
|
164
|
+
const { rules, error } = readFile(filePath);
|
|
165
|
+
if (error) return { error };
|
|
166
|
+
if (index < 0 || index >= rules.length) return { error: `编号越界: ${index}(共 ${rules.length} 条)` };
|
|
167
|
+
return { index, ...rules[index] };
|
|
168
|
+
}
|
|
169
|
+
|
|
99
170
|
export function addRule(filePath: string, rule: Record<string, unknown>): WriteResult {
|
|
100
171
|
const { rules, error } = readFile(filePath);
|
|
101
172
|
if (error) return { success: false, error };
|
|
102
173
|
const validation = validateRule(rule);
|
|
103
174
|
if (!validation.valid) return { success: false, error: validation.errors.join("; ") };
|
|
175
|
+
// 去重检测:签名相同则覆盖
|
|
176
|
+
const dupIdx = findDuplicateBySignature(rules, rule);
|
|
177
|
+
if (dupIdx !== null) {
|
|
178
|
+
rules[dupIdx] = rule;
|
|
179
|
+
const writeResult = safeWrite(filePath, rules);
|
|
180
|
+
return writeResult.success
|
|
181
|
+
? { success: true, index: dupIdx, overwritten: true }
|
|
182
|
+
: writeResult;
|
|
183
|
+
}
|
|
104
184
|
rules.push(rule);
|
|
105
185
|
const writeResult = safeWrite(filePath, rules);
|
|
106
186
|
return writeResult.success ? { success: true, index: rules.length - 1 } : writeResult;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shepherd 规则工具辅助函数
|
|
3
|
+
*
|
|
4
|
+
* scope 路径推导、合并列表、跨 scope 去重检测。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import { listRules } from "./rules-editor";
|
|
10
|
+
import { findDuplicateBySignature } from "./rules-editor";
|
|
11
|
+
|
|
12
|
+
/** 项目级规则文件名 */
|
|
13
|
+
const PROJECT_RULES_FILE = "shepherd-rules.json";
|
|
14
|
+
|
|
15
|
+
export type Scope = "global" | "project";
|
|
16
|
+
|
|
17
|
+
/** 根据 scope 推导规则文件路径 */
|
|
18
|
+
export function getRulesFilePath(
|
|
19
|
+
scope: Scope | undefined,
|
|
20
|
+
globalDir: string,
|
|
21
|
+
cwd: string,
|
|
22
|
+
): string {
|
|
23
|
+
if (scope === "project") {
|
|
24
|
+
return path.join(cwd, ".pi", "extensions", PROJECT_RULES_FILE);
|
|
25
|
+
}
|
|
26
|
+
// 默认 global
|
|
27
|
+
return path.join(globalDir, "rules.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ListRuleItem {
|
|
31
|
+
index: number;
|
|
32
|
+
comment: string;
|
|
33
|
+
scope: Scope;
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
action?: string;
|
|
36
|
+
tool?: string;
|
|
37
|
+
hook?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 按_scope 列出规则。scope 不传时返回全局+项目合并列表。
|
|
43
|
+
*/
|
|
44
|
+
export function listRulesByScope(
|
|
45
|
+
globalDir: string,
|
|
46
|
+
cwd: string,
|
|
47
|
+
scope?: Scope,
|
|
48
|
+
): ListRuleItem[] {
|
|
49
|
+
const results: ListRuleItem[] = [];
|
|
50
|
+
|
|
51
|
+
if (!scope || scope === "global") {
|
|
52
|
+
const globalPath = path.join(globalDir, "rules.json");
|
|
53
|
+
if (fs.existsSync(globalPath)) {
|
|
54
|
+
const r = listRules(globalPath);
|
|
55
|
+
if (!r.error && r.rules) {
|
|
56
|
+
for (const rule of r.rules) {
|
|
57
|
+
results.push({ ...rule, scope: "global" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!scope || scope === "project") {
|
|
64
|
+
const projectPath = path.join(cwd, ".pi", "extensions", PROJECT_RULES_FILE);
|
|
65
|
+
if (fs.existsSync(projectPath)) {
|
|
66
|
+
const r = listRules(projectPath);
|
|
67
|
+
if (!r.error && r.rules) {
|
|
68
|
+
for (const rule of r.rules) {
|
|
69
|
+
results.push({ ...rule, scope: "project" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 跨 scope 去重检测:写入 A scope 时检查 B scope 是否有签名相同的规则。
|
|
80
|
+
* 返回 warning 字符串或 null。
|
|
81
|
+
*/
|
|
82
|
+
export function checkCrossScopeDuplicate(
|
|
83
|
+
targetScope: Scope,
|
|
84
|
+
globalDir: string,
|
|
85
|
+
cwd: string,
|
|
86
|
+
newRule: Record<string, unknown>,
|
|
87
|
+
): string | null {
|
|
88
|
+
const otherScope: Scope = targetScope === "global" ? "project" : "global";
|
|
89
|
+
const otherPath = getRulesFilePath(otherScope, globalDir, cwd);
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(otherPath)) return null;
|
|
92
|
+
|
|
93
|
+
const content = fs.readFileSync(otherPath, "utf-8");
|
|
94
|
+
let rules: Record<string, unknown>[];
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(content);
|
|
97
|
+
if (!Array.isArray(parsed)) return null;
|
|
98
|
+
rules = parsed;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const dupIdx = findDuplicateBySignature(rules, newRule);
|
|
104
|
+
if (dupIdx !== null) {
|
|
105
|
+
const scopeLabel = otherScope === "global" ? "全局" : "项目级";
|
|
106
|
+
return `⚠ ${scopeLabel}已有相似规则 [${otherScope}:${dupIdx}]`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 确保项目级规则文件的目录存在
|
|
114
|
+
*/
|
|
115
|
+
export function ensureProjectDir(cwd: string): void {
|
|
116
|
+
const dir = path.join(cwd, ".pi", "extensions");
|
|
117
|
+
if (!fs.existsSync(dir)) {
|
|
118
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
}
|