pi-soly 0.3.0 → 0.4.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.
@@ -0,0 +1,28 @@
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
+ }
package/index.ts CHANGED
@@ -62,6 +62,10 @@ import { detectEnv, buildEnvSection, type EnvSummary } from "./env.ts";
62
62
  import { buildCodeMap, buildCodeMapSection, type CodeMap } from "./codemap.ts";
63
63
  import { loadIntentDocs, buildIntentSection, loadInlineIntentBodies, type IntentDoc } from "./intent.ts";
64
64
 
65
+ // Built-in sub-features (merged from former pi-asked, pi-agented packages):
66
+ import piAskExtension from "./ask/index.ts";
67
+ import piSwitchExtension from "./switch/index.ts";
68
+
65
69
  export default function solyExtension(pi: ExtensionAPI) {
66
70
  // ============================================================================
67
71
  // State (module-local, lives for the duration of one extension instance)
@@ -715,4 +719,8 @@ export default function solyExtension(pi: ExtensionAPI) {
715
719
  updateStatus(ctx);
716
720
  }
717
721
  });
722
+
723
+ // Mount built-in sub-features
724
+ piAskExtension(pi);
725
+ piSwitchExtension(pi);
718
726
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Project management for pi — plans, state, subagent-driven execution. Inspired by GSD.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -38,6 +38,8 @@
38
38
  "tools.ts",
39
39
  "agents-install.ts",
40
40
  "agents",
41
+ "ask",
42
+ "switch",
41
43
  "workflows",
42
44
  "workflows-data"
43
45
  ],
@@ -47,7 +49,9 @@
47
49
  "pi-package",
48
50
  "project-management",
49
51
  "planning",
50
- "subagents"
52
+ "subagents",
53
+ "multi-question",
54
+ "agent-switcher"
51
55
  ],
52
56
  "license": "MIT",
53
57
  "pi": {
@@ -0,0 +1,107 @@
1
+ # pi-switch — generic subagent switcher for pi
2
+
3
+ A tiny pi extension that gives you a **persistent indicator of the current subagent** (header bar above chat) and lets you **cycle / set / create** agents. Generic — works with any agent in `~/.pi/agent/agents/`.
4
+
5
+ ## Features
6
+
7
+ - **Header bar above chat** — shows current agent with emoji + description
8
+ - **Ctrl+Shift+S** to cycle to next agent (Shift+Tab is taken by pi's thinking-level cycler)
9
+ - **`/agent`** slash command to show current + available
10
+ - **`/agent <name>`** to set explicitly
11
+ - **`/agent create <name>`** to scaffold a new user agent
12
+ - **`/agent doctor`** to diagnose
13
+ - **`/agent recommend <task>`** to suggest the right agent for a task
14
+ - **Task → agent heuristics** baked into the system prompt so the LLM picks the right agent for the task
15
+ - Persists to `.soly/agent` (if soly project) or `~/.pi-switch/agent` (standalone)
16
+ - Reads user agents from `~/.pi/agent/agents/*.md` on every cycle — drop a file and Ctrl+Shift+S to see it
17
+
18
+ ## How agents work
19
+
20
+ Agents are markdown files with YAML frontmatter. pi-subagents (and pi-switch) discover them from these locations:
21
+
22
+ | Path | Type | Editable |
23
+ |---|---|---|
24
+ | `~/.pi/agent/npm/node_modules/pi-subagents/agents/*.md` | built-in (worker, oracle, scout, ...) | ❌ |
25
+ | `~/.pi/agent/agents/*.md` | user-defined | ✅ |
26
+ | `~/.pi/agent/extensions/soly/agents/*.md` (auto-installed if `useSolyWorkerSubagents: true` in `.soly/config.json`) | soly-augmented (soly-worker, soly-debugger, ...) | ✅ source |
27
+
28
+ ### Frontmatter schema
29
+
30
+ ```markdown
31
+ ---
32
+ name: my-reviewer # required, unique, [a-zA-Z0-9_-]{1,64}
33
+ description: One-liner shown in picker
34
+ thinking: medium # off | minimal | low | medium | high | xhigh
35
+ systemPromptMode: replace # replace | append
36
+ inheritProjectContext: true
37
+ inheritSkills: false
38
+ tools: read, grep, find, ls, bash, edit, write
39
+ defaultContext: fork # fresh | fork
40
+ ---
41
+
42
+ You are `my-reviewer`. The system prompt goes here.
43
+ ```
44
+
45
+ ## Create a new agent
46
+
47
+ ### Option A: manually
48
+ Drop a markdown file in `~/.pi/agent/agents/<name>.md` (see schema above). Press `Ctrl+Shift+S` in pi — it joins the cycle.
49
+
50
+ ### Option B: via slash command
51
+ ```
52
+ /agent create my-debugger
53
+ ```
54
+ You'll be prompted for a one-liner description. Then edit the file to specialize the system prompt.
55
+
56
+ ## Test agents
57
+
58
+ | Action | How |
59
+ |---|---|
60
+ | See current + available | `/agent` |
61
+ | Cycle | `Ctrl+Shift+S` |
62
+ | Set explicitly | `/agent soly-debugger` |
63
+ | Diagnose | `/agent doctor` |
64
+ | Recommend for a task | `/agent recommend investigate React Server Components` |
65
+
66
+ The LLM can also auto-pick — see "Task → agent" below.
67
+
68
+ ## Task → agent heuristics
69
+
70
+ The LLM's system prompt includes a table mapping task keywords to agents. When the user request matches, the LLM should call `/agent <name>` first, then `subagent({ agent: <name>, ... })`.
71
+
72
+ | Keywords | Agent | Why |
73
+ |---|---|---|
74
+ | research, investigate, look up, find out, explore, compare libraries | 📚 researcher | external docs, ecosystem behavior |
75
+ | scout, scan, map, where is, locate, skim | 🔍 scout | codebase recon |
76
+ | plan, design, architect, outline, structure | 📋 planner | decompose into steps |
77
+ | review, audit, check, adversarial, critique, qa | 👀 reviewer | adversarial review |
78
+ | oracle, decision, tradeoff, which approach, drift | 🔮 oracle | decision consistency |
79
+ | debug, bug, fix, crash, error, repro | 🐞 soly-debugger | bug investigation |
80
+ | test, tests, coverage, spec, assert | 🧪 soly-tester | test-only work |
81
+ | refactor, clean up, simplify, extract, rename | 🔄 soly-refactor | pure refactoring |
82
+ | document, docs, readme, jsdoc | 📝 soly-documenter | doc updates |
83
+ | implement, build, write code, add feature | ⚡ worker | implementation |
84
+ | orchestrate, coordinate, dispatch, chain | 🤝 delegate | multi-agent |
85
+
86
+ Same keywords in Russian work (изучи, баг, тест, etc.).
87
+
88
+ ## Integration with other extensions
89
+
90
+ - **soly** reads `globalThis.__PI_SWITCH_AGENT__` to know which subagent to launch for `soly execute`. Falls back to `"worker"` if pi-switch isn't loaded.
91
+ - **Soly** also auto-installs soly-augmented agents (soly-worker, soly-debugger, etc.) to `~/.pi/agent/agents/` when `useSolyWorkerSubagents: true` in `.soly/config.json`.
92
+
93
+ ## Files
94
+
95
+ - `core.ts` — agent metadata, discovery, cycling, persistence
96
+ - `prompt.ts` — system-prompt section + task→agent heuristics + `recommendAgent`
97
+ - `index.ts` — header bar, Ctrl+Shift+S, `/agent` slash command, `/agent create`/`/agent doctor`/`/agent recommend`
98
+ - `tests/core.test.ts` — 21 tests for core logic
99
+ - `tests/prompt.test.ts` — 20 tests for prompt + recommendAgent
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ cd ~/.pi/agent/extensions/pi-switch
105
+ bun test # 41 tests
106
+ bun run typecheck # tsc --noEmit
107
+ ```
package/switch/core.ts ADDED
@@ -0,0 +1,202 @@
1
+ // =============================================================================
2
+ // core.ts — Generic subagent switcher for pi
3
+ // =============================================================================
4
+ //
5
+ // Lets the user pick which subagent the LLM uses (for `subagent(...)` calls
6
+ // in the pi-subagents system, and for any extension that reads the current
7
+ // agent). Generic — works with pi-subagents' built-ins (worker, oracle,
8
+ // scout, ...) AND any user-defined agent in `~/.pi/agent/agents/`.
9
+ //
10
+ // Cycle order (Shift+Tab in pi is taken by thinking-level, so we use
11
+ // Ctrl+Shift+S — mnemonic for "S"witch).
12
+ //
13
+ // Communication with other extensions:
14
+ // - Writes `globalThis.__PI_SWITCH_AGENT__` (in-process)
15
+ // - Reads/writes `.soly/agent` if it exists (cross-session persistence,
16
+ // shared with soly extension). If no soly project, persists to
17
+ // `~/.pi-switch/agent` instead.
18
+ // =============================================================================
19
+
20
+ import * as fs from "node:fs";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+
24
+ /** Default agent used when no override is set. */
25
+ export const DEFAULT_AGENT = "worker";
26
+
27
+ /** Built-in pi-subagents that we always offer in the cycle. */
28
+ export const BUILTIN_AGENTS: readonly string[] = [
29
+ "worker",
30
+ "oracle",
31
+ "scout",
32
+ "researcher",
33
+ "planner",
34
+ "context-builder",
35
+ "reviewer",
36
+ "delegate",
37
+ ] as const;
38
+
39
+ /** Visual metadata for every known agent. Used by the rich status badge,
40
+ * the header bar, and the multi-line switch notify. */
41
+ export interface AgentMeta {
42
+ emoji: string;
43
+ shortLabel: string;
44
+ description: string;
45
+ writesFiles: boolean;
46
+ }
47
+
48
+ export const AGENT_META: Record<string, AgentMeta> = {
49
+ worker: { emoji: "\u26a1", shortLabel: "worker", description: "generic implementation, all tools", writesFiles: true },
50
+ oracle: { emoji: "\ud83d\udd2e", shortLabel: "oracle", description: "decision-consistency, no file edits", writesFiles: false },
51
+ scout: { emoji: "\ud83d\udd0d", shortLabel: "scout", description: "codebase recon, read-only", writesFiles: false },
52
+ researcher: { emoji: "\ud83d\udcda", shortLabel: "researcher", description: "external docs / libraries", writesFiles: false },
53
+ planner: { emoji: "\ud83d\udccb", shortLabel: "planner", description: "planning + ordering, no code", writesFiles: false },
54
+ "context-builder": { emoji: "\ud83c\udfd7", shortLabel: "ctx-builder", description: "context handoff for other agents", writesFiles: true },
55
+ reviewer: { emoji: "\ud83d\udc40", shortLabel: "reviewer", description: "adversarial code review", writesFiles: false },
56
+ delegate: { emoji: "\ud83e\udd1d", shortLabel: "delegate", description: "pure orchestration, dispatches others", writesFiles: false },
57
+ };
58
+
59
+ /** Get metadata for an agent. Falls back to a neutral entry for unknown. */
60
+ export function getAgentMeta(name: string): AgentMeta {
61
+ return AGENT_META[name] ?? {
62
+ emoji: "\u2753",
63
+ shortLabel: name.length > 12 ? name.slice(0, 11) + "\u2026" : name,
64
+ description: "user-defined agent",
65
+ writesFiles: true,
66
+ };
67
+ }
68
+
69
+ /** Validate an agent name. */
70
+ export function isValidAgentName(name: string): boolean {
71
+ return /^[a-zA-Z0-9_-]{1,64}$/.test(name);
72
+ }
73
+
74
+ /** Discover agent `.md` files in user dir. */
75
+ export function discoverUserAgents(userDir: string = path.join(os.homedir(), ".pi", "agent", "agents")): string[] {
76
+ if (!fs.existsSync(userDir)) return [];
77
+ const names: string[] = [];
78
+ for (const file of fs.readdirSync(userDir)) {
79
+ if (!file.endsWith(".md")) continue;
80
+ try {
81
+ const raw = fs.readFileSync(path.join(userDir, file), "utf-8");
82
+ const m = raw.match(/^---\n([\s\S]*?)\n---/);
83
+ if (!m) continue;
84
+ const fm = m[1] ?? "";
85
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
86
+ if (nameMatch) {
87
+ const n = (nameMatch[1] ?? "").trim();
88
+ if (isValidAgentName(n)) names.push(n);
89
+ }
90
+ } catch { /* skip */ }
91
+ }
92
+ return names;
93
+ }
94
+
95
+ /** Build the full cycle of available agents. Built-ins first, then
96
+ * user-discovered. Dedupes while preserving first-occurrence order. */
97
+ export function availableAgents(userDir?: string): string[] {
98
+ const out: string[] = [];
99
+ const seen = new Set<string>();
100
+ const push = (n: string) => {
101
+ if (!seen.has(n)) {
102
+ seen.add(n);
103
+ out.push(n);
104
+ }
105
+ };
106
+ for (const a of BUILTIN_AGENTS) push(a);
107
+ for (const a of discoverUserAgents(userDir)) push(a);
108
+ return out;
109
+ }
110
+
111
+ /** Cycle order. */
112
+ export function nextAgent(current: string, cycle: readonly string[]): string {
113
+ if (cycle.length === 0) return DEFAULT_AGENT;
114
+ const idx = cycle.indexOf(current);
115
+ if (idx < 0) return cycle[0]!;
116
+ return cycle[(idx + 1) % cycle.length]!;
117
+ }
118
+
119
+ /** Parse a user-supplied agent name. */
120
+ export function parseAgentName(raw: string): string | null {
121
+ const n = raw.trim();
122
+ if (!isValidAgentName(n)) return null;
123
+ return n;
124
+ }
125
+
126
+ /** Short badge: `<emoji> <name>`. Null for default (silent). */
127
+ export function formatAgentBadge(agent: string): string | null {
128
+ if (agent === DEFAULT_AGENT) return null;
129
+ const meta = getAgentMeta(agent);
130
+ return `${meta.emoji} ${agent}`;
131
+ }
132
+
133
+ /** Multi-line switch notify. */
134
+ export function formatAgentSwitchNotify(prev: string, next: string): string {
135
+ const prevMeta = getAgentMeta(prev);
136
+ const nextMeta = getAgentMeta(next);
137
+ const lines: string[] = [
138
+ "pi-switch agent changed",
139
+ "",
140
+ ` ${prevMeta.emoji} ${prev.padEnd(16)} → ${nextMeta.emoji} ${next}`,
141
+ ` ${"".padEnd(16)} ${nextMeta.description}`,
142
+ "",
143
+ ` writes files: ${nextMeta.writesFiles ? "yes" : "no (read-only)"} · next subagent call uses: ${next}`,
144
+ ];
145
+ return lines.join("\n");
146
+ }
147
+
148
+ /** Group agents: built-ins + user-defined. */
149
+ export function groupedAvailableAgents(userDir?: string): Array<{ header: string; agents: string[] }> {
150
+ const all = availableAgents(userDir);
151
+ const groups: Array<{ header: string; agents: string[] }> = [];
152
+ const builtin = all.filter((a) => BUILTIN_AGENTS.includes(a));
153
+ if (builtin.length > 0) groups.push({ header: "built-in", agents: builtin });
154
+ const user = all.filter((a) => !BUILTIN_AGENTS.includes(a));
155
+ if (user.length > 0) groups.push({ header: "user-defined", agents: user });
156
+ return groups;
157
+ }
158
+
159
+ /** Header line shown above chat. Persistent, dim, single line. */
160
+ export function formatHeaderLine(agent: string): string {
161
+ const meta = getAgentMeta(agent);
162
+ const writeTag = meta.writesFiles ? "" : " \u00b7 read-only";
163
+ return `${meta.emoji} ${agent} \u00b7 ${meta.description}${writeTag} [Ctrl+Shift+S to cycle]`;
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Persistence
168
+ // ---------------------------------------------------------------------------
169
+
170
+ /** Where to persist the current agent. Prefers `.soly/agent` if a soly
171
+ * project exists (shared with soly extension). Otherwise `~/.pi-switch/agent`. */
172
+ export function agentFilePath(cwd: string): string {
173
+ const solyAgent = path.join(cwd, ".soly", "agent");
174
+ if (fs.existsSync(path.join(cwd, ".soly"))) return solyAgent;
175
+ // Respect HOME/USERPROFILE for testability (otherwise os.homedir() ignores them on Windows)
176
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
177
+ const fallbackDir = path.join(home, ".pi-switch");
178
+ fs.mkdirSync(fallbackDir, { recursive: true });
179
+ return path.join(fallbackDir, "agent");
180
+ }
181
+
182
+ /** Read persisted agent from disk. Returns null if missing/invalid. */
183
+ export function loadAgent(cwd: string): string | null {
184
+ try {
185
+ const file = agentFilePath(cwd);
186
+ if (!fs.existsSync(file)) return null;
187
+ const raw = fs.readFileSync(file, "utf-8").trim();
188
+ if (!isValidAgentName(raw)) return null;
189
+ return raw;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /** Write current agent to disk. */
196
+ export function saveAgent(cwd: string, agent: string): void {
197
+ try {
198
+ const file = agentFilePath(cwd);
199
+ fs.mkdirSync(path.dirname(file), { recursive: true });
200
+ fs.writeFileSync(file, agent + "\n", "utf-8");
201
+ } catch { /* best-effort */ }
202
+ }