pi-soly 1.2.0 → 1.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **The project management framework for [pi-coding-agent](https://github.com/nicobailon/pi-coding-agent).**
6
6
 
7
- Plans · State · Subagents · Multi-question picker · Rotor switcher · Live task list.
7
+ Plans · State · Subagents · Multi-question picker · Live task list.
8
8
 
9
9
  One `npm install`. Zero config. Pure magic.
10
10
 
@@ -30,7 +30,7 @@ That's it. Restart pi, and you have:
30
30
 
31
31
  - **A complete project management engine** — plans, state, subagent-driven execution
32
32
  - **A multi-question picker** — `ask_pro` tool for the LLM
33
- - **A rotor switcher** — `Ctrl+Tab` to cycle, footer pill always visible
33
+ - **Live task list + notifications** — see nudge + deprecation warnings as framed Box widgets
34
34
  - **A live task list** — `todo_update` tool renders in the footer
35
35
  - **7 soly agents** installed on first run
36
36
 
@@ -43,7 +43,7 @@ That's it. Restart pi, and you have:
43
43
  | Write your own planning workflow | `/plan`, `/execute`, `/resume`, `/inspect` — ready |
44
44
  | Manually dispatch subagents | `useSolyWorkerSubagents: true` — automatic routing |
45
45
  | 3 different packages for pickers/tasks/agents | One package, one config, one install |
46
- | Rotor name as free text in slash commands | Footer pill + `Ctrl+Tab` + `/rotor` picker |
46
+ | Mode selection (oracle/scout/reviewer) | LLM picks via `subagent(...)` based on task brief |
47
47
  | Re-invent the state machine | `.soly/STATE.md` + auto-managed phases |
48
48
 
49
49
  ---
@@ -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 # dispatch plan to soly-worker subagent
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
@@ -94,30 +94,6 @@ ask_pro({
94
94
  })
95
95
  ```
96
96
 
97
- ### 🎛 Rotor Switcher
98
-
99
- Footer pill that's always there. `Ctrl+Tab` to cycle. No popup, no friction.
100
-
101
- ```
102
- [model] · ⚡ worker · todos 2/5: write tests ← footer, always visible
103
- [model] ▶ 🐢 oracle · todos 2/5: write tests ← after one Ctrl+Tab
104
- [model] · 🔍 scout · todos 2/5: write tests ← after two
105
- ```
106
-
107
- Agents:
108
- - `worker` — default, full read+write
109
- - `oracle` — read-only decision advisor
110
- - `scout` — codebase reconnaissance
111
- - `researcher` — external docs, ecosystem
112
- - `planner` — architecture and decomposition
113
- - `context-builder` — hands off context to other agents
114
- - `reviewer` — adversarial code review, read-only
115
- - `delegate` — chains agents together
116
-
117
- ### 📝 Live Task List
118
-
119
- `todo_update` tool — renders in the footer as `todos 2/5: current action`.
120
-
121
97
  The LLM can update its own task list mid-turn. You watch progress without re-asking.
122
98
 
123
99
  ```ts
@@ -171,18 +147,18 @@ todo_update({
171
147
 
172
148
  ┌──────────────────┐
173
149
  │ switch/ │
174
- rotor switcher
150
+ (no rotors —
175
151
  │ │
176
152
  │ Ctrl+Tab │
177
153
  │ footer pill │
178
- /rotor picker
154
+ removed 1.4)
179
155
  └────────┬─────────┘
180
156
 
181
157
 
182
158
  ┌──────────────────┐
183
159
  │ 7 soly agents │
184
160
  │ │
185
- soly-worker │
161
+ │ worker (cycle)
186
162
  │ soly-debugger │
187
163
  │ soly-tester │
188
164
  │ soly-reviewer │
@@ -196,7 +172,7 @@ todo_update({
196
172
 
197
173
  ## 📚 Documentation
198
174
 
199
- - **Slash commands** — `/plan`, `/execute`, `/resume`, `/inspect`, `/discuss <N>`, `/quick <task>`, `/rotor`
175
+ - **Slash commands** — `/plan`, `/execute`, `/resume`, `/inspect`, `/discuss <N>`, `/quick <task>`
200
176
  - **Tools** — `ask_pro(question[])` and `todo_update(todo[])`
201
177
  - **Events** — `session_start`, `before_agent_start`, `message_end`, `tool_call`, `tool_result`
202
178
  - **State files** — `.soly/STATE.md`, `.soly/ROADMAP.md`, `.soly/phases/<N>-<slug>/<N>-PLAN.md`
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 two kinds of user-scope assets:
5
+ // Soly ships one kind of user-scope asset:
6
6
  //
7
- // 1. Subagent configs → `~/.pi/agent/agents/`
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 both from `~/.pi/agent/`, so on first session_start we
15
- // copy our shipped files there.
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 agents. Respects HOME/USERPROFILE for
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 single file if destination doesn't exist. Idempotent. */
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 (agents + skills). Combined for convenience. */
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
 
@@ -372,8 +418,8 @@ What must the LLM do?
372
418
  };
373
419
  const subcommands: Record<string, SolySub> = {
374
420
  // `agent` subcommand REMOVED — moved to the separate `pi-switch`
375
- // extension as the `/rotor` slash command (footer pill + Ctrl+Tab).
376
- // Soly no longer owns the rotor switcher UI.
421
+ // extension (rotor switcher removed in 1.4.0).
422
+ // Soly no longer owns a rotor switcher.
377
423
  config: {
378
424
  description: "show merged config (per-project + global + defaults); edit .soly/config.json or ~/.soly/config.json",
379
425
  run: () => {
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
- /** Opt-in: install soly-manager agent config to
39
- * ~/.pi/agent/agents/ on session_start. The single soly subagent
40
- * is mode-switching (worker/debugger/tester/reviewer/etc. based on
41
- * the task brief). Off by default — most users don't need a soly-
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
@@ -66,7 +66,7 @@ import { loadIntentDocs, buildIntentSection, loadInlineIntentBodies, type Intent
66
66
 
67
67
  // Built-in sub-features (merged from former pi-asked, pi-agented packages):
68
68
  import piAskExtension from "./ask/index.ts";
69
- import piSwitchExtension from "./switch/index.ts";
69
+
70
70
 
71
71
  export default function solyExtension(pi: ExtensionAPI) {
72
72
  // ============================================================================
@@ -82,18 +82,7 @@ export default function solyExtension(pi: ExtensionAPI) {
82
82
  let sessionCwd = "";
83
83
 
84
84
  // ============================================================================
85
- // Rotor switcher (Shift+Tab cycles through available rotors)
86
- // ============================================================================
87
-
88
85
  // ============================================================================
89
- // Rotor switcher: REMOVED. The rotor cycler is now owned by the
90
- // separate `pi-switch` extension (footer pill + Ctrl+Tab + /rotor slash).
91
- // Soly owns a single subagent (soly-manager.md) and the auto-install on
92
- // opt-in. Workflows read the current agent from
93
- // globalThis.__PI_SWITCH_AGENT__ (set by pi-switch).
94
- // ============================================================================
95
-
96
- // Config (per-project + global + defaults). Refreshed on session_start
97
86
  // and on each session_start (the LLM can call /soly config to view).
98
87
  let activeConfig: SolyConfig = DEFAULT_CONFIG;
99
88
  const getActiveConfig = (): SolyConfig => activeConfig;
@@ -261,7 +250,6 @@ export default function solyExtension(pi: ExtensionAPI) {
261
250
  const hint = buildNextHint(state);
262
251
  const hintGroup = hint ? `${"\x1b[2m"}${hint}${"\x1b[0m"}` : "";
263
252
 
264
- // Agent badge — owned by pi-switch extension (header bar + status line).
265
253
  // Soly doesn't render the agent badge itself.
266
254
  const agentGroup = "";
267
255
 
@@ -324,18 +312,6 @@ export default function solyExtension(pi: ExtensionAPI) {
324
312
  getConfig: getActiveConfig,
325
313
  });
326
314
 
327
- // ============================================================================
328
- // Agent switcher: Ctrl+Shift+A cycles through available subagents.
329
- // (Shift+Tab is taken by pi's thinking-level cycler; Ctrl+Shift+A is unused
330
- // and mnemonic for "A"gent.)
331
- // ============================================================================
332
- // Agent switcher REMOVED — moved to the separate `pi-switch` extension.
333
- // Soly no longer owns Ctrl+Tab, the footer pill, or /rotor slash.
334
- // The current agent is read by soly workflows from
335
- // globalThis.__PI_SWITCH_AGENT__ (set by pi-switch), with a fallback
336
- // to "worker" if pi-switch isn't installed.
337
- // ============================================================================
338
-
339
315
  registerWorkflows(pi, {
340
316
  getState: () => state,
341
317
  getInteractiveRules: () =>
@@ -354,7 +330,7 @@ export default function solyExtension(pi: ExtensionAPI) {
354
330
  pi.on("session_start", async (event, ctx) => {
355
331
  // Deprecation warning: if the project still uses `.soly/`, nudge the
356
332
  // user toward `.agents/`. One-time per session.
357
- if (isLegacySolyDir(ctx.cwd)) {
333
+ if (activeConfig.agent.useSolyWorkerSubagents && isLegacySolyDir(ctx.cwd)) {
358
334
  notifyDeprecation(
359
335
  ctx.ui,
360
336
  `.soly/ (legacy)`,
@@ -405,21 +381,22 @@ export default function solyExtension(pi: ExtensionAPI) {
405
381
  }
406
382
  }
407
383
 
408
- // Auto-install soly user-scope assets (soly-manager agent + soly-framework
409
- // skill) to ~/.pi/agent/ on first run. Opt-in via config
410
- // `agent.useSolyWorkerSubagents` (default false). Idempotent — respects
411
- // any existing user-customized copies.
384
+ // Auto-install soly user-scope assets (soly-framework skill only,
385
+ // since 1.3.0 we don't ship a subagent) to ~/.pi/agent/skills/ on
386
+ // first run. Opt-in via config `agent.useSolyWorkerSubagents`
387
+ // (kept for backward compat, now a no-op for the skill install).
388
+ // Idempotent — respects any existing user-customized copies.
412
389
  if (activeConfig.agent.useSolyWorkerSubagents) {
413
390
  const extRoot = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
414
391
  const assets = installSolyAssets(extRoot);
415
- const installed = [...assets.agents.installed, ...assets.skills.installed.map((s) => `skill:${s}`)];
392
+ const installed = assets.skills.installed.map((s) => `skill:${s}`);
416
393
  if (installed.length > 0) {
417
394
  ctx.ui.notify(
418
- `soly: installed (${installed.join(", ")}) — run \`/subagents-doctor\` to verify`,
395
+ `soly: installed (${installed.join(", ")}) — pi will discover on next session`,
419
396
  "info",
420
397
  );
421
398
  }
422
- for (const e of [...assets.agents.errors, ...assets.skills.errors]) {
399
+ for (const e of assets.skills.errors) {
423
400
  ctx.ui.notify(`soly: install error — ${e}`, "warning");
424
401
  }
425
402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "1.2.0",
3
+ "version": "1.4.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,10 +38,8 @@
38
38
  "scratchpad.ts",
39
39
  "tools.ts",
40
40
  "agents-install.ts",
41
- "agents",
42
41
  "ask",
43
42
  "skills",
44
- "switch",
45
43
  "workflows",
46
44
  "workflows-data"
47
45
  ],