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/tsconfig.json CHANGED
@@ -1,14 +1,21 @@
1
- {
2
- "extends": "../tsconfig.base.json",
3
- "compilerOptions": {
4
- "strict": false,
5
- "outDir": "dist",
6
- "rootDir": ".",
7
- "paths": {
8
- "@pi-atelier/shared-utils": [
9
- "./node_modules/@pi-atelier/shared-utils/src/index.ts"
10
- ]
11
- }
12
- },
13
- "include": ["index.ts", "shepherd/**/*.ts", "tests/**/*.ts"]
14
- }
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "types": ["node"],
6
+ "outDir": "dist",
7
+ "rootDir": ".",
8
+ "paths": {
9
+ "@pi-atelier/shared-utils": [
10
+ "./node_modules/@pi-atelier/shared-utils/src/index.ts"
11
+ ],
12
+ "@earendil-works/pi-coding-agent": [
13
+ "../_sdk-link/dist/index.d.ts"
14
+ ],
15
+ "@pi-atelier/shepherd": [
16
+ "./shepherd/index.ts"
17
+ ]
18
+ }
19
+ },
20
+ "include": ["index.ts", "shepherd/**/*.ts", "tests/**/*.ts"]
21
+ }
@@ -1,182 +0,0 @@
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
@@ -1,182 +0,0 @@
1
- [English](README.en.md) | 程序中文文档
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 ── 解析 topic--kw1,kw2.md 文件名 │
20
- │ paths ── pi agent 标准路径常量 │
21
- │ settings ── settings.json 读写扩展配置段 │
22
- │ tool-output ── 工具输出截断(防上下文溢出) │
23
- │ agents ── 子代理定义文件发现 │
24
- │ ephemeral ── 会话临时 hint/label 栈 │
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
- > 这是 pi-atelier monorepo 内的 workspace 包,通常不需要单独安装。其他独立扩展通过 `bundledDependencies` 自动包含它。
37
-
38
- ## 导出模块
39
-
40
- ### 记忆文件解析 (`memory-parser`)
41
-
42
- 解析 `topic--kw1,kw2,kw3.md` 格式的记忆文件名,扫描目录生成索引。
43
-
44
- ```ts
45
- import { parseFileName, buildFileName, scanMemoryDir } from "@pi-atelier/shared-utils";
46
-
47
- // 解析文件名 → { topic, keywords }
48
- const { topic, keywords } = parseFileName("coding_standards--编码,git,lint.md");
49
- // topic = "coding_standards", keywords = ["编码", "git", "lint"]
50
-
51
- // 反向构建文件名
52
- const name = buildFileName("coding_standards", ["编码", "git", "lint"]);
53
- // "coding_standards--编码,git,lint.md"
54
-
55
- // 扫描目录,返回 MemoryEntry[]
56
- const entries = await scanMemoryDir("/path/to/memory");
57
- ```
58
-
59
- ### 路径常量 (`paths`)
60
-
61
- pi agent 标准路径,避免硬编码。
62
-
63
- | 常量 | 路径 | 说明 |
64
- |------|------|------|
65
- | `AGENT_DIR` | `~/.pi/agent/` | agent 根目录 |
66
- | `SETTINGS_PATH` | `~/.pi/agent/settings.json` | 全局设置 |
67
- | `MODELS_CONFIG_PATH` | `~/.pi/agent/models.json` | 模型配置 |
68
- | `MCP_CONFIG_PATH` | `~/.pi/agent/mcp.json` | MCP 服务器配置 |
69
- | `MCP_CACHE_PATH` | `~/.pi/agent/mcp-cache/` | MCP 工具缓存 |
70
- | `AGENTS_DIR` | `~/.pi/agent/agents/` | 子代理定义 |
71
- | `GLOBAL_RULES_PATH` | `~/.pi/agent/rules.md` | 全局规则 |
72
- | `MEMORY_DIR` | `~/.pi/agent/memory/` | 全局记忆 |
73
- | `MEMORY_MD_PATH` | `MEMORY.md` | 记忆索引文件名 |
74
-
75
- ### 设置管理 (`settings`)
76
-
77
- 读写 `settings.json` 中扩展的自定义配置段。
78
-
79
- ```ts
80
- import { getSettingsSection, patchSettingsSection, getSettingsValue, setSettingsValue } from "@pi-atelier/shared-utils";
81
-
82
- // 读取扩展配置段
83
- const config = await getSettingsSection("my-extension");
84
-
85
- // 增量更新配置
86
- await patchSettingsSection("my-extension", { enabled: true });
87
-
88
- // 读取/写入单个值
89
- const val = await getSettingsValue("my-extension", "key", "default");
90
- await setSettingsValue("my-extension", "key", "new-value");
91
- ```
92
-
93
- ### 工具输出截断 (`tool-output`)
94
-
95
- 防止工具返回超大结果撑爆 LLM 上下文。
96
-
97
- ```ts
98
- import { truncateToolOutput, truncatedResult, TOOL_OUTPUT_MAX_LINES } from "@pi-atelier/shared-utils";
99
-
100
- // 截断过长的输出
101
- const result = truncateToolOutput(longText, { maxLines: 200 });
102
- // { text: "...", truncated: true, originalLines: 1500, keptLines: 200 }
103
-
104
- // 快捷方式:返回 pi tool result 格式
105
- return truncatedResult(text); // 自动截断 + 返回 { content: [{ type: "text", text }] }
106
- ```
107
-
108
- ### 子代理发现 (`agents`)
109
-
110
- 扫描 `~/.pi/agent/agents/` 目录下的子代理定义文件。
111
-
112
- ```ts
113
- import { discoverAgents, getAgentDescription, formatAgentsList } from "@pi-atelier/shared-utils";
114
-
115
- // 发现所有可用子代理
116
- const agents = await discoverAgents();
117
- // [{ name: "pv-executor", description: "...", filePath: "..." }, ...]
118
-
119
- // 获取单个描述
120
- const desc = await getAgentDescription("pv-executor");
121
-
122
- // 格式化为可读列表
123
- const list = formatAgentsList(agents);
124
- ```
125
-
126
- ### 会话临时数据 (`ephemeral`)
127
-
128
- 当前会话的临时 hint/label 栈,会话结束即消失。用于跨工具调用的轻量状态传递。
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"); // 查看不移除
136
- const all = drainHints(); // 取出并清空
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
@@ -1,51 +0,0 @@
1
- {
2
- "name": "@pi-atelier/shared-utils",
3
- "version": "1.0.0",
4
- "description": "Shared utilities for pi-atelier extensions — settings, project config, memory parsing, tool output truncation, and more",
5
- "type": "module",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
8
- "exports": {
9
- ".": "./src/index.ts",
10
- "./*": "./src/*.ts"
11
- },
12
- "scripts": {
13
- "build": "tsup",
14
- "test": "vitest run",
15
- "lint": "biome check lib src index.ts",
16
- "typecheck": "tsc --noEmit"
17
- },
18
- "peerDependencies": {
19
- "@earendil-works/pi-coding-agent": ">=0.6.0"
20
- },
21
- "peerDependenciesMeta": {
22
- "@earendil-works/pi-coding-agent": {
23
- "optional": true
24
- }
25
- },
26
- "devDependencies": {
27
- "@biomejs/biome": "^2.4.15",
28
- "@vitest/coverage-v8": "^3.2.4",
29
- "tsup": "^8.0.0",
30
- "typescript": "^5.7.0",
31
- "vitest": "^3.0.0"
32
- },
33
- "tsup": {
34
- "entry": [
35
- "src/index.ts"
36
- ],
37
- "format": [
38
- "esm"
39
- ],
40
- "dts": true,
41
- "clean": true
42
- },
43
- "files": [
44
- "src/",
45
- "README.md",
46
- "README.en.md",
47
- "tsconfig.json",
48
- "vitest.config.ts",
49
- "package.json"
50
- ]
51
- }
@@ -1,120 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- // vi.mock factory is hoisted, use vi.hoisted to declare variables at the hoisted position
4
- const mockFs = vi.hoisted(() => ({
5
- existsSync: vi.fn(),
6
- readdirSync: vi.fn(),
7
- readFileSync: vi.fn(),
8
- }));
9
-
10
- vi.mock("node:fs", () => mockFs);
11
-
12
- import { discoverAgents, getAgentDescription, formatAgentsList } from "../agents";
13
-
14
- beforeEach(() => {
15
- vi.clearAllMocks();
16
- });
17
-
18
- describe("discoverAgents", () => {
19
- it("returns empty array when AGENTS_DIR does not exist", () => {
20
- mockFs.existsSync.mockReturnValue(false);
21
- expect(discoverAgents()).toEqual([]);
22
- });
23
-
24
- it("filters .md files and strips leading underscore", () => {
25
- mockFs.existsSync.mockReturnValue(true);
26
- mockFs.readdirSync.mockReturnValue([
27
- "coder.md",
28
- "_private.md",
29
- "reviewer.md",
30
- "readme.txt",
31
- "notes.md",
32
- ]);
33
- const result = discoverAgents();
34
- expect(result).toEqual(["coder", "reviewer", "notes"]);
35
- });
36
-
37
- it("returns empty when only non-md files exist", () => {
38
- mockFs.existsSync.mockReturnValue(true);
39
- mockFs.readdirSync.mockReturnValue(["file.txt", "file.json"]);
40
- expect(discoverAgents()).toEqual([]);
41
- });
42
-
43
- it("returns empty when only underscored md files exist", () => {
44
- mockFs.existsSync.mockReturnValue(true);
45
- mockFs.readdirSync.mockReturnValue(["_private.md", "_template.md"]);
46
- expect(discoverAgents()).toEqual([]);
47
- });
48
- });
49
-
50
- describe("getAgentDescription", () => {
51
- it("extracts description from frontmatter", () => {
52
- const content = [
53
- "---",
54
- "description: 代码审查助手",
55
- "version: 1.0",
56
- "---",
57
- "# Coder Agent",
58
- "Some content",
59
- ].join("\n");
60
- mockFs.readFileSync.mockReturnValue(content);
61
- const result = getAgentDescription("coder");
62
- expect(result).toBe("代码审查助手");
63
- });
64
-
65
- it("uses default description when no frontmatter", () => {
66
- mockFs.readFileSync.mockReturnValue("plain content without frontmatter");
67
- const result = getAgentDescription("coder");
68
- expect(result).toBe("read, grep, find, ls");
69
- });
70
-
71
- it("uses default description when file read fails", () => {
72
- mockFs.readFileSync.mockImplementation(() => {
73
- throw new Error("ENOENT");
74
- });
75
- const result = getAgentDescription("nonexistent");
76
- expect(result).toBe("read, grep, find, ls");
77
- });
78
-
79
- it("uses default description when frontmatter has no description field", () => {
80
- const content = [
81
- "---",
82
- "title: Agent",
83
- "---",
84
- "# Content",
85
- ].join("\n");
86
- mockFs.readFileSync.mockReturnValue(content);
87
- const result = getAgentDescription("agent");
88
- expect(result).toBe("read, grep, find, ls");
89
- });
90
-
91
- it("handles description with trailing whitespace", () => {
92
- const content = [
93
- "---",
94
- "description: 帮我写代码 ",
95
- "---",
96
- ].join("\n");
97
- mockFs.readFileSync.mockReturnValue(content);
98
- const result = getAgentDescription("agent");
99
- expect(result).toBe("帮我写代码");
100
- });
101
- });
102
-
103
- describe("formatAgentsList", () => {
104
- it("returns placeholder when no agents", () => {
105
- mockFs.existsSync.mockReturnValue(false);
106
- expect(formatAgentsList()).toBe("(无可用子代理)");
107
- });
108
-
109
- it("formats agent list with descriptions", () => {
110
- mockFs.existsSync.mockReturnValue(true);
111
- mockFs.readdirSync.mockReturnValue(["coder.md", "reviewer.md"]);
112
- mockFs.readFileSync.mockImplementation((filePath: string) => {
113
- if (filePath.includes("coder")) return "---\ndescription: Coder助手\n---\n";
114
- if (filePath.includes("reviewer")) return "---\ndescription: Reviewer助手\n---\n";
115
- return "";
116
- });
117
- const result = formatAgentsList();
118
- expect(result).toBe("- **coder**: Coder助手\n- **reviewer**: Reviewer助手");
119
- });
120
- });
@@ -1,100 +0,0 @@
1
- /**
2
- * ephemeral.ts 测试
3
- *
4
- * 模块内 _hints/_labels 是模块级变量,每次 import 可能返回缓存模块。
5
- * 使用 vi.resetModules() 确保每个测试得到干净的模块状态。
6
- */
7
- import { describe, it, expect, vi, beforeEach } from "vitest";
8
-
9
- function importEphemeral() {
10
- return import("../ephemeral");
11
- }
12
-
13
- beforeEach(() => {
14
- vi.resetModules();
15
- });
16
-
17
- describe("ephemeral hints lifecycle", () => {
18
- it("starts empty: hasHints returns false", async () => {
19
- const { hasHints } = await importEphemeral();
20
- expect(hasHints()).toBe(false);
21
- });
22
-
23
- it("peekHints returns null when empty", async () => {
24
- const { peekHints } = await importEphemeral();
25
- expect(peekHints()).toBeNull();
26
- });
27
-
28
- it("drainHints returns null when empty", async () => {
29
- const { drainHints } = await importEphemeral();
30
- expect(drainHints()).toBeNull();
31
- });
32
-
33
- it("peekLabels returns empty array when empty", async () => {
34
- const { peekLabels } = await importEphemeral();
35
- expect(peekLabels()).toEqual([]);
36
- });
37
- });
38
-
39
- describe("pushHint and peek", () => {
40
- it("pushHint adds hint, peekHints returns it without consuming", async () => {
41
- const mod = await importEphemeral();
42
- mod.pushHint("提示A");
43
- expect(mod.hasHints()).toBe(true);
44
- expect(mod.peekHints()).toBe("提示A");
45
- // peek does not consume
46
- expect(mod.peekHints()).toBe("提示A");
47
- });
48
-
49
- it("pushHint with label adds label to peekLabels", async () => {
50
- const mod = await importEphemeral();
51
- mod.pushHint("提示B", "label-b");
52
- expect(mod.peekLabels()).toEqual(["label-b"]);
53
- expect(mod.peekHints()).toBe("提示B");
54
- });
55
-
56
- it("multiple hints joined by double newline", async () => {
57
- const mod = await importEphemeral();
58
- mod.pushHint("first");
59
- mod.pushHint("second");
60
- expect(mod.peekHints()).toBe("first\n\nsecond");
61
- });
62
- });
63
-
64
- describe("drainHints", () => {
65
- it("drainHints returns hints and clears state", async () => {
66
- const mod = await importEphemeral();
67
- mod.pushHint("hint1", "lbl1");
68
- mod.pushHint("hint2", "lbl2");
69
- const result = mod.drainHints();
70
- expect(result).toBe("hint1\n\nhint2");
71
- // after drain, state is cleared
72
- expect(mod.hasHints()).toBe(false);
73
- expect(mod.peekHints()).toBeNull();
74
- expect(mod.peekLabels()).toEqual([]);
75
- });
76
-
77
- it("labels are cleared after drain", async () => {
78
- const mod = await importEphemeral();
79
- mod.pushHint("test", "mylabel");
80
- mod.drainHints();
81
- expect(mod.peekLabels()).toEqual([]);
82
- });
83
- });
84
-
85
- describe("multiple push then drain", () => {
86
- it("push 3 hints without labels", async () => {
87
- const mod = await importEphemeral();
88
- mod.pushHint("a");
89
- mod.pushHint("b");
90
- mod.pushHint("c");
91
- expect(mod.drainHints()).toBe("a\n\nb\n\nc");
92
- });
93
-
94
- it("push with labels, peekLabels returns all labels", async () => {
95
- const mod = await importEphemeral();
96
- mod.pushHint("x", "l1");
97
- mod.pushHint("y", "l2");
98
- expect(mod.peekLabels()).toEqual(["l1", "l2"]);
99
- });
100
- });