@juicesharp/rpiv-args 1.1.4 → 1.2.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.
Files changed (4) hide show
  1. package/README.md +10 -10
  2. package/args.ts +51 -14
  3. package/index.ts +3 -2
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![npm version](https://img.shields.io/npm/v/@juicesharp/rpiv-args.svg)](https://www.npmjs.com/package/@juicesharp/rpiv-args)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
13
13
 
14
- Pass arguments to your skills like a shell command. `rpiv-args` adds `$1`, `$ARGUMENTS`, `$@`, `${@:N}`, and `${@:N:L}` placeholders to [Pi Agent](https://github.com/badlogic/pi-mono) skills write `/skill:deploy api production` and your skill body sees `$1` = `api`, `$2` = `production`. Skills without placeholders are untouched, so installing `rpiv-args` is safe for any existing skill collection.
14
+ Pass arguments to your skills like a shell command. `rpiv-args` adds `$1`, `$ARGUMENTS`, `$@`, `${@:N}`, and `${@:N:L}` placeholders to [Pi Agent](https://github.com/badlogic/pi-mono) skills - write `/skill:deploy api production` and your skill body sees `$1` = `api`, `$2` = `production`. Skills without placeholders are untouched, so installing `rpiv-args` is safe for any existing skill collection.
15
15
 
16
16
  ## Install
17
17
 
@@ -31,7 +31,7 @@ Or run `/rpiv-setup` if you have `@juicesharp/rpiv-pi` installed.
31
31
  | `${@:N}` | Arguments from position N onward | `/skill:foo a b c` → `${@:2}` = `b c` |
32
32
  | `${@:N:L}` | L arguments starting at position N | `/skill:foo a b c d` → `${@:2:2}` = `b c` |
33
33
 
34
- **Indexing is 1-based** `$1` is the first argument, `$2` is the second.
34
+ **Indexing is 1-based** - `$1` is the first argument, `$2` is the second.
35
35
  Out-of-range positions resolve to an empty string. For `${@:N[:L]}`, `N` is
36
36
  clamped to `≥ 1` and out-of-range slices yield an empty string.
37
37
 
@@ -51,14 +51,14 @@ expansion). When a skill body contains at least one placeholder, the extension:
51
51
  1. Parses arguments using shell-style quoting
52
52
  2. Substitutes all placeholders in the body
53
53
  3. Wraps the result in a `<skill>` block byte-identical to Pi's native format
54
- 4. Appends the raw arguments after the block matches Pi's standard output so any tool that parses `<skill>` blocks continues to work unchanged
54
+ 4. Appends the raw arguments after the block - matches Pi's standard output so any tool that parses `<skill>` blocks continues to work unchanged
55
55
 
56
56
  When no placeholders are found in the skill body, the output is byte-identical
57
- to Pi's built-in expansion zero behavioral change.
57
+ to Pi's built-in expansion - zero behavioral change.
58
58
 
59
59
  ## Writing skills with arguments
60
60
 
61
- ### `$ARGUMENTS` vs `$1` which to use
61
+ ### `$ARGUMENTS` vs `$1` - which to use
62
62
 
63
63
  Use **`$ARGUMENTS`** (or `$@`) when the input is freeform text the LLM should
64
64
  interpret naturally:
@@ -104,7 +104,7 @@ If a positional skill receives natural language input:
104
104
  /skill:migrate-component can you migrate the search bar please
105
105
  ```
106
106
 
107
- → `Migrate the can component from you to migrate.` **broken**.
107
+ → `Migrate the can component from you to migrate.` - **broken**.
108
108
 
109
109
  The LLM is good at interpreting `$ARGUMENTS` as a whole, but positional
110
110
  placeholders blindly split on spaces. Use `$ARGUMENTS` unless your skill has
@@ -130,17 +130,17 @@ argument-hint: [component] [from] [to]
130
130
  ---
131
131
  ```
132
132
 
133
- rpiv-args ignores this field substitution is triggered by placeholders in the body, not the hint.
133
+ rpiv-args ignores this field - substitution is triggered by placeholders in the body, not the hint.
134
134
 
135
135
  **Note**: Pi currently surfaces `argument-hint` in autocomplete for prompt
136
136
  templates (`commands/*.md`) but **not** for skills (`/skill:<name>`). The
137
137
  field is read by Pi but not displayed in the `/skill:` autocomplete UI at
138
- present treat it as documentation metadata until upstream Pi exposes it.
138
+ present - treat it as documentation metadata until upstream Pi exposes it.
139
139
 
140
140
  ### Full example
141
141
 
142
142
  <details>
143
- <summary>Deploy skill SKILL.md, invocation, and the exact text the LLM sees</summary>
143
+ <summary>Deploy skill - SKILL.md, invocation, and the exact text the LLM sees</summary>
144
144
 
145
145
  ```yaml
146
146
  ---
@@ -179,7 +179,7 @@ api production
179
179
  ```
180
180
 
181
181
  Note: the raw arguments (`api production`) are also appended after the
182
- `</skill>` block this is Pi's standard behavior and is preserved for
182
+ `</skill>` block - this is Pi's standard behavior and is preserved for
183
183
  backward compatibility.
184
184
 
185
185
  </details>
package/args.ts CHANGED
@@ -1,19 +1,33 @@
1
1
  /**
2
2
  * rpiv-args — core logic.
3
3
  *
4
- * Intercepts `/skill:<name> <args>` at the input hook and emits a byte-exact
5
- * Pi skill wrapper with opt-in $N/$ARGUMENTS/$@/${@:N[:L]} substitution on
6
- * the body. Falls through (returns {action:"continue"}) when the text is not
7
- * a skill command, the skill is unknown, or the body contains no tokens —
8
- * keeping Pi's built-in behavior 100% intact for today's 17 rpiv-pi skills.
4
+ * Intercepts `/skill:<name> <args>` at the input hook and emits a Pi skill
5
+ * wrapper with opt-in $N/$ARGUMENTS/$@/${@:N[:L]} substitution on the body.
6
+ * Two emit paths:
7
+ * - No-token path: byte-identical to Pi's built-in `_expandSkillCommand`
8
+ * output (wrapper + `\n\n${args}` suffix), preserving full backward
9
+ * compatibility for skills without placeholders.
10
+ * - Token path: substitutes inside the body and INTENTIONALLY drops the
11
+ * trailing `\n\n${args}` suffix. The bare imperative outside the block
12
+ * hijacks LLM attention from the skill workflow; inside-only emission
13
+ * leaves the skill body as the sole user-message payload competing for
14
+ * attention. See architecture.md "System-Prompt Protocol & Token-Path
15
+ * Divergence".
16
+ *
17
+ * Also prepends a skill-invocation protocol to the system prompt every turn
18
+ * (via before_agent_start) so the LLM treats trailing text after `</skill>`
19
+ * as the skill's argument input rather than a separate imperative.
9
20
  *
10
21
  * Byte-exact wrapper requirement: parseSkillBlock regex at
11
22
  * node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js:40
12
- * is the load-bearing contract. Do not reformat the template literal below.
23
+ * is the load-bearing contract for the wrapper itself. Do not reformat the
24
+ * template literal below.
13
25
  */
14
26
 
15
27
  import { readFileSync } from "node:fs";
16
28
  import {
29
+ type BeforeAgentStartEvent,
30
+ type BeforeAgentStartEventResult,
17
31
  type ExtensionAPI,
18
32
  getAgentDir,
19
33
  type InputEvent,
@@ -29,13 +43,13 @@ import {
29
43
  // ---------------------------------------------------------------------------
30
44
 
31
45
  /** Matches any placeholder Pi's substituteArgs would replace. Used as the
32
- * opt-in gate: absent → pass through verbatim (D2). */
46
+ * opt-in gate: absent → pass through verbatim. */
33
47
  const TOKEN_REGEX = /\$(?:\d+|ARGUMENTS|@|\{@:\d+(?::\d+)?\})/;
34
48
 
35
- /** Prefix Pi uses (`agent-session.js:829`). Single-space tokenisation (D7). */
49
+ /** Prefix Pi uses (`agent-session.js:829`). Single-space tokenisation. */
36
50
  const SKILL_PREFIX = "/skill:";
37
51
 
38
- /** Re-entrancy guard (D8). */
52
+ /** Re-entrancy guard. */
39
53
  const WRAPPED_PREFIX = "<skill ";
40
54
 
41
55
  // ---------------------------------------------------------------------------
@@ -151,13 +165,13 @@ function appendArgs(skillBlock: string, args: string): string {
151
165
  export function handleInput(event: InputEvent): InputEventResult {
152
166
  const text = event.text;
153
167
 
154
- // D8 re-entrancy: already-wrapped text (from our own or any other
168
+ // Re-entrancy: already-wrapped text (from our own or any other
155
169
  // extension's {action:"transform"}) passes through untouched.
156
170
  if (text.startsWith(WRAPPED_PREFIX)) return { action: "continue" };
157
171
 
158
172
  if (!text.startsWith(SKILL_PREFIX)) return { action: "continue" };
159
173
 
160
- // D7 single-space tokenisation — byte-match Pi's indexOf(" ") at :831.
174
+ // Single-space tokenisation — byte-match Pi's indexOf(" ") at :831.
161
175
  const spaceIndex = text.indexOf(" ");
162
176
  const skillName = spaceIndex === -1 ? text.slice(SKILL_PREFIX.length) : text.slice(SKILL_PREFIX.length, spaceIndex);
163
177
  const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
@@ -173,17 +187,39 @@ export function handleInput(event: InputEvent): InputEventResult {
173
187
  }
174
188
 
175
189
  const { frontmatter } = parseFrontmatter<{ "argument-hint"?: string }>(content);
176
- void frontmatter; // informational only in v1 — D3
190
+ void frontmatter; // informational only in v1
177
191
  const body = stripFrontmatter(content).trim();
178
192
 
179
- // D2 opt-in gate: if body has no token, emit byte-identical to Pi's :841.
193
+ // Opt-in gate: if body has no token, emit byte-identical to Pi's :841.
180
194
  if (!TOKEN_REGEX.test(body)) {
181
195
  return { action: "transform", text: appendArgs(buildSkillBlock(entry, body), argsString) };
182
196
  }
183
197
 
184
198
  const parsed = parseCommandArgs(argsString);
185
199
  const substituted = substituteArgs(body, parsed);
186
- return { action: "transform", text: appendArgs(buildSkillBlock(entry, substituted), argsString) };
200
+ // Substitution consumes the args do not also append them after </skill>.
201
+ // Bare trailing imperatives hijack LLM attention from the skill body. See architecture.md.
202
+ return { action: "transform", text: buildSkillBlock(entry, substituted) };
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Skill-invocation protocol — prepended to the system prompt every turn via
207
+ // before_agent_start. See architecture.md for rationale and re-application
208
+ // semantics (agent-session.js:112-113 — Pi's canonical per-turn pattern).
209
+ // ---------------------------------------------------------------------------
210
+
211
+ export const SKILL_INVOCATION_PROTOCOL = `## Skill invocation protocol (CRITICAL)
212
+
213
+ A \`<skill name="..." location="...">...</skill>\` block in a user message is a structured invocation. Handle it as follows:
214
+
215
+ 1. The block body defines the workflow you must execute. Follow it.
216
+ 2. Any text after \`</skill>\` is the user's argument input to that skill — never a separate command, even when it reads as an imperative ("create X", "update Y", "delete Z").
217
+ 3. Do not bypass the skill's workflow to act on trailing text directly. The user invoked the skill because they want the skill's workflow applied to that input.
218
+
219
+ `;
220
+
221
+ export function handleBeforeAgentStart(event: BeforeAgentStartEvent): BeforeAgentStartEventResult {
222
+ return { systemPrompt: SKILL_INVOCATION_PROTOCOL + event.systemPrompt };
187
223
  }
188
224
 
189
225
  // ---------------------------------------------------------------------------
@@ -192,6 +228,7 @@ export function handleInput(event: InputEvent): InputEventResult {
192
228
 
193
229
  export function registerArgsHandler(pi: ExtensionAPI): void {
194
230
  pi.on("input", (event) => handleInput(event));
231
+ pi.on("before_agent_start", (event) => handleBeforeAgentStart(event));
195
232
  pi.on("session_start", (event) => {
196
233
  if (event.reason === "reload" || event.reason === "startup") {
197
234
  invalidateSkillIndex();
package/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * rpiv-args — Pi extension entry point.
3
3
  *
4
- * Registers the `input` event handler + a `session_start` cache invalidator.
5
- * All logic lives in args.ts.
4
+ * Registers the `input` event handler, a `before_agent_start` system-prompt
5
+ * augmenter, and a `session_start` cache invalidator. All logic lives in
6
+ * args.ts.
6
7
  */
7
8
 
8
9
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-args",
3
- "version": "1.1.4",
4
- "description": "Pi extension: pass shell-style $1 / $ARGUMENTS placeholders to your Pi skills",
3
+ "version": "1.2.0",
4
+ "description": "Pi extension. Shell-style $1 and $ARGUMENTS placeholders, expanded into your Pi skills at invocation.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",