pi-shepherd 0.1.1 → 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.
Files changed (58) hide show
  1. package/README.en.md +206 -83
  2. package/README.md +209 -88
  3. package/index.ts +262 -229
  4. package/package.json +60 -58
  5. package/rules.json +197 -218
  6. package/shepherd/compaction.ts +76 -0
  7. package/shepherd/conditions.ts +98 -0
  8. package/shepherd/ephemeral.ts +55 -52
  9. package/shepherd/git.ts +64 -0
  10. package/shepherd/index.ts +14 -0
  11. package/shepherd/line-count.ts +86 -86
  12. package/shepherd/message-end.ts +120 -0
  13. package/shepherd/rules-editor.ts +250 -135
  14. package/shepherd/rules-tool-helpers.ts +119 -0
  15. package/shepherd/rules-tool-list.ts +124 -0
  16. package/shepherd/rules-tool.ts +182 -99
  17. package/shepherd/rules-validate.ts +89 -44
  18. package/shepherd/rules.ts +366 -283
  19. package/shepherd/tool-event-types.ts +27 -14
  20. package/shepherd/tool-hooks.ts +165 -176
  21. package/shepherd/worktree-check.ts +130 -130
  22. package/tsconfig.json +21 -14
  23. package/node_modules/@pi-atelier/shared-utils/README.en.md +0 -182
  24. package/node_modules/@pi-atelier/shared-utils/README.md +0 -182
  25. package/node_modules/@pi-atelier/shared-utils/package.json +0 -51
  26. package/node_modules/@pi-atelier/shared-utils/src/__tests__/agents.test.ts +0 -120
  27. package/node_modules/@pi-atelier/shared-utils/src/__tests__/ephemeral.test.ts +0 -100
  28. package/node_modules/@pi-atelier/shared-utils/src/__tests__/file-lock.test.ts +0 -152
  29. package/node_modules/@pi-atelier/shared-utils/src/__tests__/filter-match.test.ts +0 -187
  30. package/node_modules/@pi-atelier/shared-utils/src/__tests__/memory-parser.test.ts +0 -170
  31. package/node_modules/@pi-atelier/shared-utils/src/__tests__/paths.test.ts +0 -126
  32. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config-edge.test.ts +0 -138
  33. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-config.test.ts +0 -257
  34. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools-mcp.test.ts +0 -189
  35. package/node_modules/@pi-atelier/shared-utils/src/__tests__/project-tools.test.ts +0 -204
  36. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-advanced.test.ts +0 -269
  37. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup-array.test.ts +0 -267
  38. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-backup.test.ts +0 -520
  39. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-read.test.ts +0 -116
  40. package/node_modules/@pi-atelier/shared-utils/src/__tests__/settings-write.test.ts +0 -119
  41. package/node_modules/@pi-atelier/shared-utils/src/__tests__/tool-output.test.ts +0 -145
  42. package/node_modules/@pi-atelier/shared-utils/src/agents.ts +0 -39
  43. package/node_modules/@pi-atelier/shared-utils/src/ephemeral.ts +0 -42
  44. package/node_modules/@pi-atelier/shared-utils/src/file-lock.ts +0 -62
  45. package/node_modules/@pi-atelier/shared-utils/src/filter-match.ts +0 -100
  46. package/node_modules/@pi-atelier/shared-utils/src/index.ts +0 -71
  47. package/node_modules/@pi-atelier/shared-utils/src/memory-parser.ts +0 -96
  48. package/node_modules/@pi-atelier/shared-utils/src/paths.ts +0 -23
  49. package/node_modules/@pi-atelier/shared-utils/src/project-config.ts +0 -241
  50. package/node_modules/@pi-atelier/shared-utils/src/project-tools.ts +0 -191
  51. package/node_modules/@pi-atelier/shared-utils/src/settings-array.ts +0 -73
  52. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-rollback.ts +0 -104
  53. package/node_modules/@pi-atelier/shared-utils/src/settings-backup-utils.ts +0 -75
  54. package/node_modules/@pi-atelier/shared-utils/src/settings-backup.ts +0 -172
  55. package/node_modules/@pi-atelier/shared-utils/src/settings.ts +0 -104
  56. package/node_modules/@pi-atelier/shared-utils/src/tool-output.ts +0 -149
  57. package/node_modules/@pi-atelier/shared-utils/tsconfig.json +0 -9
  58. package/node_modules/@pi-atelier/shared-utils/vitest.config.ts +0 -24
package/index.ts CHANGED
@@ -1,229 +1,262 @@
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
- }
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 { dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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
+ hasGitUntracked,
46
+ hasWarnings,
47
+ isSubagent,
48
+ loadRules,
49
+ notifySummary,
50
+ pushWarning,
51
+ registerMessageEnd,
52
+ registerSessionCompact,
53
+ registerToolCall,
54
+ registerToolResult,
55
+ resetMessageEndState,
56
+ StateTracker,
57
+ type ToolState,
58
+ } from "./shepherd";
59
+ import { isQuestionEnding, ruleMatches } from "./shepherd/rules";
60
+ import type { BuiltinContext } from "./shepherd/conditions";
61
+ import { registerRulesEditorTool } from "./shepherd/rules-tool";
62
+
63
+ /** 本地 hints 缓冲区(收集 pi.events.emit("ephemeral:hint") 的数据) */
64
+ const _localHints: { text: string; short?: string }[] = [];
65
+
66
+ /** 可变状态:跨 hook 共享 */
67
+ let _aborted = false;
68
+ let _wasDirty = false;
69
+ const _agentEndFired = new Set<string>();
70
+ const _toolState: ToolState = {
71
+ hasEdits: false,
72
+ tracker: new StateTracker(),
73
+ cachedTools: null,
74
+ };
75
+
76
+ export default function shepherdExtension(pi: ExtensionAPI) {
77
+ // ── 读取配置(三层合并:defaults → 全局 settings → 项目 settings)──
78
+ const { config: shepherdConfig } = getEffectiveConfig<{
79
+ projectRulesPattern: string;
80
+ maxWarnings: number;
81
+ }>(
82
+ "shepherd",
83
+ {
84
+ projectRulesPattern: "shepherd-rules-",
85
+ maxWarnings: 5,
86
+ },
87
+ process.cwd(),
88
+ );
89
+
90
+ // ── 监听跨扩展 hints(通过 pi.events 绕过 jiti 多实例) ──
91
+ pi.events.on("ephemeral:hint", (data) => {
92
+ const { text, short } = data as { text: string; short?: string };
93
+ _localHints.push({ text, short });
94
+ });
95
+
96
+ // ── before_provider_request:注入临时提示 ──────────────────
97
+ pi.on("before_provider_request", async (event, ctx) => {
98
+ // shepherd 规则 hints
99
+ const shepherdText = drainHints();
100
+ if (shepherdText) {
101
+ _localHints.unshift({ text: shepherdText });
102
+ }
103
+
104
+ // 通知摘要:short 优先,fallback 到 notifySummary 截断
105
+ const shortParts = _localHints
106
+ .map((h) => h.short)
107
+ .filter(Boolean) as string[];
108
+ const longParts = _localHints
109
+ .map((h) => (h.short ? null : h.text))
110
+ .filter(Boolean) as string[];
111
+ const notifyText = [...shortParts, ...longParts].join("\n\n");
112
+
113
+ const allHints = _localHints
114
+ .splice(0)
115
+ .map((h) => h.text)
116
+ .join("\n\n");
117
+ let payload = event.payload as ProviderPayload;
118
+
119
+ if (allHints) {
120
+ const text = allHints;
121
+ payload = { ...payload };
122
+ payload.messages = [...(payload.messages ?? [])];
123
+ payload.messages.push({
124
+ role: "user",
125
+ content: [{ type: "text", text }],
126
+ });
127
+ ctx.ui.notify?.(notifySummary(notifyText), "warning");
128
+ }
129
+
130
+ return payload;
131
+ });
132
+
133
+ // ── session_start ──────────────────────────────────────────
134
+ pi.on("session_start", async (_event, ctx) => {
135
+ checkWorktrees(ctx.ui);
136
+ });
137
+
138
+ // ── agent_start ────────────────────────────────────────────
139
+ pi.on("agent_start", async (_event, ctx) => {
140
+ _aborted = ctx.signal?.aborted ?? false;
141
+ _toolState.hasEdits = false;
142
+ _toolState.cachedTools = null;
143
+ resetMessageEndState();
144
+ if (ctx.signal && !ctx.signal.aborted) {
145
+ ctx.signal.addEventListener("abort", () => {
146
+ _aborted = true;
147
+ });
148
+ }
149
+ _wasDirty = hasGitUncommittedChanges();
150
+ });
151
+
152
+ pi.on("input", async (_event) => {
153
+ /* 用户输入新消息 — 允许 agent_end 规则再次触发 */
154
+ _agentEndFired.clear();
155
+ });
156
+
157
+ // ── agent_end ──────────────────────────────────────────────
158
+ pi.on("agent_end", async (event, _ctx) => {
159
+ if (isSubagent() || _aborted) return;
160
+ const rules = loadRules(RULES_DIR, {
161
+ projectRulesPattern: shepherdConfig.projectRulesPattern,
162
+ }).filter((r) => r.hook === "agent_end");
163
+ if (rules.length === 0) return;
164
+
165
+ const lastAssistant = [...event.messages]
166
+ .reverse()
167
+ .find((m) => m.role === "assistant");
168
+ const stopReason: string | undefined = (
169
+ lastAssistant as unknown as PayloadMessage | undefined
170
+ )?.stopReason as string | undefined;
171
+
172
+ // 提取最后一条 assistant 消息的文本内容(用于 not_question_ending 条件)
173
+ let lastAssistantText = "";
174
+ if (lastAssistant) {
175
+ const parts = (lastAssistant as unknown as PayloadMessage).content;
176
+ if (Array.isArray(parts)) {
177
+ lastAssistantText = parts
178
+ .filter((p: { type?: string }) => p.type === "text")
179
+ .map((p: { text?: string }) => p.text ?? "")
180
+ .join("");
181
+ } else if (typeof parts === "string") {
182
+ lastAssistantText = parts;
183
+ }
184
+ }
185
+ let pushed = false;
186
+
187
+ // 如果 AI 在问用户问题(等待回复),跳过所有 agent_end 提醒
188
+ const isQuestion = isQuestionEnding(lastAssistantText);
189
+
190
+ for (const rule of rules) {
191
+ const allowedReasons = rule.stopReason ?? ["stop"];
192
+ if (!allowedReasons.includes((stopReason ?? "") as never)) continue;
193
+ if (_agentEndFired.has(rule.comment)) continue;
194
+ if (isQuestion) continue; // AI 在问问题,跳过收尾提醒
195
+
196
+ // 统一条件匹配:通过 ruleMatches 检查 conditions(含 builtin)
197
+ const ctx: BuiltinContext = {
198
+ hasEdits: _toolState.hasEdits,
199
+ gitDirty: hasGitUncommittedChanges(),
200
+ gitUntracked: hasGitUntracked(),
201
+ lastAssistantText,
202
+ };
203
+ const shouldNotify = ruleMatches(rule, {}, ctx);
204
+ if (ctx.gitDirty) _wasDirty = true;
205
+
206
+ if (shouldNotify && rule.action === "notify") {
207
+ _agentEndFired.add(rule.comment);
208
+ pushWarning(rule.reason, rule.comment);
209
+ pushed = true;
210
+ }
211
+ }
212
+
213
+ // 统一发送唯一的 sendMessage(triggerTurn):
214
+ // - pushed:agent_end 自己推入了 warning
215
+ // - hasWarnings():message_end 或 tool_result 推入了 warning(它们不再自己发 sendMessage)
216
+ // 只发一次,避免多个 hook 各发一次导致第二个 drainHints 拿空 → 空回复
217
+ if (pushed || hasWarnings()) {
218
+ setTimeout(() => {
219
+ try {
220
+ pi.sendMessage(
221
+ { customType: "shepherd-agent-end", display: false, content: "" },
222
+ { triggerTurn: true },
223
+ );
224
+ } catch {
225
+ /* session 已替换 */
226
+ }
227
+ }, 0);
228
+ }
229
+ });
230
+
231
+ // ── session_shutdown ───────────────────────────────────────
232
+ pi.on("session_shutdown", async (_event, ctx) => {
233
+ const rules = loadRules(RULES_DIR, {
234
+ projectRulesPattern: shepherdConfig.projectRulesPattern,
235
+ }).filter((r) => r.hook === "session_shutdown");
236
+ if (rules.length === 0) return;
237
+ for (const rule of rules) {
238
+ // 统一条件匹配
239
+ const builtinCtx: BuiltinContext = {
240
+ hasEdits: false,
241
+ gitDirty: hasGitUncommittedChanges(),
242
+ gitUntracked: hasGitUntracked(),
243
+ };
244
+ const shouldNotify = ruleMatches(rule, {}, builtinCtx);
245
+ if (shouldNotify && rule.action === "notify") {
246
+ ctx.ui?.notify?.(`⚠️ shepherd: ${rule.reason}`, "warning");
247
+ }
248
+ }
249
+ });
250
+
251
+ // ── tool_call + tool_result(提取到 tool-hooks.ts)────────
252
+ const _rulesOpts = {
253
+ projectRulesPattern: shepherdConfig.projectRulesPattern,
254
+ };
255
+ registerToolCall(pi, _toolState, RULES_DIR, _rulesOpts);
256
+ registerToolResult(pi, _toolState, RULES_DIR, _rulesOpts);
257
+ registerMessageEnd(pi, _toolState, RULES_DIR, _rulesOpts);
258
+ registerSessionCompact(pi, _toolState, RULES_DIR, _rulesOpts);
259
+
260
+ // ── shepherd_rules 工具:规则文件安全编辑 ───────────────────
261
+ registerRulesEditorTool(pi, RULES_DIR, process.cwd());
262
+ }
package/package.json CHANGED
@@ -1,58 +1,60 @@
1
- {
2
- "name": "pi-shepherd",
3
- "version": "0.1.1",
4
- "description": "Line count guard and behavior rules extension for pi-coding-agent",
5
- "main": "index.ts",
6
- "scripts": {
7
- "test": "vitest run",
8
- "lint": "biome check lib tests index.ts tool-hooks.ts",
9
- "typecheck": "tsc --noEmit"
10
- },
11
- "dependencies": {
12
- "@pi-atelier/shared-utils": "^1.0.0",
13
- "ignore": "^7.0.5"
14
- },
15
- "peerDependencies": {
16
- "@earendil-works/pi-coding-agent": ">=0.9.0"
17
- },
18
- "peerDependenciesMeta": {
19
- "@earendil-works/pi-coding-agent": {
20
- "optional": true
21
- }
22
- },
23
- "pi": {
24
- "extensions": [
25
- "."
26
- ]
27
- },
28
- "devDependencies": {
29
- "@biomejs/biome": "^2.4.15",
30
- "@vitest/coverage-v8": "^3.2.4",
31
- "vitest": "^3.0.0"
32
- },
33
- "files": [
34
- "index.ts",
35
- "shepherd/",
36
- "rules.json",
37
- "README.md",
38
- "README.en.md",
39
- "tsconfig.json",
40
- "vitest.config.ts",
41
- "package.json"
42
- ],
43
- "bundleDependencies": [
44
- "@pi-atelier/shared-utils"
45
- ],
46
- "keywords": [
47
- "pi-package"
48
- ],
49
- "homepage": "https://catlain.github.io/pi-atelier/",
50
- "repository": {
51
- "type": "git",
52
- "url": "git+ssh://git@github.com/catlain/pi-shepherd.git"
53
- },
54
- "bugs": {
55
- "url": "https://github.com/catlain/pi-shepherd/issues"
56
- },
57
- "license": "MIT"
58
- }
1
+ {
2
+ "name": "pi-shepherd",
3
+ "version": "0.2.0",
4
+ "description": "Line count guard and behavior rules extension for pi-coding-agent",
5
+ "main": "index.ts",
6
+ "scripts": {
7
+ "test": "vitest run",
8
+ "lint": "biome check lib tests index.ts tool-hooks.ts",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@pi-atelier/shared-utils": "^1.0.0",
13
+ "ignore": "^7.0.5"
14
+ },
15
+ "peerDependencies": {
16
+ "@earendil-works/pi-coding-agent": ">=0.9.0"
17
+ },
18
+ "peerDependenciesMeta": {
19
+ "@earendil-works/pi-coding-agent": {
20
+ "optional": true
21
+ }
22
+ },
23
+ "optionalDependencies": {
24
+ "@earendil-works/pi-coding-agent": "^0.76.0"
25
+ },
26
+ "pi": {
27
+ "extensions": [
28
+ "."
29
+ ]
30
+ },
31
+ "devDependencies": {
32
+ "@biomejs/biome": "^2.4.15",
33
+ "@types/node": "^25.9.2",
34
+ "@vitest/coverage-v8": "^3.2.4",
35
+ "typescript": "^6.0.3",
36
+ "vitest": "^3.0.0"
37
+ },
38
+ "files": [
39
+ "index.ts",
40
+ "shepherd/",
41
+ "rules.json",
42
+ "README.md",
43
+ "README.en.md",
44
+ "tsconfig.json",
45
+ "vitest.config.ts",
46
+ "package.json"
47
+ ],
48
+ "keywords": [
49
+ "pi-package"
50
+ ],
51
+ "homepage": "https://catlain.github.io/pi-atelier/",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+ssh://git@github.com/catlain/pi-shepherd.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/catlain/pi-shepherd/issues"
58
+ },
59
+ "license": "MIT"
60
+ }