pi-skill-playbook 0.1.3 → 0.1.5

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
@@ -1,33 +1,39 @@
1
1
  # Pi Skill Playbook
2
2
 
3
- Pi Skill Playbook is a Pi extension that guides Agent Skill usage flows with visible, human-mediated playbooks.
3
+ [![CI](https://github.com/eiei114/pi-skill-playbook/actions/workflows/auto-release.yml/badge.svg)](https://github.com/eiei114/pi-skill-playbook/actions/workflows/auto-release.yml)
4
+ [![Publish](https://github.com/eiei114/pi-skill-playbook/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-skill-playbook/actions/workflows/publish.yml)
5
+ [![npm version](https://img.shields.io/npm/v/pi-skill-playbook.svg)](https://www.npmjs.com/package/pi-skill-playbook)
6
+ [![npm downloads](https://img.shields.io/npm/dt/pi-skill-playbook.svg)](https://www.npmjs.com/package/pi-skill-playbook)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Pi package](https://img.shields.io/badge/Pi-package-blue)](https://github.com/eiei114/pi-skill-playbook)
4
9
 
5
- MVP scope:
10
+ Human-mediated Agent Skill playbooks for the [Pi coding agent](https://github.com/earendil-works/pi-coding-agent).
6
11
 
7
- - `/playbook list`
8
- - `/playbook start <playbook-id> [--run <name>]`
9
- - `/playbook resume <run-id>`
10
- - `/playbook status [run-id]`
11
- - `/playbook done`
12
- - `/playbook choose <outcome>`
13
- - strict YAML validation
14
- - active run widget below the editor
15
- - marker-based auto advance for single-outcome steps
16
- - local run state in `.pi/playbook-runs/`
12
+ ## What this is
17
13
 
18
- Deferred after scaffold:
14
+ Pi Skill Playbook is a Pi extension that guides Agent Skill usage flows with **visible, human-controlled playbooks**. It shows the current workflow step, next recommended skill command, and completion criteria — but never runs skills automatically.
19
15
 
20
- - `/playbook import-web`
21
- - `/playbook record`
16
+ Define ordered skill workflows as YAML playbooks in your project, then drive them step by step from the Pi command palette.
22
17
 
23
- ## Install from npm
18
+ ## Features
19
+
20
+ - **Playbook-driven workflows** — Define multi-step skill sequences with named outcomes and transitions in YAML.
21
+ - **Human-mediated** — Every step requires an explicit user action. No hidden automation.
22
+ - **Marker-based auto advance** — Single-outcome steps advance automatically when the assistant emits a visible `PLAYBOOK_OUTCOME:` marker. Multi-outcome steps always require explicit confirmation.
23
+ - **Active run widget** — Displays current step, skill command, completion criteria, and outcome labels below the editor.
24
+ - **Strict YAML validation** — Playbooks are validated on load for structure, transitions, and skill references.
25
+ - **Tab completion** — Commands, playbook IDs, run IDs, outcomes, and flags all support tab completion.
26
+ - **Local run state** — Run state is stored in `.pi/playbook-runs/` inside the target project, never in git.
27
+
28
+ ## Install
29
+
30
+ ### From npm
24
31
 
25
32
  ```bash
26
33
  pi install npm:pi-skill-playbook
27
34
  ```
28
35
 
29
-
30
- ## Install from GitHub
36
+ ### From GitHub
31
37
 
32
38
  ```bash
33
39
  pi install git:github.com/eiei114/pi-skill-playbook
@@ -39,9 +45,7 @@ Or with a full Git URL:
39
45
  pi install git+https://github.com/eiei114/pi-skill-playbook.git
40
46
  ```
41
47
 
42
- ## Install locally
43
-
44
- Clone the repository first, then run Pi from the checkout:
48
+ ### Locally (for development)
45
49
 
46
50
  ```bash
47
51
  git clone https://github.com/eiei114/pi-skill-playbook.git
@@ -50,55 +54,141 @@ npm install
50
54
  pi -e .
51
55
  ```
52
56
 
53
- Or install as a local Pi package:
57
+ ## Quick start
54
58
 
55
- ```bash
56
- pi install /path/to/pi-skill-playbook
57
- ```
59
+ 1. **Copy a sample playbook** into your project:
58
60
 
59
- ## Add a playbook to a project
61
+ ```bash
62
+ mkdir -p .pi/playbooks
63
+ cp node_modules/pi-skill-playbook/samples/feature-development.yml .pi/playbooks/
64
+ ```
60
65
 
61
- MVP uses manual sample copy:
66
+ 2. **Add run state to `.gitignore`** in the target project:
62
67
 
63
- ```bash
64
- mkdir -p .pi/playbooks
65
- cp /path/to/pi-skill-playbook/samples/feature-development.yml .pi/playbooks/feature-development.yml
66
- ```
68
+ ```gitignore
69
+ .pi/playbook-runs/
70
+ ```
67
71
 
68
- Add personal run state to the target repo's `.gitignore`:
72
+ 3. **Start a run**:
69
73
 
70
- ```gitignore
71
- .pi/playbook-runs/
72
- ```
74
+ ```
75
+ /playbook:list
76
+ /playbook:start feature-development --run my-feature
77
+ ```
73
78
 
74
- ## Use
79
+ 4. **Drive the workflow**:
75
80
 
76
- ```text
77
- /playbook list
78
- /playbook start feature-development --run my-feature
79
- /playbook done
80
- /playbook choose pass
81
- /playbook status
82
- ```
81
+ ```
82
+ /skill:grill-with-docs <feature idea>
83
+ /playbook:done
84
+ /playbook:choose ready-for-prd
85
+ /playbook:status
86
+ ```
83
87
 
84
88
  The widget displays the current step, exact skill command, completion criteria, and outcome labels.
85
89
 
86
- ## Auto advance
90
+ ## Usage summary
91
+
92
+ | Command | Description |
93
+ |---|---|
94
+ | `/playbook:list` | List available playbooks with validation status |
95
+ | `/playbook:start <id> [--run <name>]` | Start a new playbook run |
96
+ | `/playbook:resume <run-id>` | Resume a paused active run |
97
+ | `/playbook:status [run-id]` | Show current step and completion criteria |
98
+ | `/playbook:done` | Complete the current step (auto-advances if single outcome) |
99
+ | `/playbook:choose <outcome>` | Choose an outcome for multi-branch steps |
100
+ | `/playbook:cancel [run-id]` | Cancel an active run |
101
+
102
+ Legacy space-separated forms (`/playbook start`) remain available for compatibility.
87
103
 
88
- Playbooks default to `autoAdvance: auto`.
104
+ ### Auto advance
89
105
 
90
- When a user explicitly runs the current step skill with `/skill:<name>`, Pi Skill Playbook injects a short prompt that asks the assistant to leave a visible completion marker:
106
+ Playbooks default to `autoAdvance: auto`. When a user explicitly runs the current step's skill with `/skill:<name>`, Pi Skill Playbook injects a short prompt asking the assistant to emit a visible completion marker:
91
107
 
92
108
  ```text
93
109
  PLAYBOOK_OUTCOME: ready-for-prd
94
110
  ```
95
111
 
96
- If the current step has exactly one outcome, a valid marker advances the run automatically. If the step has multiple outcomes, the marker is shown as a suggestion and the user must confirm with `/playbook choose <outcome>`.
112
+ | Mode | Behavior |
113
+ |---|---|
114
+ | `auto` (default) | Marker can advance single-outcome steps automatically |
115
+ | `suggest` | Marker only suggests `/playbook:done` or `/playbook:choose` |
116
+ | `off` | No prompt injection or completion detection |
117
+
118
+ ## Package contents
119
+
120
+ ```
121
+ pi-skill-playbook/
122
+ ├── extensions/ Pi extension entry point
123
+ ├── src/ Domain logic: validation, state, rendering, auto-advance
124
+ ├── samples/ Example playbooks (feature-development.yml)
125
+ ├── tests/ Node.js test suite
126
+ ├── docs/adr/ Architecture decision records
127
+ ├── LICENSE MIT
128
+ └── README.md
129
+ ```
97
130
 
98
- Optional playbook setting:
131
+ ### Playbook YAML structure
99
132
 
100
133
  ```yaml
101
- autoAdvance: auto # default: marker can advance single-outcome steps
102
- autoAdvance: suggest # marker only suggests /playbook done or /playbook choose
103
- autoAdvance: off # no prompt injection or completion detection
134
+ version: 1
135
+ id: my-playbook
136
+ name: My Playbook
137
+ entry: first-step
138
+ autoAdvance: auto # auto | suggest | off
139
+
140
+ skills:
141
+ my-skill:
142
+ role: entry
143
+
144
+ steps:
145
+ first-step:
146
+ primarySkill: my-skill
147
+ commandHint: "/skill:my-skill <arg>"
148
+ doneWhen:
149
+ - Criterion one.
150
+ - Criterion two.
151
+ transitions:
152
+ - outcome: done
153
+ to: complete # "complete" ends the run
104
154
  ```
155
+
156
+ See [`samples/feature-development.yml`](samples/feature-development.yml) for a full example.
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ npm install
162
+ npm run check # TypeScript type check
163
+ npm test # Run tests (Node.js built-in test runner + tsx)
164
+ ```
165
+
166
+ Requires Node.js ≥ 24, TypeScript 5.8+, and tsx 4.20+.
167
+
168
+ ## Release
169
+
170
+ Releases are automated via GitHub Actions:
171
+
172
+ 1. Bump `version` in `package.json` and merge to `main`.
173
+ 2. `auto-release.yml` detects the new version, creates a git tag and GitHub Release.
174
+ 3. `publish.yml` publishes to npm with provenance.
175
+
176
+ Manual dispatch is also available from the Actions tab.
177
+
178
+ ## Security
179
+
180
+ - Run state is local-only (`.pi/playbook-runs/`). No data leaves the machine.
181
+ - The extension does **not** inject system prompts for skill execution. It only adds a short playbook prompt describing the current step, valid outcomes, and expected marker format.
182
+ - No automatic file edits — the extension warns with a `.gitignore` snippet instead of modifying files.
183
+ - Report vulnerabilities via [GitHub Security Advisories](https://github.com/eiei114/pi-skill-playbook/security/advisories/new).
184
+
185
+ ## Links
186
+
187
+ - [npm package](https://www.npmjs.com/package/pi-skill-playbook)
188
+ - [GitHub repository](https://github.com/eiei114/pi-skill-playbook)
189
+ - [Pi coding agent](https://github.com/earendil-works/pi-coding-agent)
190
+ - [Architecture decisions](docs/adr/)
191
+
192
+ ## License
193
+
194
+ [MIT](LICENSE)
@@ -9,6 +9,30 @@ import type { LoadedPlaybook, PlaybookRunState } from "../src/types.js";
9
9
 
10
10
  const WIDGET_ID = "pi-skill-playbook";
11
11
 
12
+ const COMMANDS = [
13
+ ["list", "list available playbooks"],
14
+ ["start", "start a playbook run"],
15
+ ["resume", "resume an active playbook run"],
16
+ ["status", "show playbook run status"],
17
+ ["done", "complete the current step"],
18
+ ["choose", "choose a step outcome"],
19
+ ["cancel", "cancel an active playbook run"],
20
+ ] as const;
21
+
22
+ const COLON_COMMAND_ALIASES = COMMANDS.map(([command, description]) => ({
23
+ name: `playbook:${command}`,
24
+ command,
25
+ description,
26
+ }));
27
+
28
+ const COLON_COMPLETION_COMMANDS = new Set(["start", "resume", "status", "cancel", "choose"]);
29
+
30
+ type CommandContext = {
31
+ cwd: string;
32
+ hasUI: boolean;
33
+ ui?: UiLike;
34
+ };
35
+
12
36
  export default function piSkillPlaybook(pi: ExtensionAPI) {
13
37
  let completionCwd = process.cwd();
14
38
  let pendingSkillInvocation: string | undefined;
@@ -65,44 +89,70 @@ export default function piSkillPlaybook(pi: ExtensionAPI) {
65
89
  handler: async (args, ctx) => {
66
90
  const parsed = parseArgs(args);
67
91
  const command = parsed.shift() ?? "status";
68
-
69
92
  try {
70
- switch (command) {
71
- case "list":
72
- await listPlaybooks(pi, ctx.cwd, ctx.hasUI ? ctx.ui : undefined);
73
- return;
74
- case "start":
75
- await startPlaybook(pi, ctx.cwd, parsed, ctx.hasUI ? ctx.ui : undefined);
76
- return;
77
- case "resume":
78
- await resumeRun(ctx.cwd, parsed, ctx.hasUI ? ctx.ui : undefined);
79
- return;
80
- case "status":
81
- await showStatus(ctx.cwd, parsed[0], ctx.hasUI ? ctx.ui : undefined);
82
- return;
83
- case "done":
84
- await completeCurrentStep(ctx.cwd, ctx.hasUI ? ctx.ui : undefined);
85
- return;
86
- case "choose":
87
- await chooseOutcome(ctx.cwd, parsed[0], ctx.hasUI ? ctx.ui : undefined);
88
- return;
89
- case "cancel":
90
- case "stop":
91
- case "abort":
92
- await cancelRun(ctx.cwd, parsed[0], ctx.hasUI ? ctx.ui : undefined);
93
- return;
94
- case "import-web":
95
- case "record":
96
- notify(ctx.hasUI ? ctx.ui : undefined, `/${command} is deferred after the Core 6 MVP scaffold.` , "warning");
97
- return;
98
- default:
99
- notify(ctx.hasUI ? ctx.ui : undefined, usage(), "error");
100
- }
93
+ await handlePlaybookCommand(pi, command, parsed, ctx);
101
94
  } catch (error) {
102
95
  notify(ctx.hasUI ? ctx.ui : undefined, error instanceof Error ? error.message : String(error), "error");
103
96
  }
104
97
  },
105
98
  });
99
+
100
+ for (const alias of COLON_COMMAND_ALIASES) {
101
+ pi.registerCommand(alias.name, {
102
+ description: `Playbook: ${alias.description}. Alias for /playbook ${alias.command}.`,
103
+ ...(COLON_COMPLETION_COMMANDS.has(alias.command)
104
+ ? { getArgumentCompletions: (prefix) => getPlaybookColonArgumentCompletions(completionCwd, alias.command, prefix) }
105
+ : {}),
106
+ handler: async (args, ctx) => {
107
+ try {
108
+ await handlePlaybookCommand(pi, alias.command, parseArgs(args), ctx);
109
+ } catch (error) {
110
+ notify(ctx.hasUI ? ctx.ui : undefined, error instanceof Error ? error.message : String(error), "error");
111
+ }
112
+ },
113
+ });
114
+ }
115
+ }
116
+
117
+ async function handlePlaybookCommand(
118
+ pi: ExtensionAPI,
119
+ command: string,
120
+ args: string[],
121
+ ctx: CommandContext,
122
+ ): Promise<void> {
123
+ const ui = ctx.hasUI ? ctx.ui : undefined;
124
+
125
+ switch (command) {
126
+ case "list":
127
+ await listPlaybooks(pi, ctx.cwd, ui);
128
+ return;
129
+ case "start":
130
+ await startPlaybook(pi, ctx.cwd, args, ui);
131
+ return;
132
+ case "resume":
133
+ await resumeRun(ctx.cwd, args, ui);
134
+ return;
135
+ case "status":
136
+ await showStatus(ctx.cwd, args[0], ui);
137
+ return;
138
+ case "done":
139
+ await completeCurrentStep(ctx.cwd, ui);
140
+ return;
141
+ case "choose":
142
+ await chooseOutcome(ctx.cwd, args[0], ui);
143
+ return;
144
+ case "cancel":
145
+ case "stop":
146
+ case "abort":
147
+ await cancelRun(ctx.cwd, args[0], ui);
148
+ return;
149
+ case "import-web":
150
+ case "record":
151
+ notify(ui, `/playbook:${command} is deferred after the Core 6 MVP scaffold.`, "warning");
152
+ return;
153
+ default:
154
+ notify(ui, usage(), "error");
155
+ }
106
156
  }
107
157
 
108
158
  async function listPlaybooks(pi: ExtensionAPI, cwd: string, ui: UiLike | undefined): Promise<void> {
@@ -125,7 +175,7 @@ async function listPlaybooks(pi: ExtensionAPI, cwd: string, ui: UiLike | undefin
125
175
 
126
176
  async function startPlaybook(pi: ExtensionAPI, cwd: string, args: string[], ui: UiLike | undefined): Promise<void> {
127
177
  const playbookId = args[0];
128
- if (!playbookId) throw new Error("Usage: /playbook start <playbook-id> [--run <name>]");
178
+ if (!playbookId) throw new Error("Usage: /playbook:start <playbook-id> [--run <name>]");
129
179
 
130
180
  const playbook = await findPlaybook(cwd, playbookId);
131
181
  if (!playbook) throw new Error(`Playbook '${playbookId}' not found in .pi/playbooks/.`);
@@ -157,7 +207,7 @@ async function startPlaybook(pi: ExtensionAPI, cwd: string, args: string[], ui:
157
207
 
158
208
  async function resumeRun(cwd: string, args: string[], ui: UiLike | undefined): Promise<void> {
159
209
  const runId = args[0];
160
- if (!runId) throw new Error("Usage: /playbook resume <run-id>");
210
+ if (!runId) throw new Error("Usage: /playbook:resume <run-id>");
161
211
  const run = await loadRun(cwd, runId);
162
212
  if (!run) throw new Error(`Run '${runId}' not found.`);
163
213
  if (run.status !== "active") throw new Error(`Run '${runId}' is ${run.status}.`);
@@ -170,7 +220,7 @@ async function resumeRun(cwd: string, args: string[], ui: UiLike | undefined): P
170
220
 
171
221
  async function cancelRun(cwd: string, explicitRunId: string | undefined, ui: UiLike | undefined): Promise<void> {
172
222
  const runId = explicitRunId ?? (await loadActiveRunId(cwd));
173
- if (!runId) throw new Error("Usage: /playbook cancel [run-id]");
223
+ if (!runId) throw new Error("Usage: /playbook:cancel [run-id]");
174
224
  const run = await loadRun(cwd, runId);
175
225
  if (!run) throw new Error(`Run '${runId}' not found.`);
176
226
 
@@ -231,7 +281,7 @@ async function completeCurrentStep(cwd: string, ui: UiLike | undefined): Promise
231
281
  }
232
282
 
233
283
  async function chooseOutcome(cwd: string, outcome: string | undefined, ui: UiLike | undefined): Promise<void> {
234
- if (!outcome) throw new Error("Usage: /playbook choose <outcome>");
284
+ if (!outcome) throw new Error("Usage: /playbook:choose <outcome>");
235
285
  const { run, playbook } = await loadActive(cwd);
236
286
  await advanceRun(cwd, playbook, run, outcome, ui);
237
287
  }
@@ -333,13 +383,14 @@ function notify(ui: UiLike | undefined, message: string, level: "info" | "warnin
333
383
  function usage(): string {
334
384
  return [
335
385
  "Usage:",
336
- "/playbook list",
337
- "/playbook start <playbook-id> [--run <name>]",
338
- "/playbook resume <run-id>",
339
- "/playbook status [run-id]",
340
- "/playbook done",
341
- "/playbook choose <outcome>",
342
- "/playbook cancel [run-id]",
386
+ "/playbook:list",
387
+ "/playbook:start <playbook-id> [--run <name>]",
388
+ "/playbook:resume <run-id>",
389
+ "/playbook:status [run-id]",
390
+ "/playbook:done",
391
+ "/playbook:choose <outcome>",
392
+ "/playbook:cancel [run-id]",
393
+ "Legacy space-separated /playbook <subcommand> forms remain available for compatibility.",
343
394
  ].join("\n");
344
395
  }
345
396
 
@@ -353,6 +404,26 @@ export async function getPlaybookArgumentCompletions(cwd: string, prefix: string
353
404
  }
354
405
  }
355
406
 
407
+ export async function getPlaybookColonArgumentCompletions(
408
+ cwd: string,
409
+ command: string,
410
+ prefix: string,
411
+ ): Promise<CompletionItem[] | null> {
412
+ try {
413
+ const normalizedPrefix = prefix.trimStart();
414
+ const legacyPrefix = normalizedPrefix ? `${command} ${normalizedPrefix}` : command;
415
+ const items = await getPlaybookArgumentCompletionsUnsafe(cwd, legacyPrefix);
416
+ if (!items) return null;
417
+ const legacyToken = `${command} `;
418
+ return items.map((item) => ({
419
+ ...item,
420
+ value: item.value.startsWith(legacyToken) ? item.value.slice(legacyToken.length) : item.value,
421
+ }));
422
+ } catch {
423
+ return null;
424
+ }
425
+ }
426
+
356
427
  async function getPlaybookArgumentCompletionsUnsafe(cwd: string, prefix: string): Promise<CompletionItem[] | null> {
357
428
  const parsed = parseCompletionPrefix(prefix);
358
429
  const command = parsed.command;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-playbook",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Pi extension for passive, human-mediated Agent Skill playbooks.",
6
6
  "keywords": ["pi-package", "pi-extension", "agent-skills", "playbook"],
@@ -91,7 +91,7 @@ export function planCompletion(
91
91
  kind: "suggest",
92
92
  outcome: resolved.outcome,
93
93
  to: resolved.to,
94
- message: `Completion marked for step '${run.currentStep}'. Confirm outcome: /playbook choose ${resolved.outcome}`,
94
+ message: `Completion marked for step '${run.currentStep}'. Confirm outcome: /playbook:choose ${resolved.outcome}`,
95
95
  };
96
96
  }
97
97
 
@@ -101,8 +101,8 @@ export function planCompletion(
101
101
  outcome: resolved.outcome,
102
102
  to: resolved.to,
103
103
  message: resolved.outcome === "complete"
104
- ? `Completion marked for final step '${run.currentStep}'. Run /playbook done to complete.`
105
- : `Completion marked for step '${run.currentStep}'. Run /playbook done to advance to '${resolved.to}'.`,
104
+ ? `Completion marked for final step '${run.currentStep}'. Run /playbook:done to complete.`
105
+ : `Completion marked for step '${run.currentStep}'. Run /playbook:done to advance to '${resolved.to}'.`,
106
106
  };
107
107
  }
108
108
 
@@ -129,7 +129,7 @@ export function lastAssistantText(messages: unknown[]): string {
129
129
 
130
130
  function suggestPlan(step: PlaybookStep, stepId: string): CompletionPlan {
131
131
  if (step.transitions.length === 0) {
132
- return { kind: "suggest", outcome: "complete", to: "complete", message: `Completion suspected for final step '${stepId}'. Run /playbook done to complete.` };
132
+ return { kind: "suggest", outcome: "complete", to: "complete", message: `Completion suspected for final step '${stepId}'. Run /playbook:done to complete.` };
133
133
  }
134
134
  if (step.transitions.length === 1) {
135
135
  const transition = step.transitions[0]!;
@@ -137,12 +137,12 @@ function suggestPlan(step: PlaybookStep, stepId: string): CompletionPlan {
137
137
  kind: "suggest",
138
138
  outcome: transition.outcome,
139
139
  to: transition.to,
140
- message: `Completion suspected for step '${stepId}'. Run /playbook done to advance to '${transition.to}'.`,
140
+ message: `Completion suspected for step '${stepId}'. Run /playbook:done to advance to '${transition.to}'.`,
141
141
  };
142
142
  }
143
143
  return {
144
144
  kind: "suggest",
145
- message: `Completion suspected for step '${stepId}'. Choose outcome: ${step.transitions.map((transition) => `/playbook choose ${transition.outcome}`).join(" | ")}`,
145
+ message: `Completion suspected for step '${stepId}'. Choose outcome: ${step.transitions.map((transition) => `/playbook:choose ${transition.outcome}`).join(" | ")}`,
146
146
  };
147
147
  }
148
148