@zaganjade/pi-multi-skill 1.3.1 → 1.3.4

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
@@ -199,19 +199,45 @@ Skills are found from **all** of these sources (merged, deduplicated by name):
199
199
 
200
200
  This means bundles like `@debug` and `@cc-feature` work even when Superpowers skills live in the Claude plugin cache rather than `~/.claude/skills`.
201
201
 
202
- ### Claude Code-style message wrapper
202
+ ### Pi-native skill display
203
203
 
204
- Combined output uses a structured wrapper (similar to Cursor `manually_attached_skills`):
204
+ Pi collapses user messages only when the **entire** message matches its native skill envelope:
205
205
 
206
206
  ```xml
207
- <manually_attached_skills count="3">
208
- …priority rules…
209
- <skill name="" mode="meta">…</skill>
210
- <embedded_command>/workflow-status</embedded_command>
211
- <user_query>Your instructions here</user_query>
212
- </manually_attached_skills>
207
+ <skill name="skill-a, skill-b" location="pi-multi-skill">
208
+ <manually_attached_skills count="2" bundles="@debug">
209
+ …priority rules
210
+ <skill name="skill-a" location="/path/to/SKILL.md">…</skill-block>
211
+ <skill name="skill-b" location="/path/to/SKILL.md">…</skill-block>
212
+ <user_query>Your instructions here</user_query>
213
+ </manually_attached_skills>
214
+ </skill>
215
+
216
+ Your instructions here
213
217
  ```
214
218
 
219
+ - **1 skill, no extras** → single native block → `[skill] skill-name`
220
+ - **2+ skills** (or bundles/instructions) → outer native block hides all inner content → `[skill] a, b, c` (Ctrl+O to expand)
221
+ - The user's free-text instructions are appended AFTER `</skill>` as Pi's
222
+ `userMessage` tail, so they render as a normal visible message below the
223
+ collapsed header: `[skill] a, b (ctrl+o to expand)` + `your instructions`.
224
+ - Inner `<manually_attached_skills bundles="…">` is preserved for **pi-usage** bundle attribution
225
+
226
+ > **Why inner blocks close with `</skill-block>` and instructions live outside the envelope?**
227
+ > Two constraints shaped this. First, Pi's display parser matches the outer
228
+ > envelope with a non-greedy regex and cannot tell a nested `</skill>` apart
229
+ > from the envelope's own closing tag — with nested `</skill>`, any trailing
230
+ > section caused the parser to split at the last inner `</skill>` and **leak**
231
+ > `<user_query>…`, `</manually_attached_skills>`, and the outer `</skill>` into
232
+ > the rendered user message as raw tags. Inner blocks therefore open with
233
+ > `<skill name="…">` (kept so **pi-usage** still attributes each skill via
234
+ > `/<skill\s+name="([^"]+)"/g`) but close with the non-colliding `</skill-block>`,
235
+ > which neither parser's regex can match. Second, so the user's typed
236
+ > instructions stay visible (not hidden inside the collapsed block) while the
237
+ > skill *content* collapses, instructions are placed after `</skill>\n\n` as
238
+ > plain text — exactly Pi's native `userMessage` tail. Covered by
239
+ > `node --test test/parse-leak.test.mjs`.
240
+
215
241
  ### Session hints
216
242
 
217
243
  After each user turn, the extension may suggest a relevant bundle (non-blocking `info` notification):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaganjade/pi-multi-skill",
3
- "version": "1.3.1",
3
+ "version": "1.3.4",
4
4
  "description": "Chain multiple skills via /skills — bundles (JSON/YAML), load modes, BMAD --auto, /skills-last, /skills-setup, conflict resolution, parallel dispatch, activation stats, bundle attribution for pi-usage.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -21,6 +21,9 @@
21
21
  "url": "https://github.com/ZaganJade/pi-extension/issues"
22
22
  },
23
23
  "main": "./src/index.ts",
24
+ "scripts": {
25
+ "test": "node --test test/"
26
+ },
24
27
  "files": [
25
28
  "src/",
26
29
  "README.md",
@@ -1,17 +1,17 @@
1
- {
2
- "bundles": {
3
- "my-stack": {
4
- "description": "Only skills installed on this machine — no BMAD required",
5
- "skills": ["frontend-design", "create-rule"],
6
- "order": "explicit",
7
- "default_mode": "meta"
8
- },
9
- "my-team-planning": {
10
- "description": "Custom team planning workflow (requires BMAD)",
11
- "skills": ["bmad-master", "analyst", "pm"],
12
- "order": "process-first",
13
- "default_mode": "meta",
14
- "requires": "BMAD Method"
15
- }
16
- }
17
- }
1
+ {
2
+ "bundles": {
3
+ "my-stack": {
4
+ "description": "Only skills installed on this machine — no BMAD required",
5
+ "skills": ["frontend-design", "create-rule"],
6
+ "order": "explicit",
7
+ "default_mode": "meta"
8
+ },
9
+ "my-team-planning": {
10
+ "description": "Custom team planning workflow (requires BMAD)",
11
+ "skills": ["bmad-master", "analyst", "pm"],
12
+ "order": "process-first",
13
+ "default_mode": "meta",
14
+ "requires": "BMAD Method"
15
+ }
16
+ }
17
+ }
package/src/bmad-auto.ts CHANGED
@@ -1,99 +1,99 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- const PHASE_SKILLS: Record<string, string[]> = {
5
- analysis: ["bmad-master", "analyst"],
6
- planning: ["bmad-master", "analyst", "pm"],
7
- solutioning: ["bmad-master", "architect", "ux-designer"],
8
- implementation: ["bmad-master", "developer", "scrum-master"],
9
- };
10
-
11
- function readText(path: string): string | null {
12
- try {
13
- return readFileSync(path, "utf-8");
14
- } catch {
15
- return null;
16
- }
17
- }
18
-
19
- function workflowStatus(content: string, key: string): string | null {
20
- const patterns = [
21
- new RegExp(`${key}:\\s*\\n[\\s\\S]*?status:\\s*([\\w-]+)`, "i"),
22
- new RegExp(`${key}:[\\s\\S]*?status:\\s*([\\w\\s-]+)`, "i"),
23
- ];
24
- for (const re of patterns) {
25
- const match = content.match(re);
26
- if (match) return match[1].trim().toLowerCase().replace(/\s+/g, "-");
27
- }
28
- return null;
29
- }
30
-
31
- function isIncomplete(status: string | null): boolean {
32
- if (!status) return true;
33
- return /not-started|not_started|pending|required|in-progress|in_progress|started/.test(
34
- status,
35
- );
36
- }
37
-
38
- function detectPhase(content: string): keyof typeof PHASE_SKILLS {
39
- const checks: Array<[string, keyof typeof PHASE_SKILLS]> = [
40
- ["product-brief", "analysis"],
41
- ["brainstorm", "analysis"],
42
- ["research", "analysis"],
43
- ["prd", "planning"],
44
- ["tech-spec", "planning"],
45
- ["tech_spec", "planning"],
46
- ["architecture", "solutioning"],
47
- ["ux-design", "solutioning"],
48
- ["sprint-planning", "implementation"],
49
- ["dev-story", "implementation"],
50
- ["create-story", "implementation"],
51
- ];
52
-
53
- for (const [key, phase] of checks) {
54
- if (isIncomplete(workflowStatus(content, key))) return phase;
55
- }
56
-
57
- if (/implementation|phase:\s*4/i.test(content)) return "implementation";
58
- if (/solutioning|phase:\s*3/i.test(content)) return "solutioning";
59
- if (/planning|phase:\s*2/i.test(content)) return "planning";
60
- return "analysis";
61
- }
62
-
63
- function projectLevel(cwd: string): number {
64
- const configPath = join(cwd, "bmad", "config.yaml");
65
- const content = readText(configPath);
66
- if (!content) return 1;
67
- const match = content.match(/project_level:\s*(\d)/i);
68
- return match ? Number.parseInt(match[1], 10) : 1;
69
- }
70
-
71
- export function resolveBmadAutoSkills(cwd: string): string[] {
72
- const statusPath = join(cwd, "docs", "bmm-workflow-status.yaml");
73
- const content = readText(statusPath);
74
-
75
- if (!content) {
76
- const level = projectLevel(cwd);
77
- if (level <= 1) return ["bmad-master", "pm"];
78
- return ["bmad-master", "analyst"];
79
- }
80
-
81
- const phase = detectPhase(content);
82
- const skills = [...(PHASE_SKILLS[phase] ?? ["bmad-master"])];
83
-
84
- const level = projectLevel(cwd);
85
- if (level <= 1 && phase === "planning" && !skills.includes("pm")) {
86
- skills.push("pm");
87
- }
88
-
89
- return [...new Set(skills)];
90
- }
91
-
92
- export function bmadAutoHint(cwd: string): string | null {
93
- const statusPath = join(cwd, "docs", "bmm-workflow-status.yaml");
94
- if (!existsSync(statusPath)) {
95
- return "BMAD status not found — loaded bmad-master with planning defaults";
96
- }
97
- const phase = detectPhase(readText(statusPath) ?? "");
98
- return `BMAD --auto detected phase: ${phase}`;
99
- }
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const PHASE_SKILLS: Record<string, string[]> = {
5
+ analysis: ["bmad-master", "analyst"],
6
+ planning: ["bmad-master", "analyst", "pm"],
7
+ solutioning: ["bmad-master", "architect", "ux-designer"],
8
+ implementation: ["bmad-master", "developer", "scrum-master"],
9
+ };
10
+
11
+ function readText(path: string): string | null {
12
+ try {
13
+ return readFileSync(path, "utf-8");
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function workflowStatus(content: string, key: string): string | null {
20
+ const patterns = [
21
+ new RegExp(`${key}:\\s*\\n[\\s\\S]*?status:\\s*([\\w-]+)`, "i"),
22
+ new RegExp(`${key}:[\\s\\S]*?status:\\s*([\\w\\s-]+)`, "i"),
23
+ ];
24
+ for (const re of patterns) {
25
+ const match = content.match(re);
26
+ if (match) return match[1].trim().toLowerCase().replace(/\s+/g, "-");
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function isIncomplete(status: string | null): boolean {
32
+ if (!status) return true;
33
+ return /not-started|not_started|pending|required|in-progress|in_progress|started/.test(
34
+ status,
35
+ );
36
+ }
37
+
38
+ function detectPhase(content: string): keyof typeof PHASE_SKILLS {
39
+ const checks: Array<[string, keyof typeof PHASE_SKILLS]> = [
40
+ ["product-brief", "analysis"],
41
+ ["brainstorm", "analysis"],
42
+ ["research", "analysis"],
43
+ ["prd", "planning"],
44
+ ["tech-spec", "planning"],
45
+ ["tech_spec", "planning"],
46
+ ["architecture", "solutioning"],
47
+ ["ux-design", "solutioning"],
48
+ ["sprint-planning", "implementation"],
49
+ ["dev-story", "implementation"],
50
+ ["create-story", "implementation"],
51
+ ];
52
+
53
+ for (const [key, phase] of checks) {
54
+ if (isIncomplete(workflowStatus(content, key))) return phase;
55
+ }
56
+
57
+ if (/implementation|phase:\s*4/i.test(content)) return "implementation";
58
+ if (/solutioning|phase:\s*3/i.test(content)) return "solutioning";
59
+ if (/planning|phase:\s*2/i.test(content)) return "planning";
60
+ return "analysis";
61
+ }
62
+
63
+ function projectLevel(cwd: string): number {
64
+ const configPath = join(cwd, "bmad", "config.yaml");
65
+ const content = readText(configPath);
66
+ if (!content) return 1;
67
+ const match = content.match(/project_level:\s*(\d)/i);
68
+ return match ? Number.parseInt(match[1], 10) : 1;
69
+ }
70
+
71
+ export function resolveBmadAutoSkills(cwd: string): string[] {
72
+ const statusPath = join(cwd, "docs", "bmm-workflow-status.yaml");
73
+ const content = readText(statusPath);
74
+
75
+ if (!content) {
76
+ const level = projectLevel(cwd);
77
+ if (level <= 1) return ["bmad-master", "pm"];
78
+ return ["bmad-master", "analyst"];
79
+ }
80
+
81
+ const phase = detectPhase(content);
82
+ const skills = [...(PHASE_SKILLS[phase] ?? ["bmad-master"])];
83
+
84
+ const level = projectLevel(cwd);
85
+ if (level <= 1 && phase === "planning" && !skills.includes("pm")) {
86
+ skills.push("pm");
87
+ }
88
+
89
+ return [...new Set(skills)];
90
+ }
91
+
92
+ export function bmadAutoHint(cwd: string): string | null {
93
+ const statusPath = join(cwd, "docs", "bmm-workflow-status.yaml");
94
+ if (!existsSync(statusPath)) {
95
+ return "BMAD status not found — loaded bmad-master with planning defaults";
96
+ }
97
+ const phase = detectPhase(readText(statusPath) ?? "");
98
+ return `BMAD --auto detected phase: ${phase}`;
99
+ }