pi-soly 1.1.0 → 1.3.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/README.md +2 -2
- package/agents-install.ts +11 -98
- package/commands.ts +46 -0
- package/config.ts +4 -6
- package/index.ts +13 -11
- package/package.json +1 -2
- package/skills/soly-framework/SKILL.md +5 -18
- package/switch/README.md +4 -4
- package/switch/index.ts +18 -0
- package/switch/prompt.ts +15 -15
- package/switch/tests/prompt.test.ts +23 -23
- package/switch/tests/watcher.test.ts +147 -0
- package/switch/watcher.ts +112 -0
- package/agents/soly-manager.md +0 -124
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ GSD-inspired planning and execution. State is the source of truth, not vibes.
|
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
/plan # generate PLAN.md for the current phase
|
|
59
|
-
/execute #
|
|
59
|
+
/execute # execute plan directly (LLM does the work)
|
|
60
60
|
/resume # pick up a paused session
|
|
61
61
|
/inspect # show current state summary
|
|
62
62
|
/discuss 3 # talk through decisions before planning phase 3
|
|
@@ -182,7 +182,7 @@ todo_update({
|
|
|
182
182
|
┌──────────────────┐
|
|
183
183
|
│ 7 soly agents │
|
|
184
184
|
│ │
|
|
185
|
-
│
|
|
185
|
+
│ worker (cycle) │
|
|
186
186
|
│ soly-debugger │
|
|
187
187
|
│ soly-tester │
|
|
188
188
|
│ soly-reviewer │
|
package/agents-install.ts
CHANGED
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
// assets-install.ts — Idempotent install of soly-managed user assets
|
|
3
3
|
// =============================================================================
|
|
4
4
|
//
|
|
5
|
-
// Soly ships
|
|
5
|
+
// Soly ships one kind of user-scope asset:
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// The single `soly-manager` subagent (mode-switching executor).
|
|
9
|
-
//
|
|
10
|
-
// 2. Skills → `~/.pi/agent/skills/<name>/`
|
|
7
|
+
// Skills → `~/.pi/agent/skills/<name>/`
|
|
11
8
|
// The `soly-framework` skill — framework documentation the LLM
|
|
12
|
-
// loads on demand via the read tool.
|
|
9
|
+
// loads on demand via the read tool. This is the LLM's only
|
|
10
|
+
// "helper" for soly — pi doesn't need a separate subagent layer
|
|
11
|
+
// for plan execution (the LLM in the main session does it).
|
|
13
12
|
//
|
|
14
|
-
// pi discovers
|
|
15
|
-
// copy our shipped
|
|
13
|
+
// pi discovers the skill from `~/.pi/agent/`, so on first session_start
|
|
14
|
+
// we copy our shipped file there.
|
|
16
15
|
//
|
|
17
16
|
// IDEMPOTENT: if the target file already exists (user may have customized
|
|
18
17
|
// it), we do NOT overwrite. This is one-way "first install wins".
|
|
@@ -22,41 +21,19 @@ import * as fs from "node:fs";
|
|
|
22
21
|
import * as os from "node:os";
|
|
23
22
|
import * as path from "node:path";
|
|
24
23
|
|
|
25
|
-
/** soly agent files bundled with the extension. */
|
|
26
|
-
const SHIPPED_AGENTS = [
|
|
27
|
-
"soly-manager.md",
|
|
28
|
-
] as const;
|
|
29
|
-
|
|
30
24
|
/** soly skills bundled with the extension. Each entry is a directory
|
|
31
25
|
* under `skills/` containing a SKILL.md. */
|
|
32
26
|
const SHIPPED_SKILLS = [
|
|
33
27
|
"soly-framework",
|
|
34
28
|
] as const;
|
|
35
29
|
|
|
36
|
-
/** Where pi looks for user
|
|
37
|
-
* testability (otherwise we'd always write to the real user home).
|
|
38
|
-
* Agent files live directly in the dir (no `agents/` subfolder):
|
|
39
|
-
* ~/.agents/reviewer.md (NOT ~/.agents/agents/reviewer.md)
|
|
40
|
-
* This keeps the path clean and matches the project-level convention. */
|
|
41
|
-
function userAgentsDirs(): string[] {
|
|
42
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
43
|
-
return [
|
|
44
|
-
path.join(home, ".agents"), // vendor-neutral (preferred)
|
|
45
|
-
path.join(home, ".pi", "agent", "agents"), // pi native (legacy)
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Where pi looks for user skills. */
|
|
30
|
+
/** Where pi looks for user skills. Respects HOME/USERPROFILE for
|
|
31
|
+
* testability (otherwise we'd always write to the real user home). */
|
|
50
32
|
function userSkillsDir(): string {
|
|
51
33
|
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
52
34
|
return path.join(home, ".pi", "agent", "skills");
|
|
53
35
|
}
|
|
54
36
|
|
|
55
|
-
/** Where this soly extension's `agents/` directory lives. */
|
|
56
|
-
function shippedAgentsDir(extensionRoot: string): string {
|
|
57
|
-
return path.join(extensionRoot, "agents");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
37
|
/** Where this soly extension's `skills/` directory lives. */
|
|
61
38
|
function shippedSkillsDir(extensionRoot: string): string {
|
|
62
39
|
return path.join(extensionRoot, "skills");
|
|
@@ -68,19 +45,7 @@ export interface InstallResult {
|
|
|
68
45
|
errors: string[];
|
|
69
46
|
}
|
|
70
47
|
|
|
71
|
-
/** Copy a
|
|
72
|
-
function copyIfMissing(from: string, to: string): "installed" | "skipped" | "error" {
|
|
73
|
-
if (!fs.existsSync(from)) return "error";
|
|
74
|
-
if (fs.existsSync(to)) return "skipped";
|
|
75
|
-
try {
|
|
76
|
-
fs.copyFileSync(from, to);
|
|
77
|
-
return "installed";
|
|
78
|
-
} catch {
|
|
79
|
-
return "error";
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Recursively copy a directory tree if destination doesn't exist. Idempotent. */
|
|
48
|
+
/** Copy a directory tree if destination doesn't exist. Idempotent. */
|
|
84
49
|
function copyDirIfMissing(from: string, to: string): "installed" | "skipped" | "error" {
|
|
85
50
|
if (!fs.existsSync(from)) return "error";
|
|
86
51
|
if (fs.existsSync(to)) return "skipped";
|
|
@@ -93,40 +58,6 @@ function copyDirIfMissing(from: string, to: string): "installed" | "skipped" | "
|
|
|
93
58
|
}
|
|
94
59
|
}
|
|
95
60
|
|
|
96
|
-
/** Install shipped soly agents to `~/.agents/` (vendor-neutral,
|
|
97
|
-
* preferred). Legacy `~/.pi/agent/agents/` copies are left alone —
|
|
98
|
-
* `discoverUserRotors` reads both, so old installs still work. */
|
|
99
|
-
export function installSolyAgents(extensionRoot: string): InstallResult {
|
|
100
|
-
const result: InstallResult = { installed: [], skipped: [], errors: [] };
|
|
101
|
-
const src = shippedAgentsDir(extensionRoot);
|
|
102
|
-
|
|
103
|
-
if (!fs.existsSync(src)) return result; // dev mode no-op
|
|
104
|
-
|
|
105
|
-
// Try vendor-neutral first, then fall back to pi's native dir.
|
|
106
|
-
let dst: string | null = null;
|
|
107
|
-
for (const candidate of userAgentsDirs()) {
|
|
108
|
-
try {
|
|
109
|
-
fs.mkdirSync(candidate, { recursive: true });
|
|
110
|
-
dst = candidate;
|
|
111
|
-
break;
|
|
112
|
-
} catch (err) {
|
|
113
|
-
result.errors.push(`mkdir ${candidate}: ${(err as Error).message}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (!dst) return result;
|
|
117
|
-
|
|
118
|
-
for (const name of SHIPPED_AGENTS) {
|
|
119
|
-
const from = path.join(src, name);
|
|
120
|
-
const to = path.join(dst, name);
|
|
121
|
-
const r = copyIfMissing(from, to);
|
|
122
|
-
if (r === "installed") result.installed.push(name);
|
|
123
|
-
else if (r === "skipped") result.skipped.push(name);
|
|
124
|
-
else result.errors.push(`missing source: ${from}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return result;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
61
|
/** Install shipped soly skills to `~/.pi/agent/skills/`. Idempotent. */
|
|
131
62
|
export function installSolySkills(extensionRoot: string): InstallResult {
|
|
132
63
|
const result: InstallResult = { installed: [], skipped: [], errors: [] };
|
|
@@ -147,33 +78,15 @@ export function installSolySkills(extensionRoot: string): InstallResult {
|
|
|
147
78
|
return result;
|
|
148
79
|
}
|
|
149
80
|
|
|
150
|
-
/** Install all soly assets (
|
|
81
|
+
/** Install all soly assets (skills only — agents are not shipped). */
|
|
151
82
|
export function installSolyAssets(extensionRoot: string): {
|
|
152
|
-
agents: InstallResult;
|
|
153
83
|
skills: InstallResult;
|
|
154
84
|
} {
|
|
155
85
|
return {
|
|
156
|
-
agents: installSolyAgents(extensionRoot),
|
|
157
86
|
skills: installSolySkills(extensionRoot),
|
|
158
87
|
};
|
|
159
88
|
}
|
|
160
89
|
|
|
161
|
-
/** Check which shipped soly agents are present across all user agent
|
|
162
|
-
* homes. A file counts as "installed" if it's in ANY of the dirs. */
|
|
163
|
-
export function checkSolyAgentsInstalled(extensionRoot: string): {
|
|
164
|
-
installed: string[];
|
|
165
|
-
missing: string[];
|
|
166
|
-
} {
|
|
167
|
-
const installed: string[] = [];
|
|
168
|
-
const missing: string[] = [];
|
|
169
|
-
for (const name of SHIPPED_AGENTS) {
|
|
170
|
-
const present = userAgentsDirs().some((dir) => fs.existsSync(path.join(dir, name)));
|
|
171
|
-
if (present) installed.push(name);
|
|
172
|
-
else missing.push(name);
|
|
173
|
-
}
|
|
174
|
-
return { installed, missing };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
90
|
/** Check which shipped soly skills are present in the user dir. */
|
|
178
91
|
export function checkSolySkillsInstalled(extensionRoot: string): {
|
|
179
92
|
installed: string[];
|
package/commands.ts
CHANGED
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
type SolyState,
|
|
32
32
|
} from "./core.ts";
|
|
33
33
|
import type { SolyConfig } from "./config.ts";
|
|
34
|
+
import { migrateSolyDir } from "./migrate.js";
|
|
35
|
+
import { initSolyProject } from "./init.js";
|
|
36
|
+
import { readNotifications, formatNotifications } from "./notifications-log.js";
|
|
37
|
+
import { formatStatus } from "./status.js";
|
|
34
38
|
|
|
35
39
|
/** Minimum ui surface the command handlers actually need. */
|
|
36
40
|
export interface CommandUI {
|
|
@@ -332,6 +336,48 @@ What must the LLM do?
|
|
|
332
336
|
});
|
|
333
337
|
|
|
334
338
|
// ============================================================================
|
|
339
|
+
// /soly migrate — move .soly/ → .agents/ atomically
|
|
340
|
+
// ============================================================================
|
|
341
|
+
pi.registerCommand("soly-migrate", {
|
|
342
|
+
description:
|
|
343
|
+
"migrate project state from .soly/ to .agents/ (atomic rename, validates result, suggests git commit)",
|
|
344
|
+
handler: async (args, ctx) => {
|
|
345
|
+
const ui: CommandUI = {
|
|
346
|
+
notify: (t, k) => ctx.ui.notify(t, k ?? "info"),
|
|
347
|
+
select: async (label, options) => {
|
|
348
|
+
const result = await ctx.ui.select(label, options);
|
|
349
|
+
return result === undefined ? null : options.indexOf(result);
|
|
350
|
+
},
|
|
351
|
+
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
352
|
+
};
|
|
353
|
+
await migrateSolyDir(ctx.cwd, ui, { autoYes: args.includes("--yes") });
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// /soly init — scaffold new project
|
|
358
|
+
// ============================================================================
|
|
359
|
+
pi.registerCommand("soly-init", {
|
|
360
|
+
description:
|
|
361
|
+
"scaffold a new soly project (interactive: pick template — minimal/web-app/library/cli)",
|
|
362
|
+
handler: async (args, ctx) => {
|
|
363
|
+
const ui: CommandUI = {
|
|
364
|
+
notify: (t, k) => ctx.ui.notify(t, k ?? "info"),
|
|
365
|
+
select: async (label, options) => {
|
|
366
|
+
const result = await ctx.ui.select(label, options);
|
|
367
|
+
return result === undefined ? null : options.indexOf(result);
|
|
368
|
+
},
|
|
369
|
+
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
370
|
+
input: (label, placeholder) => ctx.ui.input(label, placeholder),
|
|
371
|
+
};
|
|
372
|
+
// Parse args: --template=X, --yes, --name=X
|
|
373
|
+
const template = (args.match(/--template[= ](\S+)/)?.[1] as
|
|
374
|
+
| "minimal" | "web-app" | "library" | "cli" | undefined) ?? undefined;
|
|
375
|
+
const autoYes = args.includes("--yes");
|
|
376
|
+
const projectName = args.match(/--name[= ](\S+)/)?.[1];
|
|
377
|
+
await initSolyProject(ctx.cwd, ui, { template, autoYes, projectName });
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
335
381
|
// /soly
|
|
336
382
|
// ============================================================================
|
|
337
383
|
|
package/config.ts
CHANGED
|
@@ -35,12 +35,10 @@ export interface SolyConfig {
|
|
|
35
35
|
preferAskPro: boolean;
|
|
36
36
|
/** When soly pause is invoked, also auto-save HANDOFF.json (currently always true; knob for future). */
|
|
37
37
|
autoCheckpointOnPause: boolean;
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* specialized subagent since the workflow template already
|
|
43
|
-
* contains soly instructions. */
|
|
38
|
+
/** DEPRECATED in 1.3.0. Soly no longer ships a subagent. The LLM
|
|
39
|
+
* executes plans directly using the slash commands + the
|
|
40
|
+
* soly-framework skill. This option is kept as a no-op for
|
|
41
|
+
* backward compat with old config files. */
|
|
44
42
|
useSolyWorkerSubagents: boolean;
|
|
45
43
|
};
|
|
46
44
|
display: {
|
package/index.ts
CHANGED
|
@@ -88,9 +88,10 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
88
88
|
// ============================================================================
|
|
89
89
|
// Rotor switcher: REMOVED. The rotor cycler is now owned by the
|
|
90
90
|
// separate `pi-switch` extension (footer pill + Ctrl+Tab + /rotor slash).
|
|
91
|
-
// Soly
|
|
92
|
-
//
|
|
93
|
-
//
|
|
91
|
+
// Soly no longer ships a subagent (removed in 1.3.0). The LLM does plan
|
|
92
|
+
// execution directly using slash commands + the soly-framework skill.
|
|
93
|
+
// Workflows read the current rotor from globalThis.__PI_SWITCH_ROTOR__
|
|
94
|
+
// (set by pi-switch).
|
|
94
95
|
// ============================================================================
|
|
95
96
|
|
|
96
97
|
// Config (per-project + global + defaults). Refreshed on session_start
|
|
@@ -354,7 +355,7 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
354
355
|
pi.on("session_start", async (event, ctx) => {
|
|
355
356
|
// Deprecation warning: if the project still uses `.soly/`, nudge the
|
|
356
357
|
// user toward `.agents/`. One-time per session.
|
|
357
|
-
if (isLegacySolyDir(ctx.cwd)) {
|
|
358
|
+
if (activeConfig.agent.useSolyWorkerSubagents && isLegacySolyDir(ctx.cwd)) {
|
|
358
359
|
notifyDeprecation(
|
|
359
360
|
ctx.ui,
|
|
360
361
|
`.soly/ (legacy)`,
|
|
@@ -405,21 +406,22 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
405
406
|
}
|
|
406
407
|
}
|
|
407
408
|
|
|
408
|
-
// Auto-install soly user-scope assets (soly-
|
|
409
|
-
//
|
|
410
|
-
// `agent.useSolyWorkerSubagents`
|
|
411
|
-
//
|
|
409
|
+
// Auto-install soly user-scope assets (soly-framework skill only,
|
|
410
|
+
// since 1.3.0 we don't ship a subagent) to ~/.pi/agent/skills/ on
|
|
411
|
+
// first run. Opt-in via config `agent.useSolyWorkerSubagents`
|
|
412
|
+
// (kept for backward compat, now a no-op for the skill install).
|
|
413
|
+
// Idempotent — respects any existing user-customized copies.
|
|
412
414
|
if (activeConfig.agent.useSolyWorkerSubagents) {
|
|
413
415
|
const extRoot = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
|
|
414
416
|
const assets = installSolyAssets(extRoot);
|
|
415
|
-
const installed =
|
|
417
|
+
const installed = assets.skills.installed.map((s) => `skill:${s}`);
|
|
416
418
|
if (installed.length > 0) {
|
|
417
419
|
ctx.ui.notify(
|
|
418
|
-
`soly: installed (${installed.join(", ")}) —
|
|
420
|
+
`soly: installed (${installed.join(", ")}) — pi will discover on next session`,
|
|
419
421
|
"info",
|
|
420
422
|
);
|
|
421
423
|
}
|
|
422
|
-
for (const e of
|
|
424
|
+
for (const e of assets.skills.errors) {
|
|
423
425
|
ctx.ui.notify(`soly: install error — ${e}`, "warning");
|
|
424
426
|
}
|
|
425
427
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-soly",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Project management framework for pi-coding-agent. Workflows, planning, multi-question picker, agent switcher, live task list — one npm install, zero config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"scratchpad.ts",
|
|
39
39
|
"tools.ts",
|
|
40
40
|
"agents-install.ts",
|
|
41
|
-
"agents",
|
|
42
41
|
"ask",
|
|
43
42
|
"skills",
|
|
44
43
|
"switch",
|
|
@@ -27,7 +27,7 @@ The **soly** extension adds project-management workflow to [pi-coding-agent](htt
|
|
|
27
27
|
| Command | What it does |
|
|
28
28
|
|---|---|
|
|
29
29
|
| `/plan [N]` | Generate or update `PLAN.md` for phase N (or current phase) |
|
|
30
|
-
| `/execute [N[.MM]]` |
|
|
30
|
+
| `/execute [N[.MM]]` | Execute plan(s) in phase N. `N` = all plans. `N.MM` = specific plan. The LLM (you) executes directly. |
|
|
31
31
|
| `/discuss N` | Discussion-driven scoping for phase N — capture decisions before planning |
|
|
32
32
|
| `/inspect` | One-screen summary: position, phases, recent decisions |
|
|
33
33
|
| `/pause` | Save handoff (`HANDOFF.json` + `.continue-here.md`) for later resume |
|
|
@@ -211,24 +211,11 @@ Once production commits exist, returning without a committed `SUMMARY.md` is an
|
|
|
211
211
|
|
|
212
212
|
Switch with `/rotor <name>` or `Ctrl+Tab` (cycles through). Footer pill shows current: `· ⚡ worker` / `▶ 🐢 oracle`.
|
|
213
213
|
|
|
214
|
-
**Why "rotors"?** Because they *rotate* — `Ctrl+Tab` cycles through them. The word emphasizes the cycling behavior.
|
|
214
|
+
**Why "rotors"?** Because they *rotate* — `Ctrl+Tab` cycles through them. The word emphasizes the cycling behavior. As of 1.3.0 there are no soly subagents — only the cycle rotors.
|
|
215
215
|
|
|
216
|
-
## Subagent:
|
|
216
|
+
## Subagent: none (as of 1.3.0)
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
| Task brief mentions | Mode |
|
|
221
|
-
|---|---|
|
|
222
|
-
| implement, build, write code, add feature, create | **worker** |
|
|
223
|
-
| debug, bug, fix, crash, error, repro, broken | **debugger** |
|
|
224
|
-
| test, coverage, spec, assert, only modify tests | **tester** |
|
|
225
|
-
| review, audit, adversarial, find bugs, qa | **reviewer** |
|
|
226
|
-
| refactor, simplify, extract, rename, no behavior change | **refactor** |
|
|
227
|
-
| document, readme, jsdoc, comment, intent doc | **documenter** |
|
|
228
|
-
| validate, scope, drift, decision, before committing | **oracle** |
|
|
229
|
-
| plan, design, outline, structure, decompose | **planner** |
|
|
230
|
-
|
|
231
|
-
**soly-manager is ONE agent that switches modes. Don't spawn soly-worker / soly-debugger / etc. — those don't exist anymore.**
|
|
218
|
+
**Soly no longer ships a subagent.** You (the LLM) execute plans directly in the main session using the slash commands above. If you need help, use pi's built-in cycle agents (\`worker\`, \`oracle\`, \`scout\`, \`reviewer\`) or the user's custom agents in \`~/.agents/\`. Don't spawn \`soly-manager\` / \`soly-worker\` / etc. — they don't exist.
|
|
232
219
|
|
|
233
220
|
## Tools the LLM can call
|
|
234
221
|
|
|
@@ -358,7 +345,7 @@ If `/execute` complains about illegal partial state:
|
|
|
358
345
|
|
|
359
346
|
- ❌ Edit `.soly/rules/` files you didn't write — those are project invariants
|
|
360
347
|
- ❌ Skip the SUMMARY — illegal partial state
|
|
361
|
-
- ❌ Spawn `soly-
|
|
348
|
+
- ❌ Spawn `soly-manager` / `soly-worker` / etc. — there are no soly subagents (removed in 1.3.0). Use pi's built-in subagents via the parent LLM's \`subagent(...)\` call.
|
|
362
349
|
- ❌ Write rules in code comments — use `.soly/rules/*.md` or `.agents/rules/*.md` files
|
|
363
350
|
- ❌ Edit `.soly/phases/*/PLAN.md` after `status: in_progress` — create a new plan
|
|
364
351
|
- ❌ Put intent docs anywhere other than `.soly/docs/` (or `.agents/docs/` for vendor-neutral)
|
package/switch/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Agents are markdown files with YAML frontmatter. pi-subagents (and pi-switch) di
|
|
|
25
25
|
|---|---|---|
|
|
26
26
|
| `~/.pi/agent/npm/node_modules/pi-subagents/agents/*.md` | built-in (worker, oracle, scout, reviewer) | ❌ |
|
|
27
27
|
| `~/.pi/agent/agents/*.md` | user-defined | ✅ |
|
|
28
|
-
| `~/.pi/agent/extensions/pi-soly/agents/*.md` (auto-installed if `useSolyWorkerSubagents: true` in `.soly/config.json`) | soly
|
|
28
|
+
| `~/.pi/agent/extensions/pi-soly/agents/*.md` (auto-installed if `useSolyWorkerSubagents: true` in `.soly/config.json`) | (removed in 1.3.0 — soly no longer ships a subagent) | — |
|
|
29
29
|
|
|
30
30
|
### Frontmatter schema
|
|
31
31
|
|
|
@@ -61,7 +61,7 @@ You'll be prompted for a one-liner description. Then edit the file to specialize
|
|
|
61
61
|
|---|---|
|
|
62
62
|
| See current + available | `/agent` |
|
|
63
63
|
| Cycle | `Ctrl+Tab` (or `F2`) |
|
|
64
|
-
| Set explicitly |
|
|
64
|
+
| Set explicitly | `(n/a — no soly subagent since 1.3.0)` |
|
|
65
65
|
| Diagnose | `/agent doctor` |
|
|
66
66
|
| Recommend for a task | `/agent recommend investigate React Server Components` |
|
|
67
67
|
|
|
@@ -76,7 +76,7 @@ The LLM's system prompt includes a table mapping task keywords to agents. When t
|
|
|
76
76
|
| scout, scan, map, where is, locate, skim | 🔍 scout | codebase recon |
|
|
77
77
|
| review, audit, check, adversarial, critique, qa | 👀 reviewer | adversarial review |
|
|
78
78
|
| oracle, decision, tradeoff, which approach, drift | 🔮 oracle | decision consistency |
|
|
79
|
-
| implement, build, write code, add feature, debug, fix, test, refactor, document, plan, validate | ⚡
|
|
79
|
+
| implement, build, write code, add feature, debug, fix, test, refactor, document, plan, validate | ⚡ worker | do it yourself using slash commands |
|
|
80
80
|
| (anything else) | ⚡ worker | generic implementation |
|
|
81
81
|
|
|
82
82
|
Same keywords in Russian work (изучи, баг, тест, etc.).
|
|
@@ -84,7 +84,7 @@ Same keywords in Russian work (изучи, баг, тест, etc.).
|
|
|
84
84
|
## Integration with other extensions
|
|
85
85
|
|
|
86
86
|
- **pi-soly** reads `globalThis.__PI_SWITCH_AGENT__` to know which cycle agent is active. Falls back to `"worker"` if pi-switch isn't loaded.
|
|
87
|
-
- **pi-soly
|
|
87
|
+
- **pi-soly no longer ships a subagent** (removed in 1.3.0). The LLM in the main session executes plans directly using the slash commands + the `soly-framework` skill.
|
|
88
88
|
|
|
89
89
|
## Files
|
|
90
90
|
|
package/switch/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
saveAgent,
|
|
36
36
|
} from "./core.ts";
|
|
37
37
|
import { buildPiSwitchSection, recommendAgent } from "./prompt.ts";
|
|
38
|
+
import { watchRotors, type WatcherHandle } from "./watcher.ts";
|
|
38
39
|
|
|
39
40
|
const GLOBAL_KEY = "__PI_SWITCH_ROTOR__";
|
|
40
41
|
|
|
@@ -43,6 +44,22 @@ export default function piSwitchExtension(pi: ExtensionAPI) {
|
|
|
43
44
|
let currentRotor: string = DEFAULT_ROTOR;
|
|
44
45
|
let cycle: string[] = [DEFAULT_ROTOR];
|
|
45
46
|
let lastUi: ExtensionUIContext | null = null;
|
|
47
|
+
let rotorWatcher: WatcherHandle | null = null;
|
|
48
|
+
|
|
49
|
+
function startRotorWatcher(): void {
|
|
50
|
+
// Already running — restart to pick up new cwd
|
|
51
|
+
rotorWatcher?.stop();
|
|
52
|
+
rotorWatcher = watchRotors(cwd, {
|
|
53
|
+
onChange: () => {
|
|
54
|
+
refreshCycle();
|
|
55
|
+
publish();
|
|
56
|
+
rerender();
|
|
57
|
+
},
|
|
58
|
+
onNotify: (msg) => {
|
|
59
|
+
lastUi?.notify(`pi-switch: ${msg}`, "info");
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
46
63
|
|
|
47
64
|
function refreshCycle(): void {
|
|
48
65
|
cycle = availableAgents(cwd);
|
|
@@ -87,6 +104,7 @@ export default function piSwitchExtension(pi: ExtensionAPI) {
|
|
|
87
104
|
refreshCycle();
|
|
88
105
|
publish();
|
|
89
106
|
rerender();
|
|
107
|
+
startRotorWatcher();
|
|
90
108
|
});
|
|
91
109
|
|
|
92
110
|
// ----- before_agent_start: inject system-prompt section -----
|
package/switch/prompt.ts
CHANGED
|
@@ -19,7 +19,7 @@ export const TASK_AGENT_HINTS: ReadonlyArray<{
|
|
|
19
19
|
agent: "scout", emoji: "\ud83d\udd0d",
|
|
20
20
|
why: "codebase recon, patterns, file locations" },
|
|
21
21
|
{ pattern: /\b(plan|design|architect|outline|structure|break\s*down|steps|order)\b/i,
|
|
22
|
-
agent: "
|
|
22
|
+
agent: "worker", emoji: "\ud83d\udccb",
|
|
23
23
|
why: "decompose into ordered steps, identify risks" },
|
|
24
24
|
{ pattern: /\b(review|audit|check|adversarial|critique|find\s+bugs|qa)\b/i,
|
|
25
25
|
agent: "reviewer", emoji: "\ud83d\udc40",
|
|
@@ -28,22 +28,22 @@ export const TASK_AGENT_HINTS: ReadonlyArray<{
|
|
|
28
28
|
agent: "oracle", emoji: "\ud83d\udd2e",
|
|
29
29
|
why: "decision consistency, hidden assumptions, drift detection" },
|
|
30
30
|
{ pattern: /\b(debug|bug|fix|crash|error|stack\s*trace|repro|why\s+is\s+this\s+broken)\b/i,
|
|
31
|
-
agent: "
|
|
31
|
+
agent: "worker", emoji: "\ud83d\udc1e",
|
|
32
32
|
why: "isolated bug investigation with minimal repro" },
|
|
33
33
|
{ pattern: /\b(test|tests|coverage|spec|assert)\b/i,
|
|
34
|
-
agent: "
|
|
34
|
+
agent: "worker", emoji: "\ud83e\uddea",
|
|
35
35
|
why: "test-only work, never modifies prod code" },
|
|
36
36
|
{ pattern: /\b(refactor|clean\s*up|simplify|extract|rename|restructure|no\s+behavior\s+change)\b/i,
|
|
37
|
-
agent: "
|
|
37
|
+
agent: "worker", emoji: "\ud83d\udd04",
|
|
38
38
|
why: "pure refactoring, behavior-preserving" },
|
|
39
39
|
{ pattern: /\b(document|docs|readme|jsdoc|comment|annotate)\b/i,
|
|
40
|
-
agent: "
|
|
40
|
+
agent: "worker", emoji: "\ud83d\udcdd",
|
|
41
41
|
why: "doc updates, READMEs, inline annotations" },
|
|
42
42
|
{ pattern: /\b(implement|build|write\s+code|add\s+feature|create\s+the)\b/i,
|
|
43
43
|
agent: "worker", emoji: "\u26a1",
|
|
44
44
|
why: "generic implementation with all tools" },
|
|
45
45
|
{ pattern: /\b(orchestrate|coordinate|dispatch|chain|run\s+in\s+parallel|first\s+.+\s+then)\b/i,
|
|
46
|
-
agent: "
|
|
46
|
+
agent: "worker", emoji: "\ud83e\udd1d",
|
|
47
47
|
why: "multi-agent orchestration" },
|
|
48
48
|
// Russian keywords (loose match — Russian words inflect heavily; we match
|
|
49
49
|
// word stems, accepting some false positives as the cost of broader coverage)
|
|
@@ -54,7 +54,7 @@ export const TASK_AGENT_HINTS: ReadonlyArray<{
|
|
|
54
54
|
agent: "scout", emoji: "\ud83d\udd0d",
|
|
55
55
|
why: "codebase recon, patterns, file locations" },
|
|
56
56
|
{ pattern: /(спланир|plan|design|architect)/i,
|
|
57
|
-
agent: "
|
|
57
|
+
agent: "worker", emoji: "\ud83d\udccb",
|
|
58
58
|
why: "decompose into ordered steps, identify risks" },
|
|
59
59
|
{ pattern: /(проверь|ревью|аудит|review|audit)/i,
|
|
60
60
|
agent: "reviewer", emoji: "\ud83d\udc40",
|
|
@@ -63,22 +63,22 @@ export const TASK_AGENT_HINTS: ReadonlyArray<{
|
|
|
63
63
|
agent: "oracle", emoji: "\ud83d\udd2e",
|
|
64
64
|
why: "decision consistency, hidden assumptions, drift detection" },
|
|
65
65
|
{ pattern: /(баг|ошибк|почему\s+(?:падает|ломает)|debug|bug|crash|stack\s*trace|repro)/i,
|
|
66
|
-
agent: "
|
|
66
|
+
agent: "worker", emoji: "\ud83d\udc1e",
|
|
67
67
|
why: "isolated bug investigation with minimal repro" },
|
|
68
68
|
{ pattern: /(тест|покрыт|test|coverage|spec|assert)/i,
|
|
69
|
-
agent: "
|
|
69
|
+
agent: "worker", emoji: "\ud83e\uddea",
|
|
70
70
|
why: "test-only work, never modifies prod code" },
|
|
71
71
|
{ pattern: /(рефактор|упрост|refactor|simplify|extract|restructure)/i,
|
|
72
|
-
agent: "
|
|
72
|
+
agent: "worker", emoji: "\ud83d\udd04",
|
|
73
73
|
why: "pure refactoring, behavior-preserving" },
|
|
74
74
|
{ pattern: /(документ|описани|document|readme|jsdoc)/i,
|
|
75
|
-
agent: "
|
|
75
|
+
agent: "worker", emoji: "\ud83d\udcdd",
|
|
76
76
|
why: "doc updates, READMEs, inline annotations" },
|
|
77
77
|
{ pattern: /(реализуй|сделай|напиши|создай|implement|build|add\s+feature|create\s+the)/i,
|
|
78
78
|
agent: "worker", emoji: "\u26a1",
|
|
79
79
|
why: "generic implementation with all tools" },
|
|
80
80
|
{ pattern: /(оркестрируй|координируй|orchestrate|coordinate|dispatch|chain)/i,
|
|
81
|
-
agent: "
|
|
81
|
+
agent: "worker", emoji: "\ud83e\udd1d",
|
|
82
82
|
why: "multi-agent orchestration" },
|
|
83
83
|
];
|
|
84
84
|
|
|
@@ -108,7 +108,7 @@ The current agent is shown in the footer status line as \`[emoji name]\`.
|
|
|
108
108
|
|
|
109
109
|
When you need a specialist for a sub-task, use the right agent via the parent LLM's \`subagent(...)\` call.
|
|
110
110
|
|
|
111
|
-
**
|
|
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
112
|
|
|
113
113
|
**Task → agent heuristics.** Before launching a generic \`subagent(...)\`, scan the request for these keywords:
|
|
114
114
|
|
|
@@ -117,14 +117,14 @@ When you need a specialist for a sub-task, use the right agent via the parent LL
|
|
|
117
117
|
| scout, scan, map, find all, where is, locate, explore codebase, skim | 🔍 scout | codebase recon, patterns, file locations |
|
|
118
118
|
| review, audit, check, adversarial, critique, find bugs, qa | 👀 reviewer | adversarial correctness, security, style review |
|
|
119
119
|
| oracle, decision, tradeoff, compare, which approach, is this wise, drift | 🔮 oracle | decision consistency, hidden assumptions |
|
|
120
|
-
|
|
|
121
|
-
| (anything else) | ⚡ worker | generic implementation, all tools |
|
|
120
|
+
| (anything else, including implement, debug, fix, test, refactor, document, plan) | ⚡ worker | generic implementation, all tools — prefer to do it yourself |
|
|
122
121
|
|
|
123
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.
|
|
124
123
|
|
|
125
124
|
DON'T:
|
|
126
125
|
- Launch a worker for analysis (use oracle/scout/reviewer)
|
|
127
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
128
|
- Spawn soly-worker / soly-debugger / soly-tester — there is only \`soly-manager\`
|
|
129
129
|
- Manually edit \`.soly/agent\` or \`~/.pi-switch/agent\` — use the slash command
|
|
130
130
|
`;
|
|
@@ -21,8 +21,8 @@ describe("buildPiSwitchSection", () => {
|
|
|
21
21
|
expect(s).toContain("scout");
|
|
22
22
|
expect(s).toContain("worker");
|
|
23
23
|
});
|
|
24
|
-
test("mentions
|
|
25
|
-
expect(s).toContain("
|
|
24
|
+
test("mentions worker as the main cycle rotor", () => {
|
|
25
|
+
expect(s).toContain("worker");
|
|
26
26
|
});
|
|
27
27
|
test("explains user-defined", () => {
|
|
28
28
|
expect(s).toMatch(/user[- ]?defined/i);
|
|
@@ -60,36 +60,36 @@ describe("recommendAgent", () => {
|
|
|
60
60
|
expect(recommendAgent("Изучи React Server Components")?.agent).toBe("researcher");
|
|
61
61
|
expect(recommendAgent("Найди инфу про Zustand")?.agent).toBe("researcher");
|
|
62
62
|
});
|
|
63
|
-
test("debug keywords →
|
|
64
|
-
expect(recommendAgent("fix this bug")?.agent).toBe("
|
|
65
|
-
expect(recommendAgent("why is this crash happening")?.agent).toBe("
|
|
66
|
-
expect(recommendAgent("repro the failing test")?.agent).toBe("
|
|
67
|
-
expect(recommendAgent("Почему падает тест?")?.agent).toBe("
|
|
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
68
|
});
|
|
69
|
-
test("refactor keywords →
|
|
70
|
-
expect(recommendAgent("refactor this function")?.agent).toBe("
|
|
71
|
-
expect(recommendAgent("simplify the auth flow")?.agent).toBe("
|
|
72
|
-
expect(recommendAgent("Упрости эту функцию")?.agent).toBe("
|
|
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
73
|
});
|
|
74
|
-
test("test keywords →
|
|
75
|
-
expect(recommendAgent("write tests for the parser")?.agent).toBe("
|
|
76
|
-
expect(recommendAgent("improve coverage")?.agent).toBe("
|
|
77
|
-
expect(recommendAgent("Напиши тесты для парсера")?.agent).toBe("
|
|
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
78
|
});
|
|
79
79
|
test("review keywords → reviewer", () => {
|
|
80
80
|
expect(recommendAgent("review this PR")?.agent).toBe("reviewer");
|
|
81
81
|
expect(recommendAgent("audit the security")?.agent).toBe("reviewer");
|
|
82
82
|
expect(recommendAgent("Проверь этот код")?.agent).toBe("reviewer");
|
|
83
83
|
});
|
|
84
|
-
test("docs keywords →
|
|
85
|
-
expect(recommendAgent("update the readme")?.agent).toBe("
|
|
86
|
-
expect(recommendAgent("add jsdoc to the function")?.agent).toBe("
|
|
87
|
-
expect(recommendAgent("Обнови документацию")?.agent).toBe("
|
|
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
88
|
});
|
|
89
|
-
test("plan keywords →
|
|
90
|
-
expect(recommendAgent("plan the migration")?.agent).toBe("
|
|
91
|
-
expect(recommendAgent("design the API")?.agent).toBe("
|
|
92
|
-
expect(recommendAgent("Спланируй миграцию")?.agent).toBe("
|
|
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
93
|
});
|
|
94
94
|
test("implement keywords → worker", () => {
|
|
95
95
|
expect(recommendAgent("implement the feature")?.agent).toBe("worker");
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// watcher.ts — Hot-reload the rotor cycle when rotor .md files change
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Watches all known rotor home dirs (project + user) and triggers a cycle
|
|
6
|
+
// refresh + brief notify when a .md file is added/removed/changed. The
|
|
7
|
+
// watcher is debounced (editors save in bursts) and stops cleanly on
|
|
8
|
+
// extension reload.
|
|
9
|
+
//
|
|
10
|
+
// Why: previously, adding a new rotor .md to `.agents/` only took effect on
|
|
11
|
+
// the next Ctrl+Tab. With this watcher, the new rotor appears in the next
|
|
12
|
+
// pill render — no user action required.
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as os from "node:os";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { rotorHomeDirs } from "./core.js";
|
|
19
|
+
|
|
20
|
+
/** Debounce window for file events (editors save in bursts). */
|
|
21
|
+
const DEBOUNCE_MS = 200;
|
|
22
|
+
/** Coalesce window for the "rotors reloaded" notify. */
|
|
23
|
+
const NOTIFY_COALESCE_MS = 500;
|
|
24
|
+
|
|
25
|
+
export interface WatcherOptions {
|
|
26
|
+
/** Called when rotors change (debounced). */
|
|
27
|
+
onChange: () => void;
|
|
28
|
+
/** Called with a debounced message about what changed. */
|
|
29
|
+
onNotify?: (message: string) => void;
|
|
30
|
+
/** Override HOME for tests. */
|
|
31
|
+
home?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WatcherHandle {
|
|
35
|
+
stop: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Watch all rotor home dirs for *.md add/remove/change. */
|
|
39
|
+
export function watchRotors(cwd: string | undefined, opts: WatcherOptions): WatcherHandle {
|
|
40
|
+
const home = opts.home ?? process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
41
|
+
const dirs = rotorHomeDirs(cwd).map((d) => d.replace(/^~/, home));
|
|
42
|
+
|
|
43
|
+
const watchers: fs.FSWatcher[] = [];
|
|
44
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
45
|
+
let notifyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
46
|
+
let pendingReasons: string[] = [];
|
|
47
|
+
let stopped = false;
|
|
48
|
+
|
|
49
|
+
const fire = () => {
|
|
50
|
+
if (stopped) return;
|
|
51
|
+
opts.onChange();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const scheduleNotify = (reason: string) => {
|
|
55
|
+
pendingReasons.push(reason);
|
|
56
|
+
if (notifyTimer) clearTimeout(notifyTimer);
|
|
57
|
+
notifyTimer = setTimeout(() => {
|
|
58
|
+
const reasons = [...new Set(pendingReasons)];
|
|
59
|
+
pendingReasons = [];
|
|
60
|
+
notifyTimer = null;
|
|
61
|
+
if (opts.onNotify) {
|
|
62
|
+
const summary =
|
|
63
|
+
reasons.length === 1
|
|
64
|
+
? reasons[0]!
|
|
65
|
+
: `${reasons.length} changes (${reasons.slice(0, 3).join(", ")}${reasons.length > 3 ? "…" : ""})`;
|
|
66
|
+
opts.onNotify(`rotors reloaded (${summary})`);
|
|
67
|
+
}
|
|
68
|
+
}, NOTIFY_COALESCE_MS);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const onEvent = (event: "add" | "change" | "unlink", filename: string | null) => {
|
|
72
|
+
if (stopped) return;
|
|
73
|
+
if (!filename || !filename.endsWith(".md")) return;
|
|
74
|
+
// Skip dotfiles (frontmatter dumps, etc.)
|
|
75
|
+
if (filename.startsWith(".")) return;
|
|
76
|
+
// Coalesce
|
|
77
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
78
|
+
debounceTimer = setTimeout(() => {
|
|
79
|
+
debounceTimer = null;
|
|
80
|
+
scheduleNotify(event);
|
|
81
|
+
fire();
|
|
82
|
+
}, DEBOUNCE_MS);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
for (const dir of dirs) {
|
|
86
|
+
// Ensure dir exists before watching (fs.watch errors on non-existent)
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
} catch { /* ignore */ }
|
|
90
|
+
try {
|
|
91
|
+
const w = fs.watch(dir, { persistent: false }, (_eventType, filename) => {
|
|
92
|
+
onEvent(_eventType as "add" | "change" | "unlink", filename);
|
|
93
|
+
});
|
|
94
|
+
watchers.push(w);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Some dirs may not exist or be unwatchable. Skip silently.
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error(`[pi-soly] cannot watch ${dir}: ${(err as Error).message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
stop: () => {
|
|
104
|
+
stopped = true;
|
|
105
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
106
|
+
if (notifyTimer) clearTimeout(notifyTimer);
|
|
107
|
+
for (const w of watchers) {
|
|
108
|
+
try { w.close(); } catch { /* ignore */ }
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/agents/soly-manager.md
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: soly-manager
|
|
3
|
-
description: Soly workflow executor. Handles any soly task end-to-end — plan, execute, debug, test, review, refactor, document. Reads the workflow brief passed by the parent and picks the right role for the task. The single writer/reviewer for soly projects.
|
|
4
|
-
thinking: high
|
|
5
|
-
systemPromptMode: replace
|
|
6
|
-
inheritProjectContext: true
|
|
7
|
-
inheritSkills: false
|
|
8
|
-
tools: read, grep, find, ls, bash, edit, write
|
|
9
|
-
defaultContext: fork
|
|
10
|
-
defaultReads: context.md, plan.md
|
|
11
|
-
defaultProgress: true
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
You are `soly-manager`: the workflow executor for the **soly** project-management extension.
|
|
15
|
-
|
|
16
|
-
The parent agent passes you a task with one of these roles. **Pick the right one based on the task brief, not on your name:**
|
|
17
|
-
|
|
18
|
-
| Task brief mentions | You are in mode | Your job |
|
|
19
|
-
|---|---|---|
|
|
20
|
-
| implement, build, write code, add feature, create | **worker** | Write the code, run verification, commit |
|
|
21
|
-
| debug, bug, fix, crash, error, repro, broken | **debugger** | Repro → isolate → fix → regression test |
|
|
22
|
-
| test, coverage, spec, assert, only modify tests | **tester** | Write tests, run full suite, never touch prod |
|
|
23
|
-
| review, audit, adversarial, find bugs, qa | **reviewer** | Read-only review with file:line evidence |
|
|
24
|
-
| refactor, simplify, extract, rename, no behavior change | **refactor** | Behavior-preserving structural change |
|
|
25
|
-
| document, readme, jsdoc, comment, intent doc | **documenter** | Update docs, never change product behavior |
|
|
26
|
-
| validate, scope, drift, decision, before committing | **oracle** | Read-only consistency check, no edits |
|
|
27
|
-
| plan, design, outline, structure, decompose | **planner** | Ordered steps with risks; not code |
|
|
28
|
-
|
|
29
|
-
**You are one agent that switches modes. You are not seven agents.** The system prompt above is your only persona — the task brief tells you which hat to wear.
|
|
30
|
-
|
|
31
|
-
## Soly-aware defaults (apply in every mode)
|
|
32
|
-
|
|
33
|
-
**Path discipline — NON-NEGOTIABLE.** All soly-managed files live under `.soly/`:
|
|
34
|
-
- `PLAN.md`, `CONTEXT.md`, `RESEARCH.md`, `SUMMARY.md` → `.soly/phases/<NN>-<slug>/`
|
|
35
|
-
- iteration files → `.soly/iterations/`
|
|
36
|
-
- handoffs → `.soly/HANDOFF.json`, `.soly/.continue-here.md`
|
|
37
|
-
- rules → `.soly/rules/` (NEVER edit — version-controlled)
|
|
38
|
-
- All other files (source code, tests) → normal project dirs
|
|
39
|
-
|
|
40
|
-
**Close-out order** (when working a plan): production-code commit(s) → SUMMARY commit → `STATUS: done` update.
|
|
41
|
-
Once production commits exist, returning without a committed SUMMARY is an **illegal partial-plan state**.
|
|
42
|
-
|
|
43
|
-
**Frontmatter contract** for `PLAN.md`: `id`, `title`, `status: pending|in_progress|done`, `phase`, `depends-on`, `parallelizable`. Read frontmatter first.
|
|
44
|
-
|
|
45
|
-
**pi-todo integration** (auto-tracks plan sub-tasks if `todo_update` tool is available):
|
|
46
|
-
1. At task start: call `todo_update` with all `status: "pending"`
|
|
47
|
-
2. Set first to `in_progress` before starting
|
|
48
|
-
3. Update as you go: `pending` → `in_progress` → `completed`
|
|
49
|
-
4. Clear list (`todo_update({todos: []})`) after SUMMARY committed
|
|
50
|
-
Skip silently if `todo_update` is not available.
|
|
51
|
-
|
|
52
|
-
## Read first (soly-aware order)
|
|
53
|
-
|
|
54
|
-
1. `.soly/STATE.md` — milestone, current position, recent decisions
|
|
55
|
-
2. `.soly/ROADMAP.md` — overall phase plan
|
|
56
|
-
3. The target `PLAN.md` (the contract) if a plan is in scope
|
|
57
|
-
4. `<phase>-CONTEXT.md` if it exists (honor user decisions)
|
|
58
|
-
5. `<phase>-RESEARCH.md` if it exists (use chosen libs/patterns)
|
|
59
|
-
|
|
60
|
-
**Iteration context file** (if the parent references one) is a pre-aggregated bundle. If given, read that INSTEAD of the individual files.
|
|
61
|
-
|
|
62
|
-
## Mode-specific discipline
|
|
63
|
-
|
|
64
|
-
These are the few hard rules per mode. Follow them or fail loudly.
|
|
65
|
-
|
|
66
|
-
### As worker (implement)
|
|
67
|
-
- Atomic edits only — no speculative scaffolding, no TODO comments
|
|
68
|
-
- Per task: read `<read_first>` → minimal correct change → verify `<acceptance_criteria>` (HARD GATE; log deviation after 2 failed fix attempts) → run `<verification>` → commit with `<type>(${PHASE}-${PLAN}): <summary>`
|
|
69
|
-
- Do NOT edit `.soly/rules/`
|
|
70
|
-
|
|
71
|
-
### As debugger (fix)
|
|
72
|
-
- **Reproduce first.** No fix without a repro. If the user gave a stack trace, build a minimal test that triggers it. If they said "X is broken", find one test case that demonstrates it.
|
|
73
|
-
- **Isolate.** Git blame, grep, bisect. State the root cause in one sentence before fixing.
|
|
74
|
-
- **Fix the cause, not the symptom.** Extra null checks, swallowed errors, type casts mask the bug.
|
|
75
|
-
- **Regression test.** If a test would have caught this, write it. Run the full suite.
|
|
76
|
-
|
|
77
|
-
### As tester
|
|
78
|
-
- **Hard rule:** you can edit `*.test.*`, `*.spec.*`, `tests/`, `__tests__/`, `test/`. You CANNOT edit anything else. If a test fails because of a prod bug, STOP and report — don't "fix" the prod code.
|
|
79
|
-
- Match the project's existing test style. Don't introduce a new style.
|
|
80
|
-
- Test behavior, not implementation. Black-box > white-box.
|
|
81
|
-
|
|
82
|
-
### As reviewer (adversarial)
|
|
83
|
-
- **Read-only.** Do NOT edit files. Do NOT fix bugs. Do NOT commit. You produce a review with file:line evidence; the parent decides what to do.
|
|
84
|
-
- Read spec → read test → read impl → diff them. Where do they disagree?
|
|
85
|
-
- Pick 3-4 relevant review angles (correctness, security, performance, maintainability, soly-style).
|
|
86
|
-
- Specific over vague: "Line 47: SQL injection. Use parameterized query." not "the code is buggy".
|
|
87
|
-
|
|
88
|
-
### As refactor
|
|
89
|
-
- **Behavior preservation is the entire point.** If a test starts failing, you've changed behavior — that's a bug, not a refactor.
|
|
90
|
-
- Smallest possible diff. Run tests after EVERY change.
|
|
91
|
-
- Don't refactor AND fix a bug. Two concerns = unreviewable.
|
|
92
|
-
- If you find a bug, stop, log it, finish the refactor without touching it.
|
|
93
|
-
|
|
94
|
-
### As documenter
|
|
95
|
-
- **You do NOT change product code.** You write READMEs, JSDoc, `.soly/docs/`, ADRs.
|
|
96
|
-
- Update, don't append. If the README has an "Architecture" section, edit in place.
|
|
97
|
-
- Link, don't repeat. 5 lines + a link > 50 lines of pasted explanation.
|
|
98
|
-
- Don't add marketing fluff ("this powerful, elegant framework...").
|
|
99
|
-
|
|
100
|
-
### As oracle (validate)
|
|
101
|
-
- **Read-only.** No edits, no code, no new workflow trees.
|
|
102
|
-
- Check: drift, hidden assumptions, scope creep, missing prerequisites, repeated mistakes, unresolved `depends-on`.
|
|
103
|
-
- Sometimes the answer is "this shouldn't be a soly plan at all" — say so.
|
|
104
|
-
- Output: inherited decisions → drift check → hidden assumptions → missing prereqs → scope check → recommendation → confidence.
|
|
105
|
-
|
|
106
|
-
### As planner
|
|
107
|
-
- Output ordered steps with explicit risks. No code. No "let me also...".
|
|
108
|
-
- Each step: description, depends-on, verification (test or command), acceptance criteria.
|
|
109
|
-
- If the parent asks for a plan, give a plan. Don't drift into implementation.
|
|
110
|
-
|
|
111
|
-
## Returning
|
|
112
|
-
|
|
113
|
-
Your final response should follow this shape:
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
Mode: <worker | debugger | tester | reviewer | refactor | documenter | oracle | planner>
|
|
117
|
-
Did: <one-sentence summary of what you did or found>
|
|
118
|
-
Changed files: <list, or "none" for read-only modes>
|
|
119
|
-
Validation: <test/typecheck/build output, or "n/a" for read-only modes>
|
|
120
|
-
Open risks / decisions needing approval: <list, or "none">
|
|
121
|
-
Recommended next step: <one line>
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Be concise. The parent synthesizes, not you.
|