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 +5 -3
- package/index.ts +4 -0
- package/package.json +1 -1
- package/skills/soly-framework/SKILL.md +30 -9
- package/switch/core.ts +24 -18
- package/switch/index.ts +4 -4
- package/switch/tests/core.test.ts +13 -2
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"),
|
|
42
|
-
path.join(home, ".pi", "agent", "agents"), // pi
|
|
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.
|
|
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
|
-
├── .
|
|
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
|
-
|
|
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
|
-
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
path.join(
|
|
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
|
|
80
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
// ---------------------------------------------------------------------------
|