pi-soly 0.8.0 → 0.9.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/agents-install.ts CHANGED
@@ -34,12 +34,14 @@ const SHIPPED_SKILLS = [
34
34
  ] as const;
35
35
 
36
36
  /** Where pi looks for user agents. Respects HOME/USERPROFILE for
37
- * testability (otherwise we'd always write to the real user home). */
37
+ * testability (otherwise we'd always write to the real user home).
38
+ * Path scheme: `<root>/.agents/agents/` (vendor-neutral) and
39
+ * `<root>/.pi/agent/agents/` (pi native, legacy). */
38
40
  function userAgentsDirs(): string[] {
39
41
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
40
42
  return [
41
- path.join(home, ".agents"), // vendor-neutral (preferred)
42
- path.join(home, ".pi", "agent", "agents"), // pi's native
43
+ path.join(home, ".agents", "agents"), // vendor-neutral (preferred)
44
+ path.join(home, ".pi", "agent", "agents"), // pi native
43
45
  ];
44
46
  }
45
47
 
package/index.ts CHANGED
@@ -353,10 +353,14 @@ export default function solyExtension(pi: ExtensionAPI) {
353
353
  // Rules sources (priority order, higher wins on relPath collision).
354
354
  // Project rules always beat global rules. .soly/rules.local/ is
355
355
  // gitignored — for personal overrides on top of the project's rules.
356
+ // .agents/rules/ is the vendor-neutral project-level convention
357
+ // (same role as the old .claude/rules/).
356
358
  ruleSources = [
357
359
  { dir: path.join(ctx.cwd, ".soly", "rules.local"), source: "project-soly", sourceLabel: "local", priority: 5 },
358
360
  { dir: path.join(ctx.cwd, ".soly", "rules"), source: "project-soly", sourceLabel: "soly", priority: 4 },
361
+ { dir: path.join(ctx.cwd, ".agents", "rules"), source: "project-agents", sourceLabel: "agents", priority: 3 },
359
362
  { dir: path.join(os.homedir(), ".soly", "rules"), source: "global-soly", sourceLabel: "soly", priority: 2 },
363
+ { dir: path.join(os.homedir(), ".agents", "rules"), source: "global-agents", sourceLabel: "agents", priority: 1 },
360
364
  ];
361
365
  refreshRules();
362
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -45,16 +45,13 @@ The **soly** extension adds project-management workflow to [pi-coding-agent](htt
45
45
  <project-root>/
46
46
  ├── AGENTS.md # vendor-neutral agent context (loaded by pi)
47
47
  ├── agents.md # same as AGENTS.md, lowercase accepted
48
- ├── .agents/ # project-level agent definitions
49
- │ ├── project-reviewer.md
50
- │ └── data-scientist.md
51
- ├── .soly/
48
+ ├── .soly/ # soly state (phases, plans, summaries)
52
49
  │ ├── ROADMAP.md # phase table
53
50
  │ ├── STATE.md # current position + decisions log
54
51
  │ ├── docs/ # 0-point intent docs (human-written, locked)
55
52
  │ │ ├── vision.md
56
53
  │ │ └── architecture.md
57
- │ ├── rules/ # project rules (version-controlled)
54
+ │ ├── rules/ # soly project rules (version-controlled)
58
55
  │ │ ├── code-style.md
59
56
  │ │ └── testing.md
60
57
  │ ├── phases/
@@ -70,8 +67,26 @@ The **soly** extension adds project-management workflow to [pi-coding-agent](htt
70
67
  │ ├── iterations/ # per-execution context bundles (auto)
71
68
  │ ├── HANDOFF.json # pause snapshot
72
69
  │ └── .continue-here.md # pause resume marker
70
+ ├── .agents/ # vendor-neutral agent config (per project)
71
+ │ ├── rules/ # agent rules (loaded with priority 3, after .soly/rules/)
72
+ │ │ ├── code-style.md
73
+ │ │ └── testing.md
74
+ │ ├── skills/ # project-scoped skills (pi auto-discovers)
75
+ │ │ └── my-skill/
76
+ │ │ └── SKILL.md
77
+ │ ├── docs/ # agent-specific docs (intent-style)
78
+ │ │ └── architecture.md
79
+ │ └── agents/ # project-specific agent definitions
80
+ │ ├── project-reviewer.md
81
+ │ └── data-scientist.md
73
82
  ```
74
83
 
84
+ **Two parallel conventions:** `.soly/` is soly-specific state (phases, plans). `.agents/` is vendor-neutral agent config (works with any AI tool that follows the AGENTS.md standard). The two coexist:
85
+
86
+ - Use `.soly/` for soly workflow artifacts (PLAN.md, SUMMARY.md, etc.)
87
+ - Use `.agents/` for things other AI tools should also see (rules, skills, agents)
88
+ - Use `AGENTS.md` for top-level project-wide agent conventions
89
+
75
90
  ## Frontmatter conventions
76
91
 
77
92
  ### PLAN.md frontmatter (required)
@@ -285,7 +300,7 @@ Intent docs are 0-point — written BEFORE any plan, by humans. They define the
285
300
 
286
301
  ### Add project-specific agents
287
302
 
288
- Drop a markdown file in `.agents/<name>.md` (project) or `~/.agents/<name>.md` (user):
303
+ Drop a markdown file in `.agents/agents/<name>.md` (project) or `~/.agents/agents/<name>.md` (user):
289
304
 
290
305
  ```markdown
291
306
  ---
@@ -298,7 +313,13 @@ tools: read, bash
298
313
  You are a data scientist. ...
299
314
  ```
300
315
 
301
- Both `~/.agents/` and `~/.pi/agent/agents/` are read (vendor-neutral preferred). `Ctrl+Tab` to see them in the cycle.
316
+ **Discovered from 4 locations** (priority order):
317
+ 1. `<project>/.agents/agents/` — project vendor-neutral (preferred)
318
+ 2. `<project>/.pi/agent/agents/` — project pi native (legacy)
319
+ 3. `~/.agents/agents/` — user vendor-neutral (preferred)
320
+ 4. `~/.pi/agent/agents/` — user pi native (legacy)
321
+
322
+ `Ctrl+Tab` to see them in the cycle.
302
323
 
303
324
  ### Add a feature to an existing phase
304
325
 
@@ -334,9 +355,9 @@ If `/execute` complains about illegal partial state:
334
355
  - ❌ Edit `.soly/rules/` files you didn't write — those are project invariants
335
356
  - ❌ Skip the SUMMARY — illegal partial state
336
357
  - ❌ Spawn `soly-worker` or `soly-debugger` — use `soly-manager` (mode-switches)
337
- - ❌ Write rules in code comments — use `.soly/rules/*.md` files
358
+ - ❌ Write rules in code comments — use `.soly/rules/*.md` or `.agents/rules/*.md` files
338
359
  - ❌ Edit `.soly/phases/*/PLAN.md` after `status: in_progress` — create a new plan
339
- - ❌ Put intent docs anywhere other than `.soly/docs/`
360
+ - ❌ Put intent docs anywhere other than `.soly/docs/` (or `.agents/docs/` for vendor-neutral)
340
361
 
341
362
  ## When in doubt
342
363
 
package/switch/core.ts CHANGED
@@ -65,23 +65,29 @@ export function isValidAgentName(name: string): boolean {
65
65
  }
66
66
 
67
67
  /** Discover agent `.md` files in user dir. */
68
- /** User agent home directories, in priority order. First existing one
69
- * wins for new agent creation; all are read and merged in the cycle.
70
- * Honors $HOME / $USERPROFILE so tests can redirect to a tmp dir. */
71
- export function userAgentDirs(): string[] {
68
+ /** All known agent home directories, in priority order (project wins
69
+ * over user-home; user-home `.agents/` wins over pi-native).
70
+ * Project-level `.agents/agents/` is a vendor-neutral per-project
71
+ * convention same role as `.soly/` or the old `.claude/`.
72
+ * Honors $HOME / $USERPROFILE for testability. */
73
+ export function agentHomeDirs(cwd?: string): string[] {
72
74
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
73
- return [
74
- path.join(home, ".agents"), // vendor-neutral (preferred)
75
- path.join(home, ".pi", "agent", "agents"), // pi's native
76
- ];
75
+ const dirs: string[] = [];
76
+ if (cwd) {
77
+ dirs.push(path.join(cwd, ".agents", "agents")); // project (vendor-neutral, preferred)
78
+ dirs.push(path.join(cwd, ".pi", "agent", "agents")); // project (pi native, legacy)
79
+ }
80
+ dirs.push(path.join(home, ".agents", "agents")); // user (vendor-neutral)
81
+ dirs.push(path.join(home, ".pi", "agent", "agents")); // user (pi native, legacy)
82
+ return dirs;
77
83
  }
78
84
 
79
- /** Read all user agent names from every registered home dir. Dedupes,
80
- * first-occurrence wins. */
81
- export function discoverUserAgents(): string[] {
85
+ /** Read all agent names from every home dir. Dedupes, first-occurrence
86
+ * wins. If cwd is provided, project dirs are scanned first. */
87
+ export function discoverUserAgents(cwd?: string): string[] {
82
88
  const seen = new Set<string>();
83
89
  const out: string[] = [];
84
- for (const dir of userAgentDirs()) {
90
+ for (const dir of agentHomeDirs(cwd)) {
85
91
  if (!fs.existsSync(dir)) continue;
86
92
  let entries: string[];
87
93
  try {
@@ -111,9 +117,9 @@ export function discoverUserAgents(): string[] {
111
117
  }
112
118
 
113
119
  /** Build the full cycle of available agents. Built-ins first, then
114
- * user-discovered (from all user agent home dirs). Dedupes while
115
- * preserving first-occurrence order. */
116
- export function availableAgents(): string[] {
120
+ * project-level agents (if cwd given), then user-home agents.
121
+ * Dedupes while preserving first-occurrence order. */
122
+ export function availableAgents(cwd?: string): string[] {
117
123
  const out: string[] = [];
118
124
  const seen = new Set<string>();
119
125
  const push = (n: string) => {
@@ -123,7 +129,7 @@ export function availableAgents(): string[] {
123
129
  }
124
130
  };
125
131
  for (const a of BUILTIN_AGENTS) push(a);
126
- for (const a of discoverUserAgents()) push(a);
132
+ for (const a of discoverUserAgents(cwd)) push(a);
127
133
  return out;
128
134
  }
129
135
 
@@ -165,8 +171,8 @@ export function formatAgentSwitchNotify(prev: string, next: string): string {
165
171
  }
166
172
 
167
173
  /** Group agents: built-ins + user-defined. */
168
- export function groupedAvailableAgents(): Array<{ header: string; agents: string[] }> {
169
- const all = availableAgents();
174
+ export function groupedAvailableAgents(cwd?: string): Array<{ header: string; agents: string[] }> {
175
+ const all = availableAgents(cwd);
170
176
  const groups: Array<{ header: string; agents: string[] }> = [];
171
177
  const builtin = all.filter((a) => BUILTIN_AGENTS.includes(a));
172
178
  if (builtin.length > 0) groups.push({ header: "built-in", agents: builtin });
package/switch/index.ts CHANGED
@@ -45,7 +45,7 @@ export default function piSwitchExtension(pi: ExtensionAPI) {
45
45
  let lastUi: ExtensionUIContext | null = null;
46
46
 
47
47
  function refreshCycle(): void {
48
- cycle = availableAgents();
48
+ cycle = availableAgents(cwd);
49
49
  if (!cycle.includes(currentAgent)) currentAgent = DEFAULT_AGENT;
50
50
  }
51
51
 
@@ -177,8 +177,8 @@ function openPicker(ui: ExtensionUIContext): void {
177
177
  function handleSet(name: string, ui: ExtensionUIContext): void {
178
178
  const target = parseAgentName(name);
179
179
  if (!target) return ui.notify(`pi-switch: invalid name "${name}".`, "error");
180
- if (!availableAgents().includes(target)) {
181
- return ui.notify(`pi-switch: unknown "${target}". available: ${availableAgents().join(", ")}`, "error");
180
+ if (!availableAgents(cwd).includes(target)) {
181
+ return ui.notify(`pi-switch: unknown "${target}". available: ${availableAgents(cwd).join(", ")}`, "error");
182
182
  }
183
183
  setAgentRef(target);
184
184
  }
@@ -314,7 +314,7 @@ you have a specific reason to change it.
314
314
  }
315
315
 
316
316
  function doctorReport(): string {
317
- const cycle = availableAgents();
317
+ const cycle = availableAgents(cwd);
318
318
  const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
319
319
  const lines: string[] = ["pi-switch doctor:", ""];
320
320
  const builtins = cycle.filter((a) => BUILTIN_AGENTS.includes(a));
@@ -123,14 +123,14 @@ describe("groupedAvailableAgents", () => {
123
123
  expect(groups[0]?.header).toBe("built-in");
124
124
  });
125
125
  test("includes user group when present", () => {
126
- // Use HOME override so the new ~.agents/ scan picks up our fixture
126
+ // Use HOME override so the new ~.agents/agents/ scan picks up our fixture
127
127
  const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
128
128
  const prevHome = process.env.HOME;
129
129
  const prevUserProfile = process.env.USERPROFILE;
130
130
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pis-home-"));
131
131
  process.env.HOME = tmp;
132
132
  process.env.USERPROFILE = tmp;
133
- const agentsDir = path.join(tmp, ".agents");
133
+ const agentsDir = path.join(tmp, ".agents", "agents");
134
134
  fs.mkdirSync(agentsDir, { recursive: true });
135
135
  fs.writeFileSync(path.join(agentsDir, "my.md"), "---\nname: my-helper\n---\n# body\n");
136
136
  const groups = groupedAvailableAgents();
@@ -141,6 +141,17 @@ describe("groupedAvailableAgents", () => {
141
141
  process.env.USERPROFILE = prevUserProfile ?? home;
142
142
  fs.rmSync(tmp, { recursive: true, force: true });
143
143
  });
144
+
145
+ test("includes project agent when present (cwd scope)", () => {
146
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "pis-proj-"));
147
+ const agentsDir = path.join(projectDir, ".agents", "agents");
148
+ fs.mkdirSync(agentsDir, { recursive: true });
149
+ fs.writeFileSync(path.join(agentsDir, "proj.md"), "---\nname: project-helper\n---\n# body\n");
150
+ const groups = groupedAvailableAgents(projectDir);
151
+ const userGroup = groups.find((g) => g.header === "user-defined");
152
+ expect(userGroup?.agents).toContain("project-helper");
153
+ fs.rmSync(projectDir, { recursive: true, force: true });
154
+ });
144
155
  });
145
156
 
146
157
  // ---------------------------------------------------------------------------