pi-skill-playbook 0.1.4 → 0.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.
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,57 +54,143 @@ 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** from the Pi TUI:
69
73
 
70
- ```gitignore
71
- .pi/playbook-runs/
72
- ```
74
+ ```
75
+ /playbook:list
76
+ /playbook:start
77
+ ```
73
78
 
74
- ## Use
79
+ When more than one playbook exists, Pi shows a selector with validation status for each playbook. The run id is generated automatically.
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
+ 4. **Drive the workflow**:
82
+
83
+ ```
84
+ /skill:grill-with-docs <feature idea>
85
+ /playbook:done
86
+ /playbook:choose
87
+ /playbook:status
88
+ ```
83
89
 
84
90
  The widget displays the current step, exact skill command, completion criteria, and outcome labels.
85
91
 
86
- ## Auto advance
92
+ ## Usage summary
93
+
94
+ | Command | Description |
95
+ |---|---|
96
+ | `/playbook:list` | List available playbooks with validation status |
97
+ | `/playbook:start` | Select a playbook and start a new run |
98
+ | `/playbook:resume` | Select an active run to resume |
99
+ | `/playbook:status [run-id]` | Show current step and completion criteria |
100
+ | `/playbook:done` | Complete the current step (auto-advances if single outcome) |
101
+ | `/playbook:choose` | Select an outcome for multi-branch steps |
102
+ | `/playbook:cancel` | Select and confirm an active run cancellation |
87
103
 
88
- Playbooks default to `autoAdvance: auto`.
104
+ Legacy explicit-argument forms (`/playbook:start <id>`, `/playbook:resume <run-id>`, `/playbook:choose <outcome>`, `/playbook:cancel <run-id>`, and space-separated `/playbook start`) remain available for scripts and non-interactive use.
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
+ ### Auto advance
107
+
108
+ 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
109
 
92
110
  ```text
93
111
  PLAYBOOK_OUTCOME: ready-for-prd
94
112
  ```
95
113
 
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>`.
114
+ | Mode | Behavior |
115
+ |---|---|
116
+ | `auto` (default) | Marker can advance single-outcome steps automatically |
117
+ | `suggest` | Marker only suggests `/playbook:done` or `/playbook:choose` |
118
+ | `off` | No prompt injection or completion detection |
119
+
120
+ ## Package contents
97
121
 
98
- Legacy space-separated forms such as `/playbook start` remain available for one release.
122
+ ```
123
+ pi-skill-playbook/
124
+ ├── extensions/ Pi extension entry point
125
+ ├── src/ Domain logic: validation, state, rendering, auto-advance
126
+ ├── samples/ Example playbooks (feature-development.yml)
127
+ ├── tests/ Node.js test suite
128
+ ├── docs/adr/ Architecture decision records
129
+ ├── LICENSE MIT
130
+ └── README.md
131
+ ```
99
132
 
100
- Optional playbook setting:
133
+ ### Playbook YAML structure
101
134
 
102
135
  ```yaml
103
- autoAdvance: auto # default: marker can advance single-outcome steps
104
- autoAdvance: suggest # marker only suggests /playbook:done or /playbook:choose
105
- autoAdvance: off # no prompt injection or completion detection
136
+ version: 1
137
+ id: my-playbook
138
+ name: My Playbook
139
+ entry: first-step
140
+ autoAdvance: auto # auto | suggest | off
141
+
142
+ skills:
143
+ my-skill:
144
+ role: entry
145
+
146
+ steps:
147
+ first-step:
148
+ primarySkill: my-skill
149
+ commandHint: "/skill:my-skill <arg>"
150
+ doneWhen:
151
+ - Criterion one.
152
+ - Criterion two.
153
+ transitions:
154
+ - outcome: done
155
+ to: complete # "complete" ends the run
106
156
  ```
157
+
158
+ See [`samples/feature-development.yml`](samples/feature-development.yml) for a full example.
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ npm install
164
+ npm run check # TypeScript type check
165
+ npm test # Run tests (Node.js built-in test runner + tsx)
166
+ ```
167
+
168
+ Requires Node.js ≥ 24, TypeScript 5.8+, and tsx 4.20+.
169
+
170
+ ## Release
171
+
172
+ Releases are automated via GitHub Actions:
173
+
174
+ 1. Bump `version` in `package.json` and merge to `main`.
175
+ 2. `auto-release.yml` detects the new version, creates a git tag and GitHub Release.
176
+ 3. `publish.yml` publishes to npm with provenance.
177
+
178
+ Manual dispatch is also available from the Actions tab.
179
+
180
+ ## Security
181
+
182
+ - Run state is local-only (`.pi/playbook-runs/`). No data leaves the machine.
183
+ - 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.
184
+ - No automatic file edits — the extension warns with a `.gitignore` snippet instead of modifying files.
185
+ - Report vulnerabilities via [GitHub Security Advisories](https://github.com/eiei114/pi-skill-playbook/security/advisories/new).
186
+
187
+ ## Links
188
+
189
+ - [npm package](https://www.npmjs.com/package/pi-skill-playbook)
190
+ - [GitHub repository](https://github.com/eiei114/pi-skill-playbook)
191
+ - [Pi coding agent](https://github.com/earendil-works/pi-coding-agent)
192
+ - [Architecture decisions](docs/adr/)
193
+
194
+ ## License
195
+
196
+ [MIT](LICENSE)
@@ -0,0 +1,15 @@
1
+ # Marker-based auto advance
2
+
3
+ Pi Skill Playbook defaults to marker-based **Auto Advance**: a run may advance automatically only when the active step's primary skill was explicitly invoked with `/skill:<name>` and the assistant emits a visible `PLAYBOOK_OUTCOME: <outcome>` marker. This keeps single-outcome workflows low-friction without turning playbooks into hidden skill automation; markerless completions only produce an advance suggestion, and multi-outcome steps still require explicit outcome choice.
4
+
5
+ ## Considered Options
6
+
7
+ - Fully automatic advancement after any matching skill invocation: rejected because failed or partial skill runs could silently advance the run.
8
+ - Suggest-only advancement: rejected as too close to the existing `/playbook done` workflow.
9
+ - Hidden structured metadata: rejected because visible markers make state changes auditable in conversation history.
10
+
11
+ ## Consequences
12
+
13
+ - Playbook prompts must tell the assistant the valid outcomes and marker format for the active step.
14
+ - `autoAdvance` defaults to `auto`, with `suggest` and `off` available per playbook.
15
+ - Auto advance changes run state and widget state only; it never runs the next skill or writes commands into the editor.
@@ -0,0 +1,29 @@
1
+ # Examples
2
+
3
+ ## Selection-first TUI flow
4
+
5
+ Copy or create YAML playbooks in the target project under `.pi/playbooks/`, then use argument-free commands from the Pi TUI:
6
+
7
+ ```text
8
+ /playbook:start
9
+ /skill:grill-with-docs <feature idea>
10
+ /playbook:done
11
+ /playbook:choose
12
+ /playbook:status
13
+ ```
14
+
15
+ - `/playbook:start` opens a playbook selector when multiple playbooks exist and generates the run id automatically.
16
+ - `/playbook:resume` opens an active-run selector.
17
+ - `/playbook:choose` opens a selector for the current step's valid outcomes.
18
+ - `/playbook:cancel` selects an active run when needed and asks for confirmation before marking it cancelled.
19
+
20
+ ## Script-compatible explicit args
21
+
22
+ Non-interactive scripts can still pass ids explicitly:
23
+
24
+ ```text
25
+ /playbook:start feature-development --run scripted-feature
26
+ /playbook:resume feature-development-20260608120000
27
+ /playbook:choose ready-for-prd
28
+ /playbook:cancel feature-development-20260608120000
29
+ ```
@@ -33,6 +33,11 @@ type CommandContext = {
33
33
  ui?: UiLike;
34
34
  };
35
35
 
36
+ type SelectOption<T> = {
37
+ label: string;
38
+ value: T;
39
+ };
40
+
36
41
  export default function piSkillPlaybook(pi: ExtensionAPI) {
37
42
  let completionCwd = process.cwd();
38
43
  let pendingSkillInvocation: string | undefined;
@@ -114,7 +119,7 @@ export default function piSkillPlaybook(pi: ExtensionAPI) {
114
119
  }
115
120
  }
116
121
 
117
- async function handlePlaybookCommand(
122
+ export async function handlePlaybookCommand(
118
123
  pi: ExtensionAPI,
119
124
  command: string,
120
125
  args: string[],
@@ -175,7 +180,12 @@ async function listPlaybooks(pi: ExtensionAPI, cwd: string, ui: UiLike | undefin
175
180
 
176
181
  async function startPlaybook(pi: ExtensionAPI, cwd: string, args: string[], ui: UiLike | undefined): Promise<void> {
177
182
  const playbookId = args[0];
178
- if (!playbookId) throw new Error("Usage: /playbook:start <playbook-id> [--run <name>]");
183
+ if (!playbookId) {
184
+ const playbook = await pickPlaybook(pi, cwd, ui);
185
+ if (!playbook) return;
186
+ await createAndActivateRun(cwd, playbook, undefined, ui);
187
+ return;
188
+ }
179
189
 
180
190
  const playbook = await findPlaybook(cwd, playbookId);
181
191
  if (!playbook) throw new Error(`Playbook '${playbookId}' not found in .pi/playbooks/.`);
@@ -186,6 +196,10 @@ async function startPlaybook(pi: ExtensionAPI, cwd: string, args: string[], ui:
186
196
  }
187
197
 
188
198
  const runName = readFlagValue(args.slice(1), "--run");
199
+ await createAndActivateRun(cwd, playbook, runName, ui);
200
+ }
201
+
202
+ async function createAndActivateRun(cwd: string, playbook: LoadedPlaybook, runName: string | undefined, ui: UiLike | undefined): Promise<void> {
189
203
  const now = new Date().toISOString();
190
204
  const run: PlaybookRunState = {
191
205
  runId: createRunId(playbook.definition.id, runName),
@@ -207,7 +221,12 @@ async function startPlaybook(pi: ExtensionAPI, cwd: string, args: string[], ui:
207
221
 
208
222
  async function resumeRun(cwd: string, args: string[], ui: UiLike | undefined): Promise<void> {
209
223
  const runId = args[0];
210
- if (!runId) throw new Error("Usage: /playbook:resume <run-id>");
224
+ if (!runId) {
225
+ const run = await pickActiveRun(cwd, ui, "Resume which playbook run?");
226
+ if (!run) return;
227
+ await resumeRun(cwd, [run.runId], ui);
228
+ return;
229
+ }
211
230
  const run = await loadRun(cwd, runId);
212
231
  if (!run) throw new Error(`Run '${runId}' not found.`);
213
232
  if (run.status !== "active") throw new Error(`Run '${runId}' is ${run.status}.`);
@@ -219,8 +238,12 @@ async function resumeRun(cwd: string, args: string[], ui: UiLike | undefined): P
219
238
  }
220
239
 
221
240
  async function cancelRun(cwd: string, explicitRunId: string | undefined, ui: UiLike | undefined): Promise<void> {
222
- const runId = explicitRunId ?? (await loadActiveRunId(cwd));
223
- if (!runId) throw new Error("Usage: /playbook:cancel [run-id]");
241
+ let runId = explicitRunId;
242
+ if (!runId) {
243
+ const run = await pickRunToCancel(cwd, ui);
244
+ if (!run) return;
245
+ runId = run.runId;
246
+ }
224
247
  const run = await loadRun(cwd, runId);
225
248
  if (!run) throw new Error(`Run '${runId}' not found.`);
226
249
 
@@ -281,11 +304,165 @@ async function completeCurrentStep(cwd: string, ui: UiLike | undefined): Promise
281
304
  }
282
305
 
283
306
  async function chooseOutcome(cwd: string, outcome: string | undefined, ui: UiLike | undefined): Promise<void> {
284
- if (!outcome) throw new Error("Usage: /playbook:choose <outcome>");
285
307
  const { run, playbook } = await loadActive(cwd);
308
+ if (!outcome) {
309
+ const selected = await pickOutcome(playbook, run, ui);
310
+ if (!selected) return;
311
+ outcome = selected.outcome;
312
+ }
286
313
  await advanceRun(cwd, playbook, run, outcome, ui);
287
314
  }
288
315
 
316
+ async function pickPlaybook(pi: ExtensionAPI, cwd: string, ui: UiLike | undefined): Promise<LoadedPlaybook | undefined> {
317
+ if (!hasSelectionUI(ui)) {
318
+ notify(ui, "Interactive playbook selection requires the Pi TUI. For scripts, use /playbook:start <playbook-id> [--run <name>].", "error");
319
+ return undefined;
320
+ }
321
+
322
+ const playbooks = await loadPlaybooks(cwd);
323
+ if (playbooks.length === 0) {
324
+ notify(ui, "No playbooks found. Create .pi/playbooks/*.yml or copy samples/feature-development.yml into .pi/playbooks/.", "info");
325
+ return undefined;
326
+ }
327
+
328
+ const availableSkills = getAvailableSkills(pi);
329
+ const duplicateErrors = duplicateIdErrors(playbooks);
330
+ const candidates = playbooks.map((playbook) => {
331
+ const validation = validatePlaybook(playbook, availableSkills, { requireSkills: true });
332
+ const errors = [...validation.errors, ...(duplicateErrors.get(playbook) ?? [])];
333
+ return { playbook, errors, valid: errors.length === 0 };
334
+ });
335
+
336
+ if (candidates.length === 1) {
337
+ const candidate = candidates[0];
338
+ if (!candidate.valid) {
339
+ notify(ui, `Playbook validation failed:\n${renderValidationErrors(candidate.errors)}`, "error");
340
+ return undefined;
341
+ }
342
+ return candidate.playbook;
343
+ }
344
+
345
+ const options = candidates.map((candidate) => ({
346
+ label: playbookSelectionLabel(candidate.playbook, candidate.errors),
347
+ value: candidate,
348
+ }));
349
+ const selected = await selectByLabel(ui, "Start which playbook?", options);
350
+ if (!selected) return undefined;
351
+ if (!selected.valid) {
352
+ notify(ui, `Playbook validation failed:\n${renderValidationErrors(selected.errors)}`, "error");
353
+ return undefined;
354
+ }
355
+ return selected.playbook;
356
+ }
357
+
358
+ function playbookSelectionLabel(playbook: LoadedPlaybook, errors: string[]): string {
359
+ const marker = errors.length === 0 ? "ok" : "invalid";
360
+ const suffix = errors.length === 0 ? "" : ` — ${errors.join("; ")}`;
361
+ return `${playbook.definition.id} — ${playbook.definition.name} (${marker})${suffix}`;
362
+ }
363
+
364
+ function duplicateIdErrors(playbooks: LoadedPlaybook[]): Map<LoadedPlaybook, string[]> {
365
+ const byId = new Map<string, LoadedPlaybook[]>();
366
+ for (const playbook of playbooks) {
367
+ const id = playbook.definition.id;
368
+ if (!id) continue;
369
+ byId.set(id, [...(byId.get(id) ?? []), playbook]);
370
+ }
371
+
372
+ const result = new Map<LoadedPlaybook, string[]>();
373
+ for (const [id, matches] of byId) {
374
+ if (matches.length < 2) continue;
375
+ const paths = matches.map((playbook) => playbook.path).join(", ");
376
+ for (const playbook of matches) result.set(playbook, [`duplicate playbook id '${id}' in ${paths}`]);
377
+ }
378
+ return result;
379
+ }
380
+
381
+ async function pickActiveRun(cwd: string, ui: UiLike | undefined, title: string): Promise<PlaybookRunState | undefined> {
382
+ if (!hasSelectionUI(ui)) {
383
+ notify(ui, "Interactive run selection requires the Pi TUI. For scripts, pass the run id explicitly.", "error");
384
+ return undefined;
385
+ }
386
+
387
+ const runs = await getRunCompletionCandidates(cwd, true);
388
+ if (runs.length === 0) {
389
+ clearWidget(ui);
390
+ notify(ui, "No active playbook runs. Start one with /playbook:start.", "info");
391
+ return undefined;
392
+ }
393
+
394
+ return selectByLabel(ui, title, runs.map((run) => ({ label: activeRunLabel(run), value: run })));
395
+ }
396
+
397
+ async function pickRunToCancel(cwd: string, ui: UiLike | undefined): Promise<PlaybookRunState | undefined> {
398
+ if (!hasConfirmUI(ui)) {
399
+ notify(ui, "Interactive cancellation requires the Pi TUI. For scripts, use /playbook:cancel <run-id>.", "error");
400
+ return undefined;
401
+ }
402
+
403
+ const runs = await getRunCompletionCandidates(cwd, true);
404
+ if (runs.length === 0) {
405
+ clearWidget(ui);
406
+ notify(ui, "No active playbook runs to cancel.", "info");
407
+ return undefined;
408
+ }
409
+
410
+ const run = runs.length === 1
411
+ ? runs[0]
412
+ : await selectByLabel(ui, "Cancel which playbook run?", runs.map((candidate) => ({ label: activeRunLabel(candidate), value: candidate })));
413
+ if (!run) return undefined;
414
+
415
+ const confirmed = await ui.confirm("Cancel playbook run?", `${run.runId} (${run.playbookId}) will be marked cancelled.`);
416
+ if (!confirmed) {
417
+ notify(ui, "Playbook cancellation skipped.", "info");
418
+ return undefined;
419
+ }
420
+ return run;
421
+ }
422
+
423
+ function activeRunLabel(run: PlaybookRunState): string {
424
+ return `${run.runId} — ${run.playbookId} (updated ${run.updatedAt})`;
425
+ }
426
+
427
+ async function pickOutcome(playbook: LoadedPlaybook, run: PlaybookRunState, ui: UiLike | undefined) {
428
+ if (!hasSelectionUI(ui)) {
429
+ notify(ui, "Interactive outcome selection requires the Pi TUI. For scripts, use /playbook:choose <outcome>.", "error");
430
+ return undefined;
431
+ }
432
+
433
+ const step = playbook.definition.steps[run.currentStep];
434
+ if (!step) throw new Error(`Current step '${run.currentStep}' is missing.`);
435
+ if (step.transitions.length === 0) {
436
+ notify(ui, "Current step has no branch outcomes. Run /playbook:done to complete it.", "info");
437
+ return undefined;
438
+ }
439
+
440
+ return selectByLabel(
441
+ ui,
442
+ `Choose outcome for ${run.currentStep}`,
443
+ step.transitions.map((transition) => ({ label: `${transition.outcome} → ${transition.to}`, value: transition })),
444
+ );
445
+ }
446
+
447
+ async function selectByLabel<T>(
448
+ ui: { select(title: string, options: string[]): Promise<string | undefined> },
449
+ title: string,
450
+ options: SelectOption<T>[],
451
+ ): Promise<T | undefined> {
452
+ const selected = await ui.select(title, options.map((option) => option.label));
453
+ return options.find((option) => option.label === selected)?.value;
454
+ }
455
+
456
+ function hasSelectionUI(ui: UiLike | undefined): ui is UiLike & { select: NonNullable<UiLike["select"]> } {
457
+ return typeof ui?.select === "function";
458
+ }
459
+
460
+ function hasConfirmUI(
461
+ ui: UiLike | undefined,
462
+ ): ui is UiLike & { select: NonNullable<UiLike["select"]>; confirm: NonNullable<UiLike["confirm"]> } {
463
+ return hasSelectionUI(ui) && typeof ui.confirm === "function";
464
+ }
465
+
289
466
  async function advanceRun(cwd: string, playbook: LoadedPlaybook, run: PlaybookRunState, outcome: string, ui: UiLike | undefined): Promise<void> {
290
467
  const step = playbook.definition.steps[run.currentStep];
291
468
  if (!step) throw new Error(`Current step '${run.currentStep}' is missing.`);
@@ -384,13 +561,13 @@ function usage(): string {
384
561
  return [
385
562
  "Usage:",
386
563
  "/playbook:list",
387
- "/playbook:start <playbook-id> [--run <name>]",
388
- "/playbook:resume <run-id>",
564
+ "/playbook:start",
565
+ "/playbook:resume",
389
566
  "/playbook:status [run-id]",
390
567
  "/playbook:done",
391
- "/playbook:choose <outcome>",
392
- "/playbook:cancel [run-id]",
393
- "Legacy space-separated /playbook <subcommand> forms remain available for compatibility.",
568
+ "/playbook:choose",
569
+ "/playbook:cancel",
570
+ "Legacy explicit args remain available for scripts: start <playbook-id> [--run <name>], resume <run-id>, choose <outcome>, cancel <run-id>.",
394
571
  ].join("\n");
395
572
  }
396
573
 
@@ -567,5 +744,7 @@ function readFlagValue(args: string[], flag: string): string | undefined {
567
744
 
568
745
  interface UiLike {
569
746
  notify(message: string, level: "info" | "warning" | "error"): void;
747
+ select?(title: string, options: string[]): Promise<string | undefined>;
748
+ confirm?(title: string, message: string): Promise<boolean>;
570
749
  setWidget(id: string, content: string[] | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void;
571
750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skill-playbook",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
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"],
@@ -16,15 +16,18 @@
16
16
  "files": [
17
17
  "extensions/",
18
18
  "src/",
19
+ "docs/",
19
20
  "samples/",
20
21
  "LICENSE",
21
22
  "README.md"
22
23
  ],
23
24
  "scripts": {
24
25
  "check": "tsc --noEmit",
26
+ "typecheck": "tsc --noEmit",
25
27
  "test": "node --test --import tsx tests/*.test.ts",
26
28
  "build": "npm run check",
27
- "validate:package": "npm pack --dry-run"
29
+ "validate:package": "npm pack --dry-run",
30
+ "ci": "npm run typecheck && npm test && npm run validate:package"
28
31
  },
29
32
  "pi": {
30
33
  "extensions": ["./extensions/index.ts"]