@zaganjade/pi-multi-skill 1.3.2 → 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 +22 -2
- package/package.json +4 -1
- package/src/build.ts +50 -7
package/README.md
CHANGED
|
@@ -207,17 +207,37 @@ Pi collapses user messages only when the **entire** message matches its native s
|
|
|
207
207
|
<skill name="skill-a, skill-b" location="pi-multi-skill">
|
|
208
208
|
<manually_attached_skills count="2" bundles="@debug">
|
|
209
209
|
…priority rules…
|
|
210
|
-
<skill name="skill-a" location="/path/to/SKILL.md">…</skill>
|
|
211
|
-
<skill name="skill-b" location="/path/to/SKILL.md">…</skill>
|
|
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
212
|
<user_query>Your instructions here</user_query>
|
|
213
213
|
</manually_attached_skills>
|
|
214
214
|
</skill>
|
|
215
|
+
|
|
216
|
+
Your instructions here
|
|
215
217
|
```
|
|
216
218
|
|
|
217
219
|
- **1 skill, no extras** → single native block → `[skill] skill-name`
|
|
218
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`.
|
|
219
224
|
- Inner `<manually_attached_skills bundles="…">` is preserved for **pi-usage** bundle attribution
|
|
220
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
|
+
|
|
221
241
|
### Session hints
|
|
222
242
|
|
|
223
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.
|
|
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",
|
package/src/build.ts
CHANGED
|
@@ -7,6 +7,26 @@ import { buildParallelDispatchBlock } from "./subagents.ts";
|
|
|
7
7
|
|
|
8
8
|
const SUBAGENT_STOP = "<SUBAGENT-STOP>";
|
|
9
9
|
const MULTI_SKILL_LOCATION = "pi-multi-skill";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Closing tag for INNER per-skill blocks inside a multi-skill payload.
|
|
13
|
+
*
|
|
14
|
+
* MUST NOT be `</skill>`. Pi's display parser (`parseSkillBlock`) uses a
|
|
15
|
+
* non-greedy regex that cannot distinguish a nested `</skill>` from the outer
|
|
16
|
+
* envelope's own closing tag. With nested `</skill>`, the parser split at the
|
|
17
|
+
* last inner `</skill>` and dumped every trailing section (`<user_query>`,
|
|
18
|
+
* `</manually_attached_skills>`, the outer `</skill>`) into the rendered
|
|
19
|
+
* `userMessage` — appearing as raw tags in the chat. Using a non-colliding
|
|
20
|
+
* closer keeps the outer envelope parseable so those sections stay inside the
|
|
21
|
+
* collapsed content.
|
|
22
|
+
*
|
|
23
|
+
* The opening tag stays `<skill name="…" location="…">` so pi-usage can still
|
|
24
|
+
* attribute each skill individually via `/<skill\s+name="([^"]+)"/g`; pi-usage
|
|
25
|
+
* only reads openings, so a non-colliding closer is safe. `</skill-block>` is
|
|
26
|
+
* chosen because it contains neither `</skill>` nor `<skill ` (with a space),
|
|
27
|
+
* so it cannot match either parser's regex.
|
|
28
|
+
*/
|
|
29
|
+
const INNER_SKILL_CLOSE = "</skill-block>";
|
|
10
30
|
const SKILL_CHECK_MARKERS = [
|
|
11
31
|
"If you think there is even a 1% chance a skill might apply",
|
|
12
32
|
"Invoke relevant or requested skills BEFORE any response",
|
|
@@ -36,6 +56,21 @@ export function formatPiSkillBlock(
|
|
|
36
56
|
return `<skill name="${escapeXml(name)}" location="${escapeXml(location)}">\n${content}\n</skill>`;
|
|
37
57
|
}
|
|
38
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Wrap a single skill's content for inclusion INSIDE a multi-skill payload.
|
|
61
|
+
*
|
|
62
|
+
* Uses a non-colliding closer (see INNER_SKILL_CLOSE) so the outer native skill
|
|
63
|
+
* block parses cleanly with no tag leakage. The opening stays
|
|
64
|
+
* `<skill name="…">` to preserve pi-usage per-skill attribution.
|
|
65
|
+
*/
|
|
66
|
+
function formatInnerSkillItem(
|
|
67
|
+
name: string,
|
|
68
|
+
location: string,
|
|
69
|
+
content: string,
|
|
70
|
+
): string {
|
|
71
|
+
return `<skill name="${escapeXml(name)}" location="${escapeXml(location)}">\n${content}\n${INNER_SKILL_CLOSE}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
export function isPiSkillBlock(text: string): boolean {
|
|
40
75
|
return PI_SKILL_BLOCK_RE.test(text);
|
|
41
76
|
}
|
|
@@ -163,7 +198,7 @@ function renderSkillBlock(
|
|
|
163
198
|
content = formatFullBody(skill, body);
|
|
164
199
|
}
|
|
165
200
|
|
|
166
|
-
return
|
|
201
|
+
return formatInnerSkillItem(skill.name, skill.filePath, content);
|
|
167
202
|
}
|
|
168
203
|
|
|
169
204
|
function buildAgentPayload(
|
|
@@ -219,12 +254,11 @@ function buildAgentPayload(
|
|
|
219
254
|
);
|
|
220
255
|
}
|
|
221
256
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
257
|
+
// NOTE: options.instructions is intentionally NOT added inside the payload.
|
|
258
|
+
// It is appended after the outer </skill> envelope in buildCombinedMessage
|
|
259
|
+
// so Pi renders it as the visible user message (the `userMessage` tail of
|
|
260
|
+
// parseSkillBlock) while the skill content stays collapsed. Wrapping it in
|
|
261
|
+
// <user_query> tags here would leak those tags into the rendered message.
|
|
228
262
|
|
|
229
263
|
if (notFound.length > 0) {
|
|
230
264
|
parts.push("");
|
|
@@ -302,6 +336,15 @@ export function buildCombinedMessage(
|
|
|
302
336
|
message = expandedBlocks[0];
|
|
303
337
|
}
|
|
304
338
|
|
|
339
|
+
// Surface the user's free-text instructions as Pi's `userMessage` tail
|
|
340
|
+
// (the optional text after `</skill>\n\n`). Pi renders the skill block
|
|
341
|
+
// collapsed as `[skill] a, b (ctrl+o to expand)` and this tail as a normal
|
|
342
|
+
// visible user message below it — which is the desired display. Plain text
|
|
343
|
+
// only: wrapping tags here would render literally.
|
|
344
|
+
if (options.instructions && expandedBlocks.length > 0) {
|
|
345
|
+
message = `${message}\n\n${options.instructions}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
305
348
|
return {
|
|
306
349
|
message,
|
|
307
350
|
notFound,
|