pi-skill-playbook 0.2.0 → 1.0.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 +3 -3
- package/docs/adr/0001-marker-based-auto-advance.md +1 -1
- package/docs/examples.md +0 -11
- package/extensions/index.ts +38 -263
- package/package.json +1 -1
- package/src/auto-advance.ts +2 -2
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Define ordered skill workflows as YAML playbooks in your project, then drive the
|
|
|
22
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
23
|
- **Active run widget** — Displays current step, skill command, completion criteria, and outcome labels below the editor.
|
|
24
24
|
- **Strict YAML validation** — Playbooks are validated on load for structure, transitions, and skill references.
|
|
25
|
-
- **
|
|
25
|
+
- **Selection UI** — Playbook, run, and outcome selection use the Pi TUI selector instead of memorized ids.
|
|
26
26
|
- **Local run state** — Run state is stored in `.pi/playbook-runs/` inside the target project, never in git.
|
|
27
27
|
|
|
28
28
|
## Install
|
|
@@ -96,12 +96,12 @@ The widget displays the current step, exact skill command, completion criteria,
|
|
|
96
96
|
| `/playbook:list` | List available playbooks with validation status |
|
|
97
97
|
| `/playbook:start` | Select a playbook and start a new run |
|
|
98
98
|
| `/playbook:resume` | Select an active run to resume |
|
|
99
|
-
| `/playbook:status
|
|
99
|
+
| `/playbook:status` | Show current step and completion criteria |
|
|
100
100
|
| `/playbook:done` | Complete the current step (auto-advances if single outcome) |
|
|
101
101
|
| `/playbook:choose` | Select an outcome for multi-branch steps |
|
|
102
102
|
| `/playbook:cancel` | Select and confirm an active run cancellation |
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
All commands are argument-free. Use the Pi TUI selection UI to pick playbooks, runs, and outcomes.
|
|
105
105
|
|
|
106
106
|
### Auto advance
|
|
107
107
|
|
|
@@ -5,7 +5,7 @@ Pi Skill Playbook defaults to marker-based **Auto Advance**: a run may advance a
|
|
|
5
5
|
## Considered Options
|
|
6
6
|
|
|
7
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
|
|
8
|
+
- Suggest-only advancement: rejected as too close to the existing `/playbook:done` workflow.
|
|
9
9
|
- Hidden structured metadata: rejected because visible markers make state changes auditable in conversation history.
|
|
10
10
|
|
|
11
11
|
## Consequences
|
package/docs/examples.md
CHANGED
|
@@ -16,14 +16,3 @@ Copy or create YAML playbooks in the target project under `.pi/playbooks/`, then
|
|
|
16
16
|
- `/playbook:resume` opens an active-run selector.
|
|
17
17
|
- `/playbook:choose` opens a selector for the current step's valid outcomes.
|
|
18
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
|
-
```
|
package/extensions/index.ts
CHANGED
|
@@ -19,14 +19,6 @@ const COMMANDS = [
|
|
|
19
19
|
["cancel", "cancel an active playbook run"],
|
|
20
20
|
] as const;
|
|
21
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
22
|
type CommandContext = {
|
|
31
23
|
cwd: string;
|
|
32
24
|
hasUI: boolean;
|
|
@@ -39,11 +31,9 @@ type SelectOption<T> = {
|
|
|
39
31
|
};
|
|
40
32
|
|
|
41
33
|
export default function piSkillPlaybook(pi: ExtensionAPI) {
|
|
42
|
-
let completionCwd = process.cwd();
|
|
43
34
|
let pendingSkillInvocation: string | undefined;
|
|
44
35
|
|
|
45
36
|
pi.on("session_start", async (_event, ctx) => {
|
|
46
|
-
completionCwd = ctx.cwd;
|
|
47
37
|
if (!ctx.hasUI) return;
|
|
48
38
|
const activeRunId = await loadActiveRunId(ctx.cwd);
|
|
49
39
|
if (!activeRunId) {
|
|
@@ -88,29 +78,12 @@ export default function piSkillPlaybook(pi: ExtensionAPI) {
|
|
|
88
78
|
}
|
|
89
79
|
});
|
|
90
80
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const parsed = parseArgs(args);
|
|
96
|
-
const command = parsed.shift() ?? "status";
|
|
97
|
-
try {
|
|
98
|
-
await handlePlaybookCommand(pi, command, parsed, ctx);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
notify(ctx.hasUI ? ctx.ui : undefined, error instanceof Error ? error.message : String(error), "error");
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
for (const alias of COLON_COMMAND_ALIASES) {
|
|
106
|
-
pi.registerCommand(alias.name, {
|
|
107
|
-
description: `Playbook: ${alias.description}. Alias for /playbook ${alias.command}.`,
|
|
108
|
-
...(COLON_COMPLETION_COMMANDS.has(alias.command)
|
|
109
|
-
? { getArgumentCompletions: (prefix) => getPlaybookColonArgumentCompletions(completionCwd, alias.command, prefix) }
|
|
110
|
-
: {}),
|
|
111
|
-
handler: async (args, ctx) => {
|
|
81
|
+
for (const [command, description] of COMMANDS) {
|
|
82
|
+
pi.registerCommand(`playbook:${command}`, {
|
|
83
|
+
description: `Playbook: ${description}.`,
|
|
84
|
+
handler: async (_args, ctx) => {
|
|
112
85
|
try {
|
|
113
|
-
await handlePlaybookCommand(pi,
|
|
86
|
+
await handlePlaybookCommand(pi, command, ctx);
|
|
114
87
|
} catch (error) {
|
|
115
88
|
notify(ctx.hasUI ? ctx.ui : undefined, error instanceof Error ? error.message : String(error), "error");
|
|
116
89
|
}
|
|
@@ -122,7 +95,6 @@ export default function piSkillPlaybook(pi: ExtensionAPI) {
|
|
|
122
95
|
export async function handlePlaybookCommand(
|
|
123
96
|
pi: ExtensionAPI,
|
|
124
97
|
command: string,
|
|
125
|
-
args: string[],
|
|
126
98
|
ctx: CommandContext,
|
|
127
99
|
): Promise<void> {
|
|
128
100
|
const ui = ctx.hasUI ? ctx.ui : undefined;
|
|
@@ -132,24 +104,24 @@ export async function handlePlaybookCommand(
|
|
|
132
104
|
await listPlaybooks(pi, ctx.cwd, ui);
|
|
133
105
|
return;
|
|
134
106
|
case "start":
|
|
135
|
-
await startPlaybook(pi, ctx.cwd,
|
|
107
|
+
await startPlaybook(pi, ctx.cwd, ui);
|
|
136
108
|
return;
|
|
137
109
|
case "resume":
|
|
138
|
-
await resumeRun(ctx.cwd,
|
|
110
|
+
await resumeRun(ctx.cwd, ui);
|
|
139
111
|
return;
|
|
140
112
|
case "status":
|
|
141
|
-
await showStatus(ctx.cwd,
|
|
113
|
+
await showStatus(ctx.cwd, ui);
|
|
142
114
|
return;
|
|
143
115
|
case "done":
|
|
144
116
|
await completeCurrentStep(ctx.cwd, ui);
|
|
145
117
|
return;
|
|
146
118
|
case "choose":
|
|
147
|
-
await chooseOutcome(ctx.cwd,
|
|
119
|
+
await chooseOutcome(ctx.cwd, ui);
|
|
148
120
|
return;
|
|
149
121
|
case "cancel":
|
|
150
122
|
case "stop":
|
|
151
123
|
case "abort":
|
|
152
|
-
await cancelRun(ctx.cwd,
|
|
124
|
+
await cancelRun(ctx.cwd, ui);
|
|
153
125
|
return;
|
|
154
126
|
case "import-web":
|
|
155
127
|
case "record":
|
|
@@ -178,25 +150,10 @@ async function listPlaybooks(pi: ExtensionAPI, cwd: string, ui: UiLike | undefin
|
|
|
178
150
|
notify(ui, lines.join("\n"), duplicateResult.valid ? "info" : "error");
|
|
179
151
|
}
|
|
180
152
|
|
|
181
|
-
async function startPlaybook(pi: ExtensionAPI, cwd: string,
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
184
|
-
|
|
185
|
-
if (!playbook) return;
|
|
186
|
-
await createAndActivateRun(cwd, playbook, undefined, ui);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const playbook = await findPlaybook(cwd, playbookId);
|
|
191
|
-
if (!playbook) throw new Error(`Playbook '${playbookId}' not found in .pi/playbooks/.`);
|
|
192
|
-
|
|
193
|
-
const validation = validatePlaybook(playbook, getAvailableSkills(pi), { requireSkills: true });
|
|
194
|
-
if (!validation.valid) {
|
|
195
|
-
throw new Error(`Playbook validation failed:\n${renderValidationErrors(validation.errors)}`);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const runName = readFlagValue(args.slice(1), "--run");
|
|
199
|
-
await createAndActivateRun(cwd, playbook, runName, ui);
|
|
153
|
+
async function startPlaybook(pi: ExtensionAPI, cwd: string, ui: UiLike | undefined): Promise<void> {
|
|
154
|
+
const playbook = await pickPlaybook(pi, cwd, ui);
|
|
155
|
+
if (!playbook) return;
|
|
156
|
+
await createAndActivateRun(cwd, playbook, undefined, ui);
|
|
200
157
|
}
|
|
201
158
|
|
|
202
159
|
async function createAndActivateRun(cwd: string, playbook: LoadedPlaybook, runName: string | undefined, ui: UiLike | undefined): Promise<void> {
|
|
@@ -219,33 +176,20 @@ async function createAndActivateRun(cwd: string, playbook: LoadedPlaybook, runNa
|
|
|
219
176
|
notify(ui, [`Started ${run.runId}.`, ...(advisory ? ["", advisory] : [])].join("\n"), advisory ? "warning" : "info");
|
|
220
177
|
}
|
|
221
178
|
|
|
222
|
-
async function resumeRun(cwd: string,
|
|
223
|
-
const
|
|
224
|
-
if (!
|
|
225
|
-
|
|
226
|
-
if (!run) return;
|
|
227
|
-
await resumeRun(cwd, [run.runId], ui);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const run = await loadRun(cwd, runId);
|
|
231
|
-
if (!run) throw new Error(`Run '${runId}' not found.`);
|
|
232
|
-
if (run.status !== "active") throw new Error(`Run '${runId}' is ${run.status}.`);
|
|
179
|
+
async function resumeRun(cwd: string, ui: UiLike | undefined): Promise<void> {
|
|
180
|
+
const run = await pickActiveRun(cwd, ui, "Resume which playbook run?");
|
|
181
|
+
if (!run) return;
|
|
182
|
+
|
|
233
183
|
const playbook = await findPlaybook(cwd, run.playbookId);
|
|
234
|
-
if (!playbook) throw new Error(`Run '${runId}' references missing playbook '${run.playbookId}'.`);
|
|
184
|
+
if (!playbook) throw new Error(`Run '${run.runId}' references missing playbook '${run.playbookId}'.`);
|
|
235
185
|
await setActiveRun(cwd, run.runId);
|
|
236
186
|
renderWidget(ui, playbook, run);
|
|
237
187
|
notify(ui, `Resumed ${run.runId}.`, "info");
|
|
238
188
|
}
|
|
239
189
|
|
|
240
|
-
async function cancelRun(cwd: string,
|
|
241
|
-
|
|
242
|
-
if (!
|
|
243
|
-
const run = await pickRunToCancel(cwd, ui);
|
|
244
|
-
if (!run) return;
|
|
245
|
-
runId = run.runId;
|
|
246
|
-
}
|
|
247
|
-
const run = await loadRun(cwd, runId);
|
|
248
|
-
if (!run) throw new Error(`Run '${runId}' not found.`);
|
|
190
|
+
async function cancelRun(cwd: string, ui: UiLike | undefined): Promise<void> {
|
|
191
|
+
const run = await pickRunToCancel(cwd, ui);
|
|
192
|
+
if (!run) return;
|
|
249
193
|
|
|
250
194
|
if (run.status !== "active") {
|
|
251
195
|
if ((await loadActiveRunId(cwd)) === run.runId) await clearActiveRun(cwd);
|
|
@@ -264,8 +208,8 @@ async function cancelRun(cwd: string, explicitRunId: string | undefined, ui: UiL
|
|
|
264
208
|
notify(ui, `Cancelled playbook run ${run.runId}.`, "info");
|
|
265
209
|
}
|
|
266
210
|
|
|
267
|
-
async function showStatus(cwd: string,
|
|
268
|
-
const runId =
|
|
211
|
+
async function showStatus(cwd: string, ui: UiLike | undefined): Promise<void> {
|
|
212
|
+
const runId = await loadActiveRunId(cwd);
|
|
269
213
|
if (!runId) {
|
|
270
214
|
notify(ui, "No active playbook run.", "info");
|
|
271
215
|
clearWidget(ui);
|
|
@@ -275,7 +219,7 @@ async function showStatus(cwd: string, explicitRunId: string | undefined, ui: Ui
|
|
|
275
219
|
if (!run) throw new Error(`Run '${runId}' not found.`);
|
|
276
220
|
if (run.status !== "active") {
|
|
277
221
|
notify(ui, `Run '${run.runId}' is ${run.status}.`, "info");
|
|
278
|
-
|
|
222
|
+
clearWidget(ui);
|
|
279
223
|
return;
|
|
280
224
|
}
|
|
281
225
|
const playbook = await findPlaybook(cwd, run.playbookId);
|
|
@@ -303,19 +247,16 @@ async function completeCurrentStep(cwd: string, ui: UiLike | undefined): Promise
|
|
|
303
247
|
await advanceRun(cwd, playbook, run, step.transitions[0].outcome, ui);
|
|
304
248
|
}
|
|
305
249
|
|
|
306
|
-
async function chooseOutcome(cwd: string,
|
|
250
|
+
async function chooseOutcome(cwd: string, ui: UiLike | undefined): Promise<void> {
|
|
307
251
|
const { run, playbook } = await loadActive(cwd);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
outcome = selected.outcome;
|
|
312
|
-
}
|
|
313
|
-
await advanceRun(cwd, playbook, run, outcome, ui);
|
|
252
|
+
const selected = await pickOutcome(playbook, run, ui);
|
|
253
|
+
if (!selected) return;
|
|
254
|
+
await advanceRun(cwd, playbook, run, selected.outcome, ui);
|
|
314
255
|
}
|
|
315
256
|
|
|
316
257
|
async function pickPlaybook(pi: ExtensionAPI, cwd: string, ui: UiLike | undefined): Promise<LoadedPlaybook | undefined> {
|
|
317
258
|
if (!hasSelectionUI(ui)) {
|
|
318
|
-
notify(ui, "Interactive playbook selection requires the Pi TUI.
|
|
259
|
+
notify(ui, "Interactive playbook selection requires the Pi TUI. Run /playbook:start from the command palette.", "error");
|
|
319
260
|
return undefined;
|
|
320
261
|
}
|
|
321
262
|
|
|
@@ -380,11 +321,11 @@ function duplicateIdErrors(playbooks: LoadedPlaybook[]): Map<LoadedPlaybook, str
|
|
|
380
321
|
|
|
381
322
|
async function pickActiveRun(cwd: string, ui: UiLike | undefined, title: string): Promise<PlaybookRunState | undefined> {
|
|
382
323
|
if (!hasSelectionUI(ui)) {
|
|
383
|
-
notify(ui, "Interactive run selection requires the Pi TUI.
|
|
324
|
+
notify(ui, "Interactive run selection requires the Pi TUI. Run /playbook:resume from the command palette.", "error");
|
|
384
325
|
return undefined;
|
|
385
326
|
}
|
|
386
327
|
|
|
387
|
-
const runs = await
|
|
328
|
+
const runs = await getActiveRuns(cwd);
|
|
388
329
|
if (runs.length === 0) {
|
|
389
330
|
clearWidget(ui);
|
|
390
331
|
notify(ui, "No active playbook runs. Start one with /playbook:start.", "info");
|
|
@@ -396,11 +337,11 @@ async function pickActiveRun(cwd: string, ui: UiLike | undefined, title: string)
|
|
|
396
337
|
|
|
397
338
|
async function pickRunToCancel(cwd: string, ui: UiLike | undefined): Promise<PlaybookRunState | undefined> {
|
|
398
339
|
if (!hasConfirmUI(ui)) {
|
|
399
|
-
notify(ui, "Interactive cancellation requires the Pi TUI.
|
|
340
|
+
notify(ui, "Interactive cancellation requires the Pi TUI. Run /playbook:cancel from the command palette.", "error");
|
|
400
341
|
return undefined;
|
|
401
342
|
}
|
|
402
343
|
|
|
403
|
-
const runs = await
|
|
344
|
+
const runs = await getActiveRuns(cwd);
|
|
404
345
|
if (runs.length === 0) {
|
|
405
346
|
clearWidget(ui);
|
|
406
347
|
notify(ui, "No active playbook runs to cancel.", "info");
|
|
@@ -426,7 +367,7 @@ function activeRunLabel(run: PlaybookRunState): string {
|
|
|
426
367
|
|
|
427
368
|
async function pickOutcome(playbook: LoadedPlaybook, run: PlaybookRunState, ui: UiLike | undefined) {
|
|
428
369
|
if (!hasSelectionUI(ui)) {
|
|
429
|
-
notify(ui, "Interactive outcome selection requires the Pi TUI.
|
|
370
|
+
notify(ui, "Interactive outcome selection requires the Pi TUI. Run /playbook:choose from the command palette.", "error");
|
|
430
371
|
return undefined;
|
|
431
372
|
}
|
|
432
373
|
|
|
@@ -563,183 +504,17 @@ function usage(): string {
|
|
|
563
504
|
"/playbook:list",
|
|
564
505
|
"/playbook:start",
|
|
565
506
|
"/playbook:resume",
|
|
566
|
-
"/playbook:status
|
|
507
|
+
"/playbook:status",
|
|
567
508
|
"/playbook:done",
|
|
568
509
|
"/playbook:choose",
|
|
569
510
|
"/playbook:cancel",
|
|
570
|
-
"Legacy explicit args remain available for scripts: start <playbook-id> [--run <name>], resume <run-id>, choose <outcome>, cancel <run-id>.",
|
|
571
511
|
].join("\n");
|
|
572
512
|
}
|
|
573
513
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
export async function getPlaybookArgumentCompletions(cwd: string, prefix: string): Promise<CompletionItem[] | null> {
|
|
577
|
-
try {
|
|
578
|
-
return await getPlaybookArgumentCompletionsUnsafe(cwd, prefix);
|
|
579
|
-
} catch {
|
|
580
|
-
return null;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
export async function getPlaybookColonArgumentCompletions(
|
|
585
|
-
cwd: string,
|
|
586
|
-
command: string,
|
|
587
|
-
prefix: string,
|
|
588
|
-
): Promise<CompletionItem[] | null> {
|
|
589
|
-
try {
|
|
590
|
-
const normalizedPrefix = prefix.trimStart();
|
|
591
|
-
const legacyPrefix = normalizedPrefix ? `${command} ${normalizedPrefix}` : command;
|
|
592
|
-
const items = await getPlaybookArgumentCompletionsUnsafe(cwd, legacyPrefix);
|
|
593
|
-
if (!items) return null;
|
|
594
|
-
const legacyToken = `${command} `;
|
|
595
|
-
return items.map((item) => ({
|
|
596
|
-
...item,
|
|
597
|
-
value: item.value.startsWith(legacyToken) ? item.value.slice(legacyToken.length) : item.value,
|
|
598
|
-
}));
|
|
599
|
-
} catch {
|
|
600
|
-
return null;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async function getPlaybookArgumentCompletionsUnsafe(cwd: string, prefix: string): Promise<CompletionItem[] | null> {
|
|
605
|
-
const parsed = parseCompletionPrefix(prefix);
|
|
606
|
-
const command = parsed.command;
|
|
607
|
-
if (!command) {
|
|
608
|
-
return completeCommands(parsed.currentToken);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (parsed.completedTokens.length === 0) {
|
|
612
|
-
return completeCommands(parsed.currentToken);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
switch (command) {
|
|
616
|
-
case "start":
|
|
617
|
-
return completeStartArguments(cwd, parsed);
|
|
618
|
-
case "resume":
|
|
619
|
-
return completeRunArgument(cwd, parsed, command, { activeOnly: true });
|
|
620
|
-
case "status":
|
|
621
|
-
return completeRunArgument(cwd, parsed, command, { activeOnly: false, optional: true });
|
|
622
|
-
case "cancel":
|
|
623
|
-
case "stop":
|
|
624
|
-
case "abort":
|
|
625
|
-
return completeRunArgument(cwd, parsed, command, { activeOnly: true, optional: true });
|
|
626
|
-
case "choose":
|
|
627
|
-
return completeOutcomeArgument(cwd, parsed);
|
|
628
|
-
default:
|
|
629
|
-
return null;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function completeCommands(token: string): CompletionItem[] | null {
|
|
634
|
-
const commands = ["list", "start", "resume", "status", "done", "choose", "cancel"];
|
|
635
|
-
return toCompletionItems(commands, token, (command) => command, (command) => command);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async function completeStartArguments(cwd: string, parsed: CompletionPrefix): Promise<CompletionItem[] | null> {
|
|
639
|
-
const args = parsed.completedTokens.slice(1);
|
|
640
|
-
if (args.length === 0) {
|
|
641
|
-
const playbooks = await loadPlaybooks(cwd);
|
|
642
|
-
return toCompletionItems(
|
|
643
|
-
playbooks,
|
|
644
|
-
parsed.currentToken,
|
|
645
|
-
(playbook) => `start ${playbook.definition.id}`,
|
|
646
|
-
(playbook) => playbook.definition.id,
|
|
647
|
-
(playbook) => playbook.definition.name,
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (args.length >= 1 && !args.includes("--run")) {
|
|
652
|
-
return toCompletionItems(["--run"], parsed.currentToken, (flag) => `start ${args.join(" ")} ${flag} `, (flag) => flag);
|
|
653
|
-
}
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async function completeRunArgument(
|
|
658
|
-
cwd: string,
|
|
659
|
-
parsed: CompletionPrefix,
|
|
660
|
-
command: string,
|
|
661
|
-
options: { activeOnly: boolean; optional?: boolean },
|
|
662
|
-
): Promise<CompletionItem[] | null> {
|
|
663
|
-
const args = parsed.completedTokens.slice(1);
|
|
664
|
-
if (args.length > 0 || (options.optional && parsed.currentToken === "" && !parsed.trailingWhitespace)) return null;
|
|
665
|
-
const runs = await getRunCompletionCandidates(cwd, options.activeOnly);
|
|
666
|
-
return toCompletionItems(
|
|
667
|
-
runs,
|
|
668
|
-
parsed.currentToken,
|
|
669
|
-
(run) => `${command} ${run.runId}`,
|
|
670
|
-
(run) => run.runId,
|
|
671
|
-
(run) => `${run.playbookId} (${run.status})`,
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async function completeOutcomeArgument(cwd: string, parsed: CompletionPrefix): Promise<CompletionItem[] | null> {
|
|
676
|
-
const args = parsed.completedTokens.slice(1);
|
|
677
|
-
if (args.length > 0) return null;
|
|
678
|
-
const { run, playbook } = await loadActive(cwd);
|
|
679
|
-
const step = playbook.definition.steps[run.currentStep];
|
|
680
|
-
if (!step) return null;
|
|
681
|
-
return toCompletionItems(
|
|
682
|
-
step.transitions,
|
|
683
|
-
parsed.currentToken,
|
|
684
|
-
(transition) => `choose ${transition.outcome}`,
|
|
685
|
-
(transition) => transition.outcome,
|
|
686
|
-
(transition) => `to ${transition.to}`,
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
async function getRunCompletionCandidates(cwd: string, activeOnly: boolean): Promise<PlaybookRunState[]> {
|
|
514
|
+
async function getActiveRuns(cwd: string): Promise<PlaybookRunState[]> {
|
|
691
515
|
const ids = await listRunIds(cwd);
|
|
692
516
|
const runs = (await Promise.all(ids.map((id) => loadRun(cwd, id)))).filter((run): run is PlaybookRunState => Boolean(run));
|
|
693
|
-
|
|
694
|
-
return filtered.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function toCompletionItems<T>(
|
|
698
|
-
values: T[],
|
|
699
|
-
token: string,
|
|
700
|
-
valueFor: (item: T) => string,
|
|
701
|
-
labelFor: (item: T) => string,
|
|
702
|
-
descriptionFor?: (item: T) => string | undefined,
|
|
703
|
-
): CompletionItem[] | null {
|
|
704
|
-
const normalizedToken = token.trim().toLowerCase();
|
|
705
|
-
const items = values
|
|
706
|
-
.filter((item) => matchesCompletion(labelFor(item), normalizedToken))
|
|
707
|
-
.map((item) => {
|
|
708
|
-
const description = descriptionFor?.(item);
|
|
709
|
-
return { value: valueFor(item), label: labelFor(item), ...(description ? { description } : {}) };
|
|
710
|
-
});
|
|
711
|
-
return items.length > 0 ? items : null;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function matchesCompletion(value: string, token: string): boolean {
|
|
715
|
-
if (token === "") return true;
|
|
716
|
-
return value.toLowerCase().includes(token);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
type CompletionPrefix = {
|
|
720
|
-
completedTokens: string[];
|
|
721
|
-
currentToken: string;
|
|
722
|
-
command: string | undefined;
|
|
723
|
-
trailingWhitespace: boolean;
|
|
724
|
-
};
|
|
725
|
-
|
|
726
|
-
function parseCompletionPrefix(prefix: string): CompletionPrefix {
|
|
727
|
-
const trailingWhitespace = /\s$/.test(prefix);
|
|
728
|
-
const tokens = parseArgs(prefix);
|
|
729
|
-
const completedTokens = trailingWhitespace ? tokens : tokens.slice(0, -1);
|
|
730
|
-
const currentToken = trailingWhitespace ? "" : tokens.at(-1) ?? "";
|
|
731
|
-
return { completedTokens, currentToken, command: completedTokens[0] ?? (!trailingWhitespace && tokens.length > 1 ? tokens[0] : undefined), trailingWhitespace };
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function parseArgs(args: string): string[] {
|
|
735
|
-
const matches = args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
736
|
-
return matches.map((arg) => arg.replace(/^(["'])(.*)\1$/, "$2"));
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function readFlagValue(args: string[], flag: string): string | undefined {
|
|
740
|
-
const index = args.indexOf(flag);
|
|
741
|
-
if (index === -1) return undefined;
|
|
742
|
-
return args[index + 1];
|
|
517
|
+
return runs.filter((run) => run.status === "active").sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
743
518
|
}
|
|
744
519
|
|
|
745
520
|
interface UiLike {
|
package/package.json
CHANGED
package/src/auto-advance.ts
CHANGED
|
@@ -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
|
|
94
|
+
message: `Completion marked for step '${run.currentStep}'. Confirm outcome with /playbook:choose.`,
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -142,7 +142,7 @@ function suggestPlan(step: PlaybookStep, stepId: string): CompletionPlan {
|
|
|
142
142
|
}
|
|
143
143
|
return {
|
|
144
144
|
kind: "suggest",
|
|
145
|
-
message: `Completion suspected for step '${stepId}'.
|
|
145
|
+
message: `Completion suspected for step '${stepId}'. Run /playbook:choose to select an outcome.`,
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|