pi-soly 1.3.0 → 1.4.1
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.md +6 -30
- package/commands.ts +2 -2
- package/index.ts +1 -27
- package/package.json +1 -2
- package/skills/soly-framework/SKILL.md +24 -97
- package/switch/README.md +0 -104
- package/switch/core.ts +0 -229
- package/switch/index.ts +0 -365
- package/switch/package.json +0 -52
- package/switch/prompt.ts +0 -131
- package/switch/tests/core.test.ts +0 -210
- package/switch/tests/index.test.ts +0 -48
- package/switch/tests/prompt.test.ts +0 -108
- package/switch/tests/watcher.test.ts +0 -147
- package/switch/tsconfig.json +0 -28
- package/switch/watcher.ts +0 -112
package/switch/prompt.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// prompt.ts — System-prompt section for the pi-switch extension
|
|
3
|
-
// =============================================================================
|
|
4
|
-
|
|
5
|
-
/** Task-pattern → recommended agent. The LLM reads this and decides
|
|
6
|
-
* whether to invoke /agent before launching subagent(...). Match is
|
|
7
|
-
* by keyword (case-insensitive). First match wins; ties broken by order. */
|
|
8
|
-
export const TASK_AGENT_HINTS: ReadonlyArray<{
|
|
9
|
-
pattern: RegExp;
|
|
10
|
-
agent: string;
|
|
11
|
-
emoji: string;
|
|
12
|
-
why: string;
|
|
13
|
-
}> = [
|
|
14
|
-
// English keywords (use \b — ASCII word boundary works for English)
|
|
15
|
-
{ pattern: /\b(research|investigate|look\s*up|find\s*out|explore|survey|compare\s+libraries|what\s+is\s+the\s+best)\b/i,
|
|
16
|
-
agent: "researcher", emoji: "\ud83d\udcda",
|
|
17
|
-
why: "external docs, ecosystem behavior, primary sources" },
|
|
18
|
-
{ pattern: /\b(scout|scan|map|find\s+all|where\s+is|locate|explore\s+codebase|skim)\b/i,
|
|
19
|
-
agent: "scout", emoji: "\ud83d\udd0d",
|
|
20
|
-
why: "codebase recon, patterns, file locations" },
|
|
21
|
-
{ pattern: /\b(plan|design|architect|outline|structure|break\s*down|steps|order)\b/i,
|
|
22
|
-
agent: "worker", emoji: "\ud83d\udccb",
|
|
23
|
-
why: "decompose into ordered steps, identify risks" },
|
|
24
|
-
{ pattern: /\b(review|audit|check|adversarial|critique|find\s+bugs|qa)\b/i,
|
|
25
|
-
agent: "reviewer", emoji: "\ud83d\udc40",
|
|
26
|
-
why: "adversarial review of correctness, security, style" },
|
|
27
|
-
{ pattern: /\b(oracle|decision|tradeoff|compare|which\s+approach|is\s+this\s+wise|drift)\b/i,
|
|
28
|
-
agent: "oracle", emoji: "\ud83d\udd2e",
|
|
29
|
-
why: "decision consistency, hidden assumptions, drift detection" },
|
|
30
|
-
{ pattern: /\b(debug|bug|fix|crash|error|stack\s*trace|repro|why\s+is\s+this\s+broken)\b/i,
|
|
31
|
-
agent: "worker", emoji: "\ud83d\udc1e",
|
|
32
|
-
why: "isolated bug investigation with minimal repro" },
|
|
33
|
-
{ pattern: /\b(test|tests|coverage|spec|assert)\b/i,
|
|
34
|
-
agent: "worker", emoji: "\ud83e\uddea",
|
|
35
|
-
why: "test-only work, never modifies prod code" },
|
|
36
|
-
{ pattern: /\b(refactor|clean\s*up|simplify|extract|rename|restructure|no\s+behavior\s+change)\b/i,
|
|
37
|
-
agent: "worker", emoji: "\ud83d\udd04",
|
|
38
|
-
why: "pure refactoring, behavior-preserving" },
|
|
39
|
-
{ pattern: /\b(document|docs|readme|jsdoc|comment|annotate)\b/i,
|
|
40
|
-
agent: "worker", emoji: "\ud83d\udcdd",
|
|
41
|
-
why: "doc updates, READMEs, inline annotations" },
|
|
42
|
-
{ pattern: /\b(implement|build|write\s+code|add\s+feature|create\s+the)\b/i,
|
|
43
|
-
agent: "worker", emoji: "\u26a1",
|
|
44
|
-
why: "generic implementation with all tools" },
|
|
45
|
-
{ pattern: /\b(orchestrate|coordinate|dispatch|chain|run\s+in\s+parallel|first\s+.+\s+then)\b/i,
|
|
46
|
-
agent: "worker", emoji: "\ud83e\udd1d",
|
|
47
|
-
why: "multi-agent orchestration" },
|
|
48
|
-
// Russian keywords (loose match — Russian words inflect heavily; we match
|
|
49
|
-
// word stems, accepting some false positives as the cost of broader coverage)
|
|
50
|
-
{ pattern: /(изуч|исслед|разузн|найди\s+инфу|research|investigate|find\s+out)/i,
|
|
51
|
-
agent: "researcher", emoji: "\ud83d\udcda",
|
|
52
|
-
why: "external docs, ecosystem behavior, primary sources" },
|
|
53
|
-
{ pattern: /(где\s+это|где\s+находит|find\s+all|locate)/i,
|
|
54
|
-
agent: "scout", emoji: "\ud83d\udd0d",
|
|
55
|
-
why: "codebase recon, patterns, file locations" },
|
|
56
|
-
{ pattern: /(спланир|plan|design|architect)/i,
|
|
57
|
-
agent: "worker", emoji: "\ud83d\udccb",
|
|
58
|
-
why: "decompose into ordered steps, identify risks" },
|
|
59
|
-
{ pattern: /(проверь|ревью|аудит|review|audit)/i,
|
|
60
|
-
agent: "reviewer", emoji: "\ud83d\udc40",
|
|
61
|
-
why: "adversarial review of correctness, security, style" },
|
|
62
|
-
{ pattern: /(решени|выбор|decision|tradeoff|drift)/i,
|
|
63
|
-
agent: "oracle", emoji: "\ud83d\udd2e",
|
|
64
|
-
why: "decision consistency, hidden assumptions, drift detection" },
|
|
65
|
-
{ pattern: /(баг|ошибк|почему\s+(?:падает|ломает)|debug|bug|crash|stack\s*trace|repro)/i,
|
|
66
|
-
agent: "worker", emoji: "\ud83d\udc1e",
|
|
67
|
-
why: "isolated bug investigation with minimal repro" },
|
|
68
|
-
{ pattern: /(тест|покрыт|test|coverage|spec|assert)/i,
|
|
69
|
-
agent: "worker", emoji: "\ud83e\uddea",
|
|
70
|
-
why: "test-only work, never modifies prod code" },
|
|
71
|
-
{ pattern: /(рефактор|упрост|refactor|simplify|extract|restructure)/i,
|
|
72
|
-
agent: "worker", emoji: "\ud83d\udd04",
|
|
73
|
-
why: "pure refactoring, behavior-preserving" },
|
|
74
|
-
{ pattern: /(документ|описани|document|readme|jsdoc)/i,
|
|
75
|
-
agent: "worker", emoji: "\ud83d\udcdd",
|
|
76
|
-
why: "doc updates, READMEs, inline annotations" },
|
|
77
|
-
{ pattern: /(реализуй|сделай|напиши|создай|implement|build|add\s+feature|create\s+the)/i,
|
|
78
|
-
agent: "worker", emoji: "\u26a1",
|
|
79
|
-
why: "generic implementation with all tools" },
|
|
80
|
-
{ pattern: /(оркестрируй|координируй|orchestrate|coordinate|dispatch|chain)/i,
|
|
81
|
-
agent: "worker", emoji: "\ud83e\udd1d",
|
|
82
|
-
why: "multi-agent orchestration" },
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
/** Heuristic: which agent does the task look like? Returns null if no
|
|
86
|
-
* pattern matches (caller should leave the current agent as-is). */
|
|
87
|
-
export function recommendAgent(taskText: string): { agent: string; emoji: string; why: string } | null {
|
|
88
|
-
for (const hint of TASK_AGENT_HINTS) {
|
|
89
|
-
if (hint.pattern.test(taskText)) {
|
|
90
|
-
return { agent: hint.agent, emoji: hint.emoji, why: hint.why };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function buildPiSwitchSection(): string {
|
|
97
|
-
return `
|
|
98
|
-
|
|
99
|
-
## pi-switch — when to use \`/agent\`
|
|
100
|
-
|
|
101
|
-
The \`/agent\` slash command + \`Ctrl+Tab\` shortcut cycle through 4 built-in cycle agents. Use the right one for the job:
|
|
102
|
-
|
|
103
|
-
- **Read-only / no edits** (oracle, scout, reviewer): for analysis, planning, review. They won't modify files.
|
|
104
|
-
- **Write tools** (worker): for implementation.
|
|
105
|
-
- **User-defined** in \`~/.pi/agent/agents/\`: any agent the user has added — drop a markdown file with YAML frontmatter (name, description) and it joins the cycle automatically.
|
|
106
|
-
|
|
107
|
-
The current agent is shown in the footer status line as \`[emoji name]\`.
|
|
108
|
-
|
|
109
|
-
When you need a specialist for a sub-task, use the right agent via the parent LLM's \`subagent(...)\` call.
|
|
110
|
-
|
|
111
|
-
**No soly subagent.** As of 1.3.0, soly no longer ships a subagent. The LLM in the main session executes plans directly using the slash commands (\`/plan\`, \`/execute\`, etc.) and the \`soly-framework\` skill. Use pi's built-in subagents (\`worker\`, \`oracle\`, \`scout\`, \`reviewer\`) for read-only research.
|
|
112
|
-
|
|
113
|
-
**Task → agent heuristics.** Before launching a generic \`subagent(...)\`, scan the request for these keywords:
|
|
114
|
-
|
|
115
|
-
| Keywords in request | Suggested agent | Why |
|
|
116
|
-
|---|---|---|
|
|
117
|
-
| scout, scan, map, find all, where is, locate, explore codebase, skim | 🔍 scout | codebase recon, patterns, file locations |
|
|
118
|
-
| review, audit, check, adversarial, critique, find bugs, qa | 👀 reviewer | adversarial correctness, security, style review |
|
|
119
|
-
| oracle, decision, tradeoff, compare, which approach, is this wise, drift | 🔮 oracle | decision consistency, hidden assumptions |
|
|
120
|
-
| (anything else, including implement, debug, fix, test, refactor, document, plan) | ⚡ worker | generic implementation, all tools — prefer to do it yourself |
|
|
121
|
-
|
|
122
|
-
For multi-step tasks, the orchestrator (you) decides which agents run and in what order. You can chain agents via \`subagent({ chain: [...] })\` or run them in parallel via parallel tasks.
|
|
123
|
-
|
|
124
|
-
DON'T:
|
|
125
|
-
- Launch a worker for analysis (use oracle/scout/reviewer)
|
|
126
|
-
- Launch an oracle for implementation (it has no write tools)
|
|
127
|
-
- Spawn \`soly-manager\` / \`soly-worker\` / etc. — there are no soly subagents anymore (as of 1.3.0)
|
|
128
|
-
- Spawn soly-worker / soly-debugger / soly-tester — there is only \`soly-manager\`
|
|
129
|
-
- Manually edit \`.soly/agent\` or \`~/.pi-switch/agent\` — use the slash command
|
|
130
|
-
`;
|
|
131
|
-
}
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// tests/core.test.ts — Tests for pi-switch core
|
|
3
|
-
// =============================================================================
|
|
4
|
-
|
|
5
|
-
/// <reference types="bun-types" />
|
|
6
|
-
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
|
-
import * as os from "node:os";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
DEFAULT_ROTOR,
|
|
12
|
-
BUILTIN_ROTORS,
|
|
13
|
-
ROTOR_META,
|
|
14
|
-
getRotorMeta,
|
|
15
|
-
isValidRotorName,
|
|
16
|
-
discoverUserRotors,
|
|
17
|
-
availableAgents,
|
|
18
|
-
nextAgent,
|
|
19
|
-
parseRotorName,
|
|
20
|
-
formatAgentBadge,
|
|
21
|
-
formatRotorSwitchNotify,
|
|
22
|
-
formatHeaderLine,
|
|
23
|
-
groupedAvailableRotors,
|
|
24
|
-
agentFilePath,
|
|
25
|
-
loadAgent,
|
|
26
|
-
saveAgent,
|
|
27
|
-
} from "../core.js";
|
|
28
|
-
|
|
29
|
-
describe("DEFAULT_ROTOR", () => {
|
|
30
|
-
test("is 'worker'", () => {
|
|
31
|
-
expect(DEFAULT_ROTOR).toBe("worker");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("isValidRotorName", () => {
|
|
36
|
-
test("accepts simple names", () => {
|
|
37
|
-
expect(isValidRotorName("worker")).toBe(true);
|
|
38
|
-
expect(isValidRotorName("my_agent")).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
test("rejects invalid", () => {
|
|
41
|
-
expect(isValidRotorName("with space")).toBe(false);
|
|
42
|
-
expect(isValidRotorName("")).toBe(false);
|
|
43
|
-
expect(isValidRotorName("a".repeat(65))).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("ROTOR_META", () => {
|
|
48
|
-
test("every built-in has metadata", () => {
|
|
49
|
-
for (const a of BUILTIN_ROTORS) {
|
|
50
|
-
expect(ROTOR_META[a]).toBeDefined();
|
|
51
|
-
expect(ROTOR_META[a]!.emoji.length).toBeGreaterThan(0);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
test("meta has writesFiles flag", () => {
|
|
55
|
-
expect(ROTOR_META.worker!.writesFiles).toBe(true);
|
|
56
|
-
expect(ROTOR_META.oracle!.writesFiles).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("getRotorMeta", () => {
|
|
61
|
-
test("returns fallback for unknown", () => {
|
|
62
|
-
const m = getRotorMeta("zzz");
|
|
63
|
-
expect(m.emoji.length).toBeGreaterThan(0);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("nextAgent", () => {
|
|
68
|
-
test("cycles forward", () => {
|
|
69
|
-
expect(nextAgent("a", ["a", "b", "c"])).toBe("b");
|
|
70
|
-
expect(nextAgent("c", ["a", "b", "c"])).toBe("a");
|
|
71
|
-
});
|
|
72
|
-
test("returns first if current not in cycle", () => {
|
|
73
|
-
expect(nextAgent("zzz", ["a", "b"])).toBe("a");
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("parseRotorName", () => {
|
|
78
|
-
test("trims and validates", () => {
|
|
79
|
-
expect(parseRotorName(" oracle ")).toBe("oracle");
|
|
80
|
-
expect(parseRotorName("with space")).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("formatAgentBadge", () => {
|
|
85
|
-
test("null for default", () => {
|
|
86
|
-
expect(formatAgentBadge(DEFAULT_ROTOR)).toBeNull();
|
|
87
|
-
});
|
|
88
|
-
test("emoji + name for non-default", () => {
|
|
89
|
-
const b = formatAgentBadge("oracle");
|
|
90
|
-
expect(b).toContain("oracle");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("formatRotorSwitchNotify", () => {
|
|
95
|
-
test("multi-line: old → new + capability", () => {
|
|
96
|
-
const out = formatRotorSwitchNotify("worker", "oracle");
|
|
97
|
-
expect(out).toContain("pi-switch agent changed");
|
|
98
|
-
expect(out).toContain("worker");
|
|
99
|
-
expect(out).toContain("oracle");
|
|
100
|
-
expect(out).toContain("writes files: no");
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("formatHeaderLine", () => {
|
|
105
|
-
test("always non-empty (even for default)", () => {
|
|
106
|
-
const h = formatHeaderLine("worker");
|
|
107
|
-
expect(h).toContain("worker");
|
|
108
|
-
expect(h).toContain("Ctrl+Tab");
|
|
109
|
-
});
|
|
110
|
-
test("includes read-only tag when applicable", () => {
|
|
111
|
-
const h = formatHeaderLine("oracle");
|
|
112
|
-
expect(h).toContain("read-only");
|
|
113
|
-
});
|
|
114
|
-
test("omits read-only tag when agent writes", () => {
|
|
115
|
-
const h = formatHeaderLine("worker");
|
|
116
|
-
expect(h).not.toContain("read-only");
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("groupedAvailableRotors", () => {
|
|
121
|
-
test("includes built-in group", () => {
|
|
122
|
-
const groups = groupedAvailableRotors("/nonexistent");
|
|
123
|
-
expect(groups[0]?.header).toBe("built-in");
|
|
124
|
-
});
|
|
125
|
-
test("includes user group when present", () => {
|
|
126
|
-
// Use HOME override so the new ~.agents/ scan picks up our fixture
|
|
127
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
128
|
-
const prevHome = process.env.HOME;
|
|
129
|
-
const prevUserProfile = process.env.USERPROFILE;
|
|
130
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pis-home-"));
|
|
131
|
-
process.env.HOME = tmp;
|
|
132
|
-
process.env.USERPROFILE = tmp;
|
|
133
|
-
const agentsDir = path.join(tmp, ".agents");
|
|
134
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
135
|
-
fs.writeFileSync(path.join(agentsDir, "my.md"), "---\nname: my-helper\n---\n# body\n");
|
|
136
|
-
const groups = groupedAvailableRotors();
|
|
137
|
-
const userGroup = groups.find((g) => g.header === "user-defined");
|
|
138
|
-
expect(userGroup?.agents).toContain("my-helper");
|
|
139
|
-
// restore
|
|
140
|
-
process.env.HOME = prevHome ?? home;
|
|
141
|
-
process.env.USERPROFILE = prevUserProfile ?? home;
|
|
142
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("includes project agent when present (cwd scope)", () => {
|
|
146
|
-
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "pis-proj-"));
|
|
147
|
-
const agentsDir = path.join(projectDir, ".agents");
|
|
148
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
149
|
-
fs.writeFileSync(path.join(agentsDir, "proj.md"), "---\nname: project-helper\n---\n# body\n");
|
|
150
|
-
const groups = groupedAvailableRotors(projectDir);
|
|
151
|
-
const userGroup = groups.find((g) => g.header === "user-defined");
|
|
152
|
-
expect(userGroup?.agents).toContain("project-helper");
|
|
153
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// Persistence
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
describe("agentFilePath", () => {
|
|
162
|
-
test("prefers .soly/agent when soly dir exists", () => {
|
|
163
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "pis-path-"));
|
|
164
|
-
fs.mkdirSync(path.join(cwd, ".soly"), { recursive: true });
|
|
165
|
-
expect(agentFilePath(cwd)).toBe(path.join(cwd, ".soly", "agent"));
|
|
166
|
-
fs.rmSync(cwd, { recursive: true, force: true });
|
|
167
|
-
});
|
|
168
|
-
test("falls back to ~/.pi-switch/agent when no soly", () => {
|
|
169
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "pis-fb-"));
|
|
170
|
-
// Ensure no .soly in cwd
|
|
171
|
-
expect(agentFilePath(cwd)).toContain(".pi-switch");
|
|
172
|
-
fs.rmSync(cwd, { recursive: true, force: true });
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe("loadAgent / saveAgent", () => {
|
|
177
|
-
let tmpCwd: string;
|
|
178
|
-
let origHome: string | undefined;
|
|
179
|
-
beforeAll(() => {
|
|
180
|
-
tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), "pis-rt-"));
|
|
181
|
-
origHome = process.env.HOME;
|
|
182
|
-
process.env.HOME = tmpCwd;
|
|
183
|
-
});
|
|
184
|
-
afterAll(() => {
|
|
185
|
-
if (origHome) process.env.HOME = origHome;
|
|
186
|
-
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("round-trip", () => {
|
|
190
|
-
saveAgent(tmpCwd, "oracle");
|
|
191
|
-
expect(loadAgent(tmpCwd)).toBe("oracle");
|
|
192
|
-
});
|
|
193
|
-
test("returns null when file missing", () => {
|
|
194
|
-
const freshHome = fs.mkdtempSync(path.join(os.tmpdir(), "pis-fresh-"));
|
|
195
|
-
fs.mkdirSync(path.join(freshHome, ".pi-switch"), { recursive: true });
|
|
196
|
-
const prevHome = process.env.HOME;
|
|
197
|
-
process.env.HOME = freshHome;
|
|
198
|
-
try {
|
|
199
|
-
expect(loadAgent("/anywhere")).toBeNull();
|
|
200
|
-
} finally {
|
|
201
|
-
process.env.HOME = prevHome;
|
|
202
|
-
}
|
|
203
|
-
fs.rmSync(freshHome, { recursive: true, force: true });
|
|
204
|
-
});
|
|
205
|
-
test("rejects invalid name on load", () => {
|
|
206
|
-
const file = agentFilePath(tmpCwd);
|
|
207
|
-
fs.writeFileSync(file, "with space\n", "utf-8");
|
|
208
|
-
expect(loadAgent(tmpCwd)).toBeNull();
|
|
209
|
-
});
|
|
210
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// tests/index.test.ts — Regression tests for index.ts handlers
|
|
3
|
-
// =============================================================================
|
|
4
|
-
//
|
|
5
|
-
// These exercise the agent-command dispatch without spinning up a real
|
|
6
|
-
// pi session. We test the parse logic by feeding the same input strings
|
|
7
|
-
// through a hand-rolled mock and inspecting the notify/output side-effects.
|
|
8
|
-
|
|
9
|
-
/// <reference types="bun-types" />
|
|
10
|
-
import { describe, test, expect } from "bun:test";
|
|
11
|
-
import { availableAgents } from "../core.js";
|
|
12
|
-
|
|
13
|
-
describe("/agent handler parse logic (regression for `/agent researcher` bug)", () => {
|
|
14
|
-
// The original bug: `/agent researcher` was interpreted as "show list"
|
|
15
|
-
// instead of "set agent to researcher" because the parser only checked
|
|
16
|
-
// the SECOND token, not the first. (After cycle reduction, `researcher`
|
|
17
|
-
// is no longer a built-in, so we use `oracle` as the example agent.)
|
|
18
|
-
test("single-arg '/agent <name>' is a set, not a list", () => {
|
|
19
|
-
const input = "oracle";
|
|
20
|
-
const parts = input.trim().split(/\s+/);
|
|
21
|
-
const subcommand = parts[0]?.toLowerCase();
|
|
22
|
-
const cycle = availableAgents();
|
|
23
|
-
// Single known agent → set, not show
|
|
24
|
-
expect(parts.length).toBe(1);
|
|
25
|
-
expect(cycle.includes(subcommand ?? "")).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
test("'/agent create <name>' → create subcommand", () => {
|
|
28
|
-
const input = "create my-debugger";
|
|
29
|
-
const subcommand = input.trim().split(/\s+/)[0];
|
|
30
|
-
expect(subcommand).toBe("create");
|
|
31
|
-
});
|
|
32
|
-
test("'/agent doctor' → doctor subcommand", () => {
|
|
33
|
-
const input = "doctor";
|
|
34
|
-
const subcommand = input.trim().split(/\s+/)[0];
|
|
35
|
-
expect(subcommand).toBe("doctor");
|
|
36
|
-
});
|
|
37
|
-
test("'/agent recommend <task>' → recommend subcommand", () => {
|
|
38
|
-
const input = "recommend investigate the bug";
|
|
39
|
-
const subcommand = input.trim().split(/\s+/)[0];
|
|
40
|
-
expect(subcommand).toBe("recommend");
|
|
41
|
-
});
|
|
42
|
-
test("'/agent <unknown>' falls through to error (NOT set)", () => {
|
|
43
|
-
const input = "nonexistent-agent-xyz";
|
|
44
|
-
const subcommand = input.trim().split(/\s+/)[0];
|
|
45
|
-
const cycle = availableAgents();
|
|
46
|
-
expect(cycle.includes(subcommand ?? "")).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// tests/prompt.test.ts — Tests for the system-prompt section + recommendAgent
|
|
3
|
-
// =============================================================================
|
|
4
|
-
|
|
5
|
-
/// <reference types="bun-types" />
|
|
6
|
-
import { describe, test, expect } from "bun:test";
|
|
7
|
-
import { buildPiSwitchSection, recommendAgent, TASK_AGENT_HINTS } from "../prompt.js";
|
|
8
|
-
|
|
9
|
-
describe("buildPiSwitchSection", () => {
|
|
10
|
-
const s = buildPiSwitchSection();
|
|
11
|
-
|
|
12
|
-
test("starts with header", () => {
|
|
13
|
-
expect(s.trim().startsWith("## pi-switch")).toBe(true);
|
|
14
|
-
});
|
|
15
|
-
test("mentions /agent command and Ctrl+Tab", () => {
|
|
16
|
-
expect(s).toContain("/agent");
|
|
17
|
-
expect(s).toContain("Ctrl+Tab");
|
|
18
|
-
});
|
|
19
|
-
test("explains built-in categories", () => {
|
|
20
|
-
expect(s).toContain("oracle");
|
|
21
|
-
expect(s).toContain("scout");
|
|
22
|
-
expect(s).toContain("worker");
|
|
23
|
-
});
|
|
24
|
-
test("mentions worker as the main cycle rotor", () => {
|
|
25
|
-
expect(s).toContain("worker");
|
|
26
|
-
});
|
|
27
|
-
test("explains user-defined", () => {
|
|
28
|
-
expect(s).toMatch(/user[- ]?defined/i);
|
|
29
|
-
expect(s).toContain("~/.pi/agent/agents/");
|
|
30
|
-
});
|
|
31
|
-
test("has anti-patterns", () => {
|
|
32
|
-
expect(s).toContain("DON");
|
|
33
|
-
});
|
|
34
|
-
test("includes task→agent heuristics table", () => {
|
|
35
|
-
expect(s).toContain("debug");
|
|
36
|
-
expect(s).toContain("refactor");
|
|
37
|
-
});
|
|
38
|
-
test("is reasonably short (< 5KB)", () => {
|
|
39
|
-
expect(s.length).toBeLessThan(5000);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("TASK_AGENT_HINTS", () => {
|
|
44
|
-
test("every hint has required fields", () => {
|
|
45
|
-
for (const h of TASK_AGENT_HINTS) {
|
|
46
|
-
expect(h.agent.length).toBeGreaterThan(0);
|
|
47
|
-
expect(h.emoji.length).toBeGreaterThan(0);
|
|
48
|
-
expect(h.why.length).toBeGreaterThan(5);
|
|
49
|
-
expect(h.pattern).toBeInstanceOf(RegExp);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("recommendAgent", () => {
|
|
55
|
-
test("research keywords → researcher (English)", () => {
|
|
56
|
-
expect(recommendAgent("look up the latest pi-subagents API")?.agent).toBe("researcher");
|
|
57
|
-
expect(recommendAgent("what is the best lib for X?")?.agent).toBe("researcher");
|
|
58
|
-
});
|
|
59
|
-
test("research keywords → researcher (Russian)", () => {
|
|
60
|
-
expect(recommendAgent("Изучи React Server Components")?.agent).toBe("researcher");
|
|
61
|
-
expect(recommendAgent("Найди инфу про Zustand")?.agent).toBe("researcher");
|
|
62
|
-
});
|
|
63
|
-
test("debug keywords → worker", () => {
|
|
64
|
-
expect(recommendAgent("fix this bug")?.agent).toBe("worker");
|
|
65
|
-
expect(recommendAgent("why is this crash happening")?.agent).toBe("worker");
|
|
66
|
-
expect(recommendAgent("repro the failing test")?.agent).toBe("worker");
|
|
67
|
-
expect(recommendAgent("Почему падает тест?")?.agent).toBe("worker");
|
|
68
|
-
});
|
|
69
|
-
test("refactor keywords → worker", () => {
|
|
70
|
-
expect(recommendAgent("refactor this function")?.agent).toBe("worker");
|
|
71
|
-
expect(recommendAgent("simplify the auth flow")?.agent).toBe("worker");
|
|
72
|
-
expect(recommendAgent("Упрости эту функцию")?.agent).toBe("worker");
|
|
73
|
-
});
|
|
74
|
-
test("test keywords → worker", () => {
|
|
75
|
-
expect(recommendAgent("write tests for the parser")?.agent).toBe("worker");
|
|
76
|
-
expect(recommendAgent("improve coverage")?.agent).toBe("worker");
|
|
77
|
-
expect(recommendAgent("Напиши тесты для парсера")?.agent).toBe("worker");
|
|
78
|
-
});
|
|
79
|
-
test("review keywords → reviewer", () => {
|
|
80
|
-
expect(recommendAgent("review this PR")?.agent).toBe("reviewer");
|
|
81
|
-
expect(recommendAgent("audit the security")?.agent).toBe("reviewer");
|
|
82
|
-
expect(recommendAgent("Проверь этот код")?.agent).toBe("reviewer");
|
|
83
|
-
});
|
|
84
|
-
test("docs keywords → worker", () => {
|
|
85
|
-
expect(recommendAgent("update the readme")?.agent).toBe("worker");
|
|
86
|
-
expect(recommendAgent("add jsdoc to the function")?.agent).toBe("worker");
|
|
87
|
-
expect(recommendAgent("Обнови документацию")?.agent).toBe("worker");
|
|
88
|
-
});
|
|
89
|
-
test("plan keywords → worker", () => {
|
|
90
|
-
expect(recommendAgent("plan the migration")?.agent).toBe("worker");
|
|
91
|
-
expect(recommendAgent("design the API")?.agent).toBe("worker");
|
|
92
|
-
expect(recommendAgent("Спланируй миграцию")?.agent).toBe("worker");
|
|
93
|
-
});
|
|
94
|
-
test("implement keywords → worker", () => {
|
|
95
|
-
expect(recommendAgent("implement the feature")?.agent).toBe("worker");
|
|
96
|
-
expect(recommendAgent("build the auth module")?.agent).toBe("worker");
|
|
97
|
-
expect(recommendAgent("Сделай эту фичу")?.agent).toBe("worker");
|
|
98
|
-
});
|
|
99
|
-
test("no match → null", () => {
|
|
100
|
-
expect(recommendAgent("hello world")).toBeNull();
|
|
101
|
-
expect(recommendAgent("")).toBeNull();
|
|
102
|
-
});
|
|
103
|
-
test("returns emoji and why", () => {
|
|
104
|
-
const r = recommendAgent("fix this bug");
|
|
105
|
-
expect(r?.emoji).toBeTruthy();
|
|
106
|
-
expect(r?.why).toBeTruthy();
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// tests/watcher.test.ts — Tests for rotor hot-reload watcher
|
|
3
|
-
// =============================================================================
|
|
4
|
-
|
|
5
|
-
/// <reference types="bun-types" />
|
|
6
|
-
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
|
-
import * as os from "node:os";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import { watchRotors } from "../watcher.js";
|
|
11
|
-
|
|
12
|
-
let tmpRoot: string;
|
|
13
|
-
let fakeHome: string;
|
|
14
|
-
let origHome: string | undefined;
|
|
15
|
-
let origUserProfile: string | undefined;
|
|
16
|
-
|
|
17
|
-
beforeAll(() => {
|
|
18
|
-
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "rotor-watch-"));
|
|
19
|
-
fakeHome = path.join(tmpRoot, "home");
|
|
20
|
-
fs.mkdirSync(fakeHome, { recursive: true });
|
|
21
|
-
origHome = process.env.HOME;
|
|
22
|
-
origUserProfile = process.env.USERPROFILE;
|
|
23
|
-
process.env.HOME = fakeHome;
|
|
24
|
-
process.env.USERPROFILE = fakeHome;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterAll(() => {
|
|
28
|
-
if (origHome !== undefined) process.env.HOME = origHome;
|
|
29
|
-
if (origUserProfile !== undefined) process.env.USERPROFILE = origUserProfile;
|
|
30
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
function sleep(ms: number): Promise<void> {
|
|
34
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("watchRotors", () => {
|
|
38
|
-
test("calls onChange when a rotor .md is added", async () => {
|
|
39
|
-
const projectDir = fs.mkdtempSync(path.join(tmpRoot, "proj-"));
|
|
40
|
-
fs.mkdirSync(path.join(projectDir, ".agents"), { recursive: true });
|
|
41
|
-
let changes = 0;
|
|
42
|
-
const handle = watchRotors(projectDir, {
|
|
43
|
-
home: fakeHome,
|
|
44
|
-
onChange: () => { changes++; },
|
|
45
|
-
});
|
|
46
|
-
try {
|
|
47
|
-
fs.writeFileSync(path.join(projectDir, ".agents", "new-rotor.md"), "---\nname: new-rotor\n---\n# body");
|
|
48
|
-
await sleep(400);
|
|
49
|
-
expect(changes).toBeGreaterThan(0);
|
|
50
|
-
} finally {
|
|
51
|
-
handle.stop();
|
|
52
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("coalesces multiple rapid changes into one onChange", async () => {
|
|
57
|
-
const projectDir = fs.mkdtempSync(path.join(tmpRoot, "proj-"));
|
|
58
|
-
fs.mkdirSync(path.join(projectDir, ".agents"), { recursive: true });
|
|
59
|
-
let changes = 0;
|
|
60
|
-
const handle = watchRotors(projectDir, {
|
|
61
|
-
home: fakeHome,
|
|
62
|
-
onChange: () => { changes++; },
|
|
63
|
-
});
|
|
64
|
-
try {
|
|
65
|
-
// Burst: 3 quick writes
|
|
66
|
-
fs.writeFileSync(path.join(projectDir, ".agents", "a.md"), "x");
|
|
67
|
-
await sleep(50);
|
|
68
|
-
fs.writeFileSync(path.join(projectDir, ".agents", "a.md"), "xy");
|
|
69
|
-
await sleep(50);
|
|
70
|
-
fs.writeFileSync(path.join(projectDir, ".agents", "a.md"), "xyz");
|
|
71
|
-
await sleep(400); // wait past debounce
|
|
72
|
-
// All three bursts collapse into ~1-2 calls (debounce)
|
|
73
|
-
expect(changes).toBeLessThan(3);
|
|
74
|
-
expect(changes).toBeGreaterThanOrEqual(1);
|
|
75
|
-
} finally {
|
|
76
|
-
handle.stop();
|
|
77
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("ignores non-.md files", async () => {
|
|
82
|
-
const projectDir = fs.mkdtempSync(path.join(tmpRoot, "proj-"));
|
|
83
|
-
fs.mkdirSync(path.join(projectDir, ".agents"), { recursive: true });
|
|
84
|
-
let changes = 0;
|
|
85
|
-
const handle = watchRotors(projectDir, {
|
|
86
|
-
home: fakeHome,
|
|
87
|
-
onChange: () => { changes++; },
|
|
88
|
-
});
|
|
89
|
-
try {
|
|
90
|
-
fs.writeFileSync(path.join(projectDir, ".agents", "notes.txt"), "ignore me");
|
|
91
|
-
fs.writeFileSync(path.join(projectDir, ".agents", ".hidden.md"), "also ignore");
|
|
92
|
-
await sleep(300);
|
|
93
|
-
expect(changes).toBe(0);
|
|
94
|
-
} finally {
|
|
95
|
-
handle.stop();
|
|
96
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("calls onNotify with summary", async () => {
|
|
101
|
-
const projectDir = fs.mkdtempSync(path.join(tmpRoot, "proj-"));
|
|
102
|
-
fs.mkdirSync(path.join(projectDir, ".agents"), { recursive: true });
|
|
103
|
-
const notifies: string[] = [];
|
|
104
|
-
const handle = watchRotors(projectDir, {
|
|
105
|
-
home: fakeHome,
|
|
106
|
-
onChange: () => {},
|
|
107
|
-
onNotify: (msg) => { notifies.push(msg); },
|
|
108
|
-
});
|
|
109
|
-
try {
|
|
110
|
-
fs.writeFileSync(path.join(projectDir, ".agents", "x.md"), "x");
|
|
111
|
-
await sleep(1000); // debounce (200) + coalesce (500) + buffer
|
|
112
|
-
expect(notifies.length).toBeGreaterThan(0);
|
|
113
|
-
expect(notifies[0]).toContain("rotors reloaded");
|
|
114
|
-
} finally {
|
|
115
|
-
handle.stop();
|
|
116
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("stop() prevents further callbacks", async () => {
|
|
121
|
-
const projectDir = fs.mkdtempSync(path.join(tmpRoot, "proj-"));
|
|
122
|
-
fs.mkdirSync(path.join(projectDir, ".agents"), { recursive: true });
|
|
123
|
-
let changes = 0;
|
|
124
|
-
const handle = watchRotors(projectDir, {
|
|
125
|
-
home: fakeHome,
|
|
126
|
-
onChange: () => { changes++; },
|
|
127
|
-
});
|
|
128
|
-
handle.stop();
|
|
129
|
-
fs.writeFileSync(path.join(projectDir, ".agents", "a.md"), "x");
|
|
130
|
-
await sleep(300);
|
|
131
|
-
expect(changes).toBe(0);
|
|
132
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("handles non-existent dirs gracefully (creates them)", () => {
|
|
136
|
-
const projectDir = fs.mkdtempSync(path.join(tmpRoot, "proj-"));
|
|
137
|
-
// Don't create .agents — watcher should create it
|
|
138
|
-
const handle = watchRotors(projectDir, {
|
|
139
|
-
home: fakeHome,
|
|
140
|
-
onChange: () => {},
|
|
141
|
-
});
|
|
142
|
-
// .agents should now exist (created by watchRotors)
|
|
143
|
-
expect(fs.existsSync(path.join(projectDir, ".agents"))).toBe(true);
|
|
144
|
-
handle.stop();
|
|
145
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
146
|
-
});
|
|
147
|
-
});
|
package/switch/tsconfig.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"module": "nodenext",
|
|
5
|
-
"moduleResolution": "nodenext",
|
|
6
|
-
"lib": [
|
|
7
|
-
"esnext"
|
|
8
|
-
],
|
|
9
|
-
"types": [
|
|
10
|
-
"node"
|
|
11
|
-
],
|
|
12
|
-
"skipLibCheck": true,
|
|
13
|
-
"noEmit": true,
|
|
14
|
-
"strict": true,
|
|
15
|
-
"esModuleInterop": true,
|
|
16
|
-
"allowSyntheticDefaultImports": true,
|
|
17
|
-
"resolveJsonModule": true,
|
|
18
|
-
"forceConsistentCasingInFileNames": true,
|
|
19
|
-
"noUncheckedIndexedAccess": false,
|
|
20
|
-
"allowImportingTsExtensions": true
|
|
21
|
-
},
|
|
22
|
-
"include": [
|
|
23
|
-
"**/*.ts"
|
|
24
|
-
],
|
|
25
|
-
"exclude": [
|
|
26
|
-
"node_modules"
|
|
27
|
-
]
|
|
28
|
-
}
|