pi-shepherd 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/rules.json CHANGED
@@ -20,7 +20,7 @@
20
20
  "flags": ""
21
21
  }
22
22
  ],
23
- "reason": "编辑了 Python 文件,必须:1) 运行 ruff check <file> 做格式检查;2) 跑覆盖该代码的单元测试(如无测试则先补充),修复所有测试问题确保通过。",
23
+ "reason": "编辑了 Python 文件,必须:1) 运行 ruff check <file> 做格式检查;2) 验证测试——迭代场景用 fast 档 `uv run python scripts/run_test_gate.py --fast`(按 diff 只跑受影响子项目,分钟级反馈);change 收尾/宣称「全量测试通过」SHALL 跑全量 `uv run python scripts/run_test_gate.py`(fast 凭证 .test-gate-fast-state.json 不属于全量绿证据)。",
24
24
  "enabled": true
25
25
  },
26
26
  {
@@ -34,7 +34,7 @@
34
34
  "pattern": "(>>|>|tee|sed\\s+-i|cp\\s|mv\\s).*settings\\.json|settings\\.json\\s*(>>|>)"
35
35
  }
36
36
  ],
37
- "reason": "直接编辑 settings.json 曾导致配置丢失(119行变3行)。必须使用 patchSettingsSectionWithBackup 或 settings_rollback。",
37
+ "reason": "直接编辑 settings.json 曾导致配置丢失(119行变3行)。必须使用 settings_patch 工具(自动备份+文件锁+校验),或 settings_rollback 回滚。",
38
38
  "enabled": true
39
39
  },
40
40
  {
@@ -50,7 +50,7 @@
50
50
  }
51
51
  ],
52
52
  "reason": "编辑了 Rust 文件,必须:1) 运行 cargo clippy 做格式和 lint 检查;2) 跑覆盖该代码的单元测试(如无测试则先补充),修复所有测试问题确保通过。",
53
- "enabled": true
53
+ "enabled": false
54
54
  },
55
55
  {
56
56
  "comment": "[TypeScript] 编辑/写入后必须跑测试",
@@ -68,26 +68,33 @@
68
68
  "enabled": true
69
69
  },
70
70
  {
71
- "comment": "收尾规则 — agent_end 时提醒 commit + 记忆 + 总结,commit 必须带 session ID",
71
+ "comment": "[arch] 会话结束前检查 git 状态(已停用:多会话并行时仓库级 git_dirty 误伤其他会话任务,收尾检查移入 opsx-archive 归档流程)",
72
72
  "hook": "agent_end",
73
+ "check": "",
73
74
  "action": "notify",
74
- "check": "has_edits",
75
- "reason": "⚠️ shepherd: 检测到文件编辑,执行收尾工作:\n1️⃣ **Git commit**:如有未提交的 git 改动 → git add + git commit。**⚠️ commit message 末尾必须加 `session:${PI_SESSION_ID}`** — 这是硬性要求,不是建议。session ID 用于会话分析和 roadmap 追踪,缺少会导致无法追溯。格式示例:`feat: xxx session:019e71c9-b340-7fae-a330-98d837e72099`\n2️⃣ **记忆更新**:如有值得记住的知识 → memory_update\n3️⃣ **会话总结**:简要总结本次会话做了什么",
76
- "stopReason": [
77
- "stop"
75
+ "reason": "Git 检测到未提交或未跟踪的文件。收尾时请确认:\n1. git add + commit(必须带 session ID)\n2. 如有重要决策/发现 → 更新记忆文件",
76
+ "enabled": false,
77
+ "subagent": false,
78
+ "conditions": [
79
+ {
80
+ "builtin": "git_dirty"
81
+ },
82
+ {
83
+ "builtin": "git_untracked"
84
+ }
78
85
  ],
79
- "enabled": true
86
+ "conditionLogic": "or"
80
87
  },
81
88
  {
82
- "comment": "[memory] git commit 后提醒更新记忆",
89
+ "comment": "[memory] git commit 后提醒更新记忆(已停用:记忆更新检查移入 opsx-archive 归档流程收口)",
83
90
  "hook": "tool_result",
84
91
  "tool": "bash",
85
92
  "pattern": "git\\s+commit",
86
93
  "action": "steer",
87
- "reason": "📝 刚执行了 git commit。检查本轮是否有值得跨会话保留的知识(架构决策、踩坑教训、结论),如果有 → 用 memory_update 工具写入。用法:①先调 memory_index 看已有文件,判断重复/合并/新建;②调 memory_update(fileName, content, scope) 一步写入文件+更新索引。fileName 格式:topic--kw1,kw2,kw3.md。scope:L1=跨项目通用,L2=项目级(默认)。单文件不超 200 行。如果没有新知识则忽略。",
94
+ "reason": "📝 刚执行了 git commit。请检查:**记忆更新** — 是否有值得跨会话保留的知识(架构决策、踩坑教训、结论),如果有 → 用 memory_update 写入。用法:①先调 memory_index 看已有文件;②调 memory_update(fileName, content, scope) 一步写入。fileName 格式:topic--kw1,kw2,kw3.md。scope:L1=跨项目通用,L2=项目级(默认)。单文件不超 200 行。如不需要则忽略。",
88
95
  "subagent": false,
89
96
  "requireSuccess": true,
90
- "enabled": true
97
+ "enabled": false
91
98
  },
92
99
  {
93
100
  "comment": "[arch] enforce-read-over-bash-cat:bash 中 cat|head/tail 读文件时强制用 read 工具",
@@ -118,7 +125,7 @@
118
125
  "conditions": [
119
126
  {
120
127
  "field": "path",
121
- "pattern": "extensions/.*\\.ts$"
128
+ "pattern": "extensions[/\\\\].*\\.ts$"
122
129
  }
123
130
  ],
124
131
  "action": "steer",
@@ -134,7 +141,7 @@
134
141
  "conditions": [
135
142
  {
136
143
  "field": "path",
137
- "pattern": "extensions/.*\\.ts$"
144
+ "pattern": "extensions[/\\\\].*\\.ts$"
138
145
  }
139
146
  ],
140
147
  "action": "steer",
@@ -204,7 +211,7 @@
204
211
  "action": "notify",
205
212
  "pattern": ".",
206
213
  "flags": "s",
207
- "reason": "推荐用 code-graph 替代 grep 搜代码——code-graph 理解 AST 语义,能按符号名、调用链、引用关系精准定位,比 grep 逐行匹配快得多且不漏不误报。选工具方法:①模糊搜索 → semantic_code_search(默认首选);②精确符号名 → get_ast_node;③引用追踪 → find_references;④调用链 → get_call_graph;⑤模块结构 → module_overview。grep 仅适合搜字面量字符串(TODO、配置值、错误消息)。如果当前搜索的就是字面量字符串则忽略此提醒。如果目标目录没有被 code-graph 索引(搜索返回空结果),调用 setup_codegraph({ directory: \"目标目录路径\" }) 一键建索引。",
214
+ "reason": "推荐用 code-graph 替代 grep 搜代码——理解 AST 语义,按符号名、调用链、引用关系精准定位。选工具:①模糊搜索 → semantic_code_search;②精确符号 → get_ast_node;③引用 → find_references;④调用链 → get_call_graph;⑤模块结构 → module_overview。grep 仅适合字面量(TODO、配置值),搜字面量则忽略此提醒。\n\n**涉及字段血缘/数据链路类任务?** → 使用 field-lineage 技能。\n\n**code-graph 故障排查:**\n- **搜不到东西?** → 可能是目标目录没索引。用 `code_graph_project_map()` 查看当前索引范围,不在范围内就调 `setup_codegraph({ directory: \"目标目录绝对路径\" })` 一键建索引(创建软链接+增量索引,即时可搜)。\n- **路径报错 `must be relative`?** → path 参数必须用相对路径(相对于 CWD),不能用绝对路径。\n- **刚改了代码搜不到?** → 索引可能过期,调 `setup_codegraph({ directory: \"项目路径\" })` 重建索引(已索引目录重复调用有效,增量更新)。\n- **确实没有结果** → 忽略以上提醒,可能是符号确实不存在。",
208
215
  "requiresTools": [
209
216
  "code_graph_semantic_code_search"
210
217
  ],
@@ -217,7 +224,7 @@
217
224
  "action": "notify",
218
225
  "pattern": "\\b(grep|rg)\\b.*\\.(py|rs|ts|js|toml)(?=[\\s'\"|)]|$)",
219
226
  "flags": "",
220
- "reason": "💡 bash grep 搜代码文件不如用 pi 内置 grep 工具或 code-graph MCP 工具——code-graph 理解 AST 语义,能精准匹配符号名和调用关系。选工具:①模糊搜索 → semantic_code_search;②精确符号 → get_ast_node;③引用追踪 → find_references;④调用链 → get_call_graph。如果搜的是字面量字符串(TODO、配置值)则忽略。如果目标目录没有被 code-graph 索引,调用 setup_codegraph({ directory: \"目标目录路径\" }) 一键建索引。",
227
+ "reason": "💡 bash grep 搜代码文件不如用 pi 内置 grep 工具或 code-graph MCP 工具——code-graph 理解 AST 语义,能精准匹配符号名和调用关系。选工具:①模糊搜索 → semantic_code_search;②精确符号 → get_ast_node;③引用追踪 → find_references;④调用链 → get_call_graph。如果搜的是字面量字符串(TODO、配置值)则忽略。\n\n**code-graph 故障排查:**\n- **搜不到东西?** → 可能是目标目录没索引。用 `code_graph_project_map()` 查看当前索引范围,不在范围内就调 `setup_codegraph({ directory: \"目标目录绝对路径\" })` 一键建索引。\n- **路径报错?** → path 参数必须用相对路径(相对于 CWD),不能用绝对路径。\n- **刚改了代码搜不到?** → 调 `setup_codegraph({ directory: \"项目路径\" })` 重建索引。",
221
228
  "enabled": true
222
229
  },
223
230
  {
@@ -231,7 +238,7 @@
231
238
  "pattern": "\\.(ts|js|py|rs)$"
232
239
  }
233
240
  ],
234
- "reason": "💡 搜代码文件推荐用 code-graph。如果 code-graph 返回空结果,可能是目标目录没有被索引。调用 setup_codegraph({ directory: \"目标目录路径\" }) 一键建索引(软链接+增量索引,当前会话即刻可搜)。用法参考 code-graph 技能:semantic_code_search(模糊搜索)、get_ast_node(精确符号)、get_call_graph(调用链)、module_overview(模块结构)。",
241
+ "reason": "💡 搜代码文件推荐用 code-graph。如果 code-graph 搜不到,排查步骤:\n1. `code_graph_project_map()` 查看当前索引了哪些目录\n2. 不在范围内 → `setup_codegraph({ directory: \"目标目录绝对路径\" })` 建索引\n3. 路径报错 → 用相对路径,不用绝对路径\n4. 刚改过代码 → `setup_codegraph({ directory: \"项目路径\" })` 重建索引\n用法:semantic_code_search(模糊搜索)、get_ast_node(精确符号)、get_call_graph(调用链)、module_overview(模块结构)。",
235
242
  "enabled": true
236
243
  },
237
244
  {
@@ -275,7 +282,7 @@
275
282
  }
276
283
  ],
277
284
  "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 会绕过这些机制导致数据不一致。",
278
- "enabled": true
285
+ "enabled": false
279
286
  },
280
287
  {
281
288
  "comment": "[roadmap] 禁止通过 bash/python 脚本直接读写 roadmap JSON — 必须用 roadmap 工具",
@@ -289,44 +296,200 @@
289
296
  }
290
297
  ],
291
298
  "reason": "⛔ 禁止通过 bash/python 脚本直接读写 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 会绕过这些机制导致数据不一致。",
299
+ "enabled": false
300
+ },
301
+ {
302
+ "action": "block",
303
+ "comment": "[settings] 禁止 bash 直接修改 settings.json — 必须用 patchSettingsSectionWithBackup 或 settings_rollback",
304
+ "conditions": [
305
+ {
306
+ "field": "command",
307
+ "pattern": "^(?!.*\\b(git|grep|find|ls|cat|head|tail|diff|test|python3.*json\\.load)\\b).*settings\\.json",
308
+ "flags": "s"
309
+ }
310
+ ],
311
+ "hook": "tool_call",
312
+ "message": "⛔ 禁止通过 bash 修改 settings.json!使用 settings_patch 工具安全修改(支持全局 scope='global' 和项目级 scope='project'),或 settings_rollback 回滚。",
313
+ "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 解析读取)。必须使用 settings_patch 工具。",
314
+ "tool": "bash",
315
+ "enabled": false
316
+ },
317
+ {
318
+ "comment": "[git] commit 成功后提醒 push",
319
+ "reason": "项目已有远程仓库 origin,commit 后应同步推送避免本地积压",
320
+ "trigger": "bash",
321
+ "event": "tool_result",
322
+ "condition": "bash command contains 'git commit' and exit code is 0",
323
+ "action": "notify",
324
+ "message": "commit 成功。检查是否有远程仓库(git remote),有的话立即 git push 同步到远程。"
325
+ },
326
+ {
327
+ "comment": "[arch] 编辑 YAML 规则后提醒同步安全网",
328
+ "hook": "tool_result",
329
+ "tool": "edit|write",
330
+ "action": "steer",
331
+ "conditions": [
332
+ {
333
+ "field": "path",
334
+ "pattern": "docs[/\\\\]agent[/\\\\]api[/\\\\].*\\.yaml$"
335
+ }
336
+ ],
337
+ "reason": "📝 架构 artifacts YAML 已变更!请执行以下闭环步骤:\n1. python scripts/govern_sync.py --preview(预览安全网变化)\n2. python scripts/govern_sync.py --apply(应用更新)\n3. python scripts/govern_check.py(验证闭环完整性)",
338
+ "enabled": false,
339
+ "subagent": false
340
+ },
341
+ {
342
+ "comment": "[quality] 大量代码编辑后提醒运行治理检查",
343
+ "hook": "tool_result",
344
+ "tool": "edit|write",
345
+ "action": "notify",
346
+ "conditions": [
347
+ {
348
+ "field": "path",
349
+ "pattern": "src[/\\\\].*\\.rs$"
350
+ }
351
+ ],
352
+ "reason": "📝 编辑了源码。请确认:\n1. cargo clippy --all-targets -- -D warnings\n2. cargo test\n3. bash scripts/check_arch.sh(架构合规)\n4. python scripts/govern_check.py(治理健康度)",
353
+ "enabled": false,
354
+ "subagent": false
355
+ },
356
+ {
357
+ "comment": "[roadmap] 读计划文件时提醒标记 task 为 doing",
358
+ "hook": "tool_result",
359
+ "tool": "read",
360
+ "action": "steer",
361
+ "conditions": [
362
+ {
363
+ "field": "path",
364
+ "pattern": "\\.pi[/\\\\]plans[/\\\\].*\\.md$"
365
+ }
366
+ ],
367
+ "reason": "📖 你正在读取计划文件,说明即将开始执行某个 Task。请在开始编码之前先用 roadmap_update(roadmapId, taskId, {status: 'doing'}) 将对应 Task 标记为 🔄 doing,这样其他会话能看到你正在做什么,避免冲突。如果已经标记过则无视。",
368
+ "subagent": false,
369
+ "requireSuccess": true,
370
+ "enabled": false
371
+ },
372
+ {
373
+ "comment": "[code-graph] 编辑代码文件后提醒更新索引",
374
+ "hook": "tool_result",
375
+ "tool": "edit|write",
376
+ "action": "steer",
377
+ "conditions": [
378
+ {
379
+ "field": "path",
380
+ "pattern": "\\.(ts|js|py|rs)$"
381
+ }
382
+ ],
383
+ "reason": "📝 编辑了代码文件,code-graph 索引可能已过期(新增/重命名/删除的符号不会被搜到)。如果接下来需要用 code-graph 搜索,先调 `setup_codegraph({ directory: \"项目路径\" })` 重建索引。如果只是编辑不再搜索,则忽略。",
384
+ "subagent": false,
385
+ "requireSuccess": true,
292
386
  "enabled": true
293
387
  },
294
388
  {
295
389
  "comment": "[settings] 禁止 edit/write settings.json — 必须用 patchSettingsSectionWithBackup",
296
- "reason": "直接编辑 settings.json 曾导致配置丢失(119 行变 3 行)。必须使用 patchSettingsSectionWithBackup 或 settings_rollback。",
390
+ "action": "block",
391
+ "hook": "tool_call",
297
392
  "tool": "edit|write",
393
+ "conditions": [
394
+ {
395
+ "field": "path",
396
+ "pattern": "settings\\.json$",
397
+ "flags": "i"
398
+ }
399
+ ],
400
+ "message": "⛔ shepherd: 直接编辑 settings.json 曾导致配置丢失(119行变3行)。必须使用 settings_patch 工具(自动备份+文件锁+校验),或 settings_rollback 回滚。",
401
+ "reason": "直接 edit/write settings.json 曾导致配置丢失(119行变3行)。必须使用 settings_patch 工具。",
402
+ "enabled": false
403
+ },
404
+ {
405
+ "comment": "归因猜测提醒:AI 回复含可能时提醒验证",
406
+ "hook": "message_end",
407
+ "conditions": [
408
+ {
409
+ "field": "text",
410
+ "pattern": "可能"
411
+ }
412
+ ],
413
+ "action": "notify",
414
+ "reason": "💡 如果你正在做归因猜测,请先找到确切证据验证(看源码、加 println!、断点调试),不要凭猜测下结论。如果你不是在做归因猜测,请无视此提示。如果你正在等待用户的决策或反馈,当前提示不代表用户同意或确认,请继续询问用户,不要自作主张继续执行。",
415
+ "enabled": false
416
+ },
417
+ {
418
+ "action": "block",
419
+ "check": null,
420
+ "comment": "禁止手动复制 pyd/dll 文件到 .venv",
421
+ "pattern": "cp.*\\.pyd|cp.*quant_py\\.dll|copy.*\\.pyd",
422
+ "reason": "Rust 编译产物必须通过 maturin develop 安装,禁止手动 cp。正确方式:rm -rf .venv/Lib/site-packages/quant_py/ && uv run maturin develop --release"
423
+ },
424
+ {
425
+ "action": "block",
426
+ "check": null,
427
+ "comment": "禁止在子项目目录创建 .venv",
428
+ "pattern": "quant-strategy/\\.venv|quant-fund/\\.venv|quant-base/\\.venv",
429
+ "reason": "AGENTS.md 规定只有根目录 .venv/ 是唯一的虚拟环境。子目录 .venv 会导致 maturin 装错位置。如果发现子目录 .venv 立即删除。"
430
+ },
431
+ {
432
+ "action": "steer",
433
+ "check": null,
434
+ "comment": "Rust 编译后必须验证 pyd 更新",
435
+ "pattern": "maturin develop",
436
+ "reason": "maturin develop 后必须验证 .pyd 时间戳+字节:ls -la --time-style=long-iso .venv/Lib/site-packages/quant_py/*.pyd(编译后查一次,跑完 uv run 再查一次)。pyd 过期有三种根因症状相同:① 目录错(必须根目录+uv run maturin develop);② uv run 按 uv.lock 同步把新 pyd 覆盖回旧产物(字节 3066880→2920960 实证,须 --no-sync);③ 改完 Rust 忘重编译。时间戳没更新 → rm -rf .venv/Lib/site-packages/quant_py/ 再重跑 maturin develop(禁止手动 cp pyd)。详见记忆 rust-comprehensive。"
437
+ },
438
+ {
439
+ "comment": "[shepherd] 禁止直接 edit/write rules.json — 必须用 shepherd_rules 工具",
298
440
  "hook": "tool_call",
441
+ "tool": "edit|write",
299
442
  "action": "block",
300
- "message": "⛔ 禁止直接 edit settings.json!使用 patchSettingsSectionWithBackup() 安全修改,或 settings_rollback 回滚。",
301
443
  "conditions": [
302
444
  {
303
445
  "field": "path",
304
- "pattern": "settings\\.json$|settings\\.json\\.bak"
446
+ "pattern": "shepherd/rules\\.json$"
305
447
  }
306
- ]
448
+ ],
449
+ "reason": "⛔ 禁止直接编辑 rules.json!请用 shepherd_rules 工具操作(自动备份+校验+回滚):\nlist: shepherd_rules(action='list') — 列出所有规则\nadd: shepherd_rules(action='add', rule={...}) — 添加规则\nupdate: shepherd_rules(action='update', index=N, changes={...}) — 更新规则\ndelete: shepherd_rules(action='delete', index=N) — 删除规则\n直接改 JSON 会绕过安全机制(备份/校验/去重检测/签名覆盖)。",
450
+ "enabled": true
307
451
  },
308
452
  {
309
453
  "action": "block",
310
- "comment": "[settings] 禁止 bash 直接修改 settings.json — 必须用 patchSettingsSectionWithBackup 或 settings_rollback",
454
+ "comment": "[shepherd] 禁止通过 bash/python 脚本直接读写 rules.json — 必须用 shepherd_rules 工具",
311
455
  "conditions": [
312
456
  {
313
457
  "field": "command",
314
- "pattern": "^(?!.*\\b(git|grep|find|ls|cat|head|tail|diff|test|python3.*json\\.load)\\b).*settings\\.json",
315
- "flags": "s"
458
+ "pattern": "(rules\\.json[\\s\\S]*(open\\(|json\\.load|json\\.dump|write|sed|awk|jq|cat))|((open\\(|json\\.load|json\\.dump|write|sed|awk|jq|cat)[\\s\\S]*rules\\.json)"
316
459
  }
317
460
  ],
461
+ "enabled": true,
318
462
  "hook": "tool_call",
319
- "message": "⛔ 禁止通过 bash 修改 settings.json!使用 patchSettingsSectionWithBackup() 安全修改,或 settings_rollback 回滚。",
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 解析读取)",
463
+ "reason": "⛔ 禁止通过脚本绕过直接读写 rules.json!请用 shepherd_rules 工具操作(自动备份+校验+回滚+去重检测)。",
321
464
  "tool": "bash"
322
465
  },
323
466
  {
324
- "comment": "[git] commit 成功后提醒 push",
325
- "reason": "项目已有远程仓库 origin,commit 后应同步推送避免本地积压",
326
- "trigger": "bash",
327
- "event": "tool_result",
328
- "condition": "bash command contains 'git commit' and exit code is 0",
467
+ "comment": "[code-graph] module_overview 返回空结果时提醒检查路径前缀",
468
+ "hook": "tool_result",
469
+ "tool": "code_graph_module_overview",
470
+ "conditions": [
471
+ {
472
+ "field": "result",
473
+ "pattern": "0 active \\+ 0 inactive"
474
+ }
475
+ ],
329
476
  "action": "notify",
330
- "message": "commit 成功。检查是否有远程仓库(git remote),有的话立即 git push 同步到远程。"
477
+ "message": "⚠️ code-graph 返回空结果。如果是通过 setup_codegraph 索引的外部目录,请用路径 _codegraph_links/<name> 而不是裸名。例如: code_graph_module_overview(path='_codegraph_links/pi-intercom')",
478
+ "reason": "AI 常误用裸名调用 module_overview(如 path='pi-intercom'),但软链接目录在索引中的路径前缀是 _codegraph_links/"
479
+ },
480
+ {
481
+ "action": "steer",
482
+ "check": null,
483
+ "comment": "纯查询优先走 ro_query 助手(锁感知只读)",
484
+ "flags": "",
485
+ "pattern": "DataStore\\s*\\(\\s*\\)",
486
+ "reason": "检测到内联 DataStore() 默认(RW)构造做纯查询——历史撞锁事故高发模式(会话 a89c 连续撞锁 2 次)。纯查询请改用锁感知助手:uv run python quant-base/scripts/ro_query.py --sql \"...\"(RO+限时重试+持锁 PID 诊断+查完即关,支持 --db 指定 live.duckdb)。若确需 RW 写操作或 DataStore.reader() 网络服务场景,继续即可。"
487
+ },
488
+ {
489
+ "action": "steer",
490
+ "comment": "compact-skill-reminder",
491
+ "enabled": true,
492
+ "hook": "session_compact",
493
+ "reason": "上下文刚被压缩:此前 read 过的 SKILL.md 技能正文已随历史摘要化挥发。system prompt 里的技能索引(名字/描述/路径)仍在——若当前任务涉及某技能,请重新 read 其 SKILL.md 获取操作指引。"
331
494
  }
332
495
  ]
@@ -0,0 +1,76 @@
1
+ /**
2
+ * session_compact hook 处理逻辑
3
+ *
4
+ * pi 会话压缩完成后(manual /compact、threshold 阈值、overflow 溢出恢复),
5
+ * 按 `hook: "session_compact"` 规则注入提醒(典型用途:技能正文已随压缩
6
+ * 挥发,提示 AI 需要时重读 SKILL.md)。
7
+ *
8
+ * 注入走 ephemeral 管线(pushWarning → 缓冲区 → before_provider_request
9
+ * drainHints 消费即焚)。不 sendMessage(triggerTurn):三种 reason 的压缩
10
+ * 之后均必然存在下一个 provider request(overflow→willRetry 自动重试;
11
+ * manual/threshold→用户下一条消息)。
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { pushWarning } from "./ephemeral.js";
16
+ import {
17
+ isSubagent,
18
+ type LoadRulesOptions,
19
+ loadRules,
20
+ ruleMatches,
21
+ } from "./rules.js";
22
+ import type { ToolState } from "./tool-hooks.js";
23
+ import { toolsAvailable } from "./tool-hooks.js";
24
+
25
+ /**
26
+ * 注册 session_compact 事件处理。
27
+ *
28
+ * 触发语义:一次压缩事件 = 一次真实挥发 = 一次提醒(不做每会话一次的
29
+ * _fired 去重——与 message_end 的"每轮一次"语义刻意不同)。
30
+ */
31
+ export function registerSessionCompact(
32
+ pi: ExtensionAPI,
33
+ state: ToolState,
34
+ rulesDir?: string,
35
+ rulesOptions?: LoadRulesOptions,
36
+ ): void {
37
+ pi.on("session_compact", async (event, _ctx) => {
38
+ const rules = loadRules(rulesDir, rulesOptions).filter(
39
+ (r) => r.hook === "session_compact",
40
+ );
41
+ if (rules.length === 0) return;
42
+
43
+ // 匹配目标:reason 为压缩触发源(manual/threshold/overflow)
44
+ const reason: string = event.reason ?? "";
45
+ const targets = {
46
+ text: "",
47
+ path: "",
48
+ command: "",
49
+ glob: "",
50
+ result: "",
51
+ reason,
52
+ };
53
+
54
+ for (const rule of rules) {
55
+ // 跳过禁用规则
56
+ if (rule.enabled === false) continue;
57
+ // 子代理控制:默认跳过,显式 subagent: true 才在子代理触发
58
+ if (isSubagent() && rule.subagent !== true) continue;
59
+ // 工具依赖检查
60
+ if (!toolsAvailable(rule, pi, state)) continue;
61
+
62
+ // 条件匹配:conditions 模式(field 可为 reason 等);
63
+ // 单条件 pattern 模式匹配 reason(向后兼容风格);
64
+ // 都没有 = 无条件触发
65
+ if (rule.conditions && rule.conditions.length > 0) {
66
+ if (!ruleMatches(rule, targets, undefined)) continue;
67
+ } else if (rule.pattern) {
68
+ const re = rule._compiled ?? new RegExp(rule.pattern, rule.flags || "");
69
+ if (!re.test(reason)) continue;
70
+ }
71
+
72
+ // steer/notify 统一走 pushWarning(即焚注入)
73
+ pushWarning(rule.reason, rule.comment);
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,98 @@
1
+ import { hasGitUntracked, isGitDirty } from "./git";
2
+
3
+ /** 内置条件类型:不依赖正则,直接检查环境状态 */
4
+ export type ConditionBuiltin =
5
+ | "git_dirty" // git 有已跟踪文件的未提交改动(M/A/D/R)
6
+ | "git_untracked" // git 有未跟踪文件(??)
7
+ | "git_uncommitted" // 旧 check 字段迁移兼容:dirty || untracked
8
+ | "has_edits" // 本轮调用过 edit/write
9
+ | "not_question_ending" // AI 最后一条消息不以问句结尾(用于抑制"等待回复"场景的 agent_end 提醒)
10
+ | "always"; // 始终匹配
11
+
12
+ /** 内置条件匹配上下文 */
13
+ export interface BuiltinContext {
14
+ hasEdits?: boolean;
15
+ gitDirty?: boolean;
16
+ gitUntracked?: boolean;
17
+ lastAssistantText?: string;
18
+ }
19
+
20
+ /**
21
+ * 宽松检测 AI 消息是否包含问句/确认模式(等待用户回复)
22
+ *
23
+ * 策略:全文搜索,只要出现任何问句模式就返回 true(宁可多跳,不误打扰用户)
24
+ * 排除代码块和引用块内的内容,避免误判技术文本中的 ? 运算符等
25
+ */
26
+ export function isQuestionEnding(text: string): boolean {
27
+ if (!text) return false;
28
+
29
+ // 去除代码块(```...```),避免 ?. 可选链、?: 三元等误判
30
+ const noCodeBlocks = text.replace(/```[\s\S]*?```/g, "");
31
+ // 去除行内代码(`...`)
32
+ const noInlineCode = noCodeBlocks.replace(/`[^`]+`/g, "");
33
+ // 宽松方案:保留所有行(含引用块),不做过滤
34
+ const prose = noInlineCode;
35
+
36
+ if (!prose.trim()) return false;
37
+
38
+ // 1. 中英文问号出现在行末或句末(后面可能跟空格/换行/标点)
39
+ if (/[??][\s]*(?:[\n]|$|[。.!,,、;;::))\]])/m.test(prose)) return true;
40
+ // 独立问号结尾
41
+ if (/[??]\s*$/m.test(prose)) return true;
42
+
43
+ // 2. 中文疑问语气词结尾(非代码上下文)
44
+ // "呢/吗/吧/嘛/么/啦/呗" 出现在句末
45
+ if (/(?:呢|吗|吧|嘛|么|啦|呗)\s*[??。.!!,,、;;:\s]*$/m.test(prose)) return true;
46
+
47
+ // 3. 选择/确认类句式(全文搜索)
48
+ const confirmPatterns = [
49
+ /要不要/,
50
+ /你觉得/,
51
+ /你想/,
52
+ /确认(?:一下)?/,
53
+ /可以吗/,
54
+ /行不行/,
55
+ /好不好/,
56
+ /是不是/,
57
+ /还是说/,
58
+ /还是先/,
59
+ /先(?:讨论|确认|看看)/,
60
+ /等(?:你|你(?:的)?回复)/,
61
+ /你(?:还)?(?:有|有没有)/,
62
+ /(?:你|您)(?:觉得|想|希望|倾向|偏好)/,
63
+ /请(?:确认|回复|告诉)/,
64
+ /你来(?:决定|选|定)/,
65
+ /直接改还是/,
66
+ ];
67
+ for (const p of confirmPatterns) {
68
+ if (p.test(prose)) return true;
69
+ }
70
+
71
+ return false;
72
+ }
73
+
74
+ /** 判断单个内置条件是否满足 */
75
+ export function matchBuiltinCondition(
76
+ builtin: ConditionBuiltin,
77
+ ctx: BuiltinContext,
78
+ ): boolean {
79
+ switch (builtin) {
80
+ case "always":
81
+ return true;
82
+ case "has_edits":
83
+ return !!ctx.hasEdits;
84
+ case "git_dirty":
85
+ return (ctx.gitDirty ?? isGitDirty()) === true;
86
+ case "git_untracked":
87
+ return (ctx.gitUntracked ?? hasGitUntracked()) === true;
88
+ case "git_uncommitted": {
89
+ // 旧 check 字段迁移兼容:git_uncommitted = dirty || untracked
90
+ const dirty = ctx.gitDirty !== undefined ? ctx.gitDirty : isGitDirty();
91
+ const untracked =
92
+ ctx.gitUntracked !== undefined ? ctx.gitUntracked : hasGitUntracked();
93
+ return dirty || untracked;
94
+ }
95
+ case "not_question_ending":
96
+ return !isQuestionEnding(ctx.lastAssistantText ?? "");
97
+ }
98
+ }
@@ -1,52 +1,55 @@
1
- /**
2
- * Shepherd 专用提示缓冲区
3
- *
4
- * shepherd 规则触发的 steer/notify 提示通过 pushWarning 推入,
5
- * 由共享的 ephemeral 注入机制在 before_provider_request 时发送。
6
- * 提示只对当前请求生效,不写入 session 历史。
7
- */
8
-
9
- /** 替换 ${ENV_VAR} 格式的环境变量 */
10
- function expandEnvVars(text: string): string {
11
- return text.replace(/\$\{(\w+)\}/g, (_match, name: string) => process.env[name] ?? `\${${name}}`);
12
- }
13
-
14
- /** 推入一条 shepherd 提示(自动加 ⚠️ shepherd: 前缀) */
15
- export function pushWarning(reason: string, label?: string): void {
16
- // shepherd 前缀用于通知气泡识别,注入时由 injectHints 统一处理
17
- pushShepherdHint(expandEnvVars(reason), label);
18
- }
19
-
20
- /** 生成通知气泡用的摘要(优先用规则名列表,fallback 截断 reason) */
21
- export function notifySummary(text: string, labels?: string[]): string {
22
- // 优先使用规则名(comment)列表
23
- if (labels && labels.length > 0) {
24
- const joined = labels.join("、");
25
- return joined.length > 120 ? joined.slice(0, 117) + "..." : joined;
26
- }
27
- // fallback:截取第一个 --- 之前的内容
28
- const idx = text.indexOf("\n---");
29
- if (idx > 0) return text.slice(0, idx);
30
- if (text.length > 120) return text.slice(0, 117) + "...";
31
- return text;
32
- }
33
-
34
- // ── 内部:shepherd 前缀推入共享缓冲区 ──
35
-
36
- import { pushHint as _pushShared, hasHints } from "./ephemeral-shared.js";
37
-
38
- const SHEPHERD_PREFIX = "⚠️ shepherd: ";
39
-
40
- function pushShepherdHint(reason: string, label?: string): void {
41
- _pushShared(`${SHEPHERD_PREFIX}${reason}`, label);
42
- }
43
-
44
- /** 推入规则格式错误提示(加 ❌ 前缀区别于普通 warning) */
45
- export function pushRuleError(msg: string): void {
46
- _pushShared(`❌ shepherd 规则格式错误: ${msg}`);
47
- }
48
-
49
- /** shepherd 是否有待发送的提示 */
50
- export function hasWarnings(): boolean {
51
- return hasHints();
52
- }
1
+ /**
2
+ * Shepherd 专用提示缓冲区
3
+ *
4
+ * shepherd 规则触发的 steer/notify 提示通过 pushWarning 推入,
5
+ * 由共享的 ephemeral 注入机制在 before_provider_request 时发送。
6
+ * 提示只对当前请求生效,不写入 session 历史。
7
+ */
8
+
9
+ /** 替换 ${ENV_VAR} 格式的环境变量 */
10
+ function expandEnvVars(text: string): string {
11
+ return text.replace(
12
+ /\$\{(\w+)\}/g,
13
+ (_match, name: string) => process.env[name] ?? `\${${name}}`,
14
+ );
15
+ }
16
+
17
+ /** 推入一条 shepherd 提示(自动加 ⚠️ shepherd: 前缀) */
18
+ export function pushWarning(reason: string, label?: string): void {
19
+ // shepherd 前缀用于通知气泡识别,注入时由 injectHints 统一处理
20
+ pushShepherdHint(expandEnvVars(reason), label);
21
+ }
22
+
23
+ /** 生成通知气泡用的摘要(优先用规则名列表,fallback 截断 reason) */
24
+ export function notifySummary(text: string, labels?: string[]): string {
25
+ // 优先使用规则名(comment)列表
26
+ if (labels && labels.length > 0) {
27
+ const joined = labels.join("、");
28
+ return joined.length > 120 ? `${joined.slice(0, 117)}...` : joined;
29
+ }
30
+ // fallback:截取第一个 --- 之前的内容
31
+ const idx = text.indexOf("\n---");
32
+ if (idx > 0) return text.slice(0, idx);
33
+ if (text.length > 120) return `${text.slice(0, 117)}...`;
34
+ return text;
35
+ }
36
+
37
+ // ── 内部:shepherd 前缀推入共享缓冲区 ──
38
+
39
+ import { pushHint as _pushShared, hasHints } from "./ephemeral-shared.js";
40
+
41
+ const SHEPHERD_PREFIX = "⚠️ shepherd: ";
42
+
43
+ function pushShepherdHint(reason: string, label?: string): void {
44
+ _pushShared(`${SHEPHERD_PREFIX}${reason}`, label);
45
+ }
46
+
47
+ /** 推入规则格式错误提示(加 ❌ 前缀区别于普通 warning) */
48
+ export function pushRuleError(msg: string): void {
49
+ _pushShared(`❌ shepherd 规则格式错误: ${msg}`);
50
+ }
51
+
52
+ /** shepherd 是否有待发送的提示 */
53
+ export function hasWarnings(): boolean {
54
+ return hasHints();
55
+ }