@danypops/pi-papyrus 0.35.2 → 0.36.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
@@ -17,7 +17,7 @@ Agent-facing domain tools own lifecycle invariants and sit above this store API:
17
17
  - **`docs`** — create/update/list/show, activate/archive/reopen, and document-safe graph links; Note mutations remain behind the Notes facade
18
18
  - **`rules`** — create/update/list/show/preview, enable/disable, and attach governance gates to tasks
19
19
  - **`skills`** — create/update/list/show/invoke/run, enable/disable, create compatibility templates, and atomically instantiate parameterized workflow runs
20
- - **`playbooks`** — a completely different beast from Skills, not a subtype: a trigger and an ordered list of steps an agent reads and follows, never mechanically instantiated. Like Tasks, a Playbook can be nested or chained with another Playbook: `contain`/`uncontain` nest a child Playbook inside a parent (its steps run as part of the parent); `depend`/`undepend` chain a prerequisite Playbook before another (its steps run first). Both compose recursively when invoked, bounded and cycle-safe. create/update/list/show/invoke, enable/disable, contain/uncontain, depend/undepend. A Playbook can declare named arguments (`{name, description?, required?}`, required defaults true); invoking with some unsupplied lists exactly which required ones are still missing and directs the agent to ask via `discuss` with `live:true` rather than guess
20
+ - **`playbooks`** — a completely different beast from Skills at the authoring level (a trigger and an ordered list of steps, written as prose), but `invoke` recycles the same materialization engine workflow Skills use: it compiles the steps and any `contain`/`depend` composition into real Tasks (one per step, plus a container task per playbook in the tree), wires them with `dependsOn` so completing one auto-focuses the next, and focuses the first one. No text dump — one step surfaces at a time, as it becomes the focused task, same as any other Task. `contain`/`uncontain` nest a child Playbook inside a parent (its steps run after the parent's own, as part of it); `depend`/`undepend` chain a prerequisite Playbook before another (it must fully complete first). `preview` renders the whole tree as text with no side effects, for reading before invoking. A Playbook can declare named arguments (`{name, description?, required?}`, required defaults true; referenced in step text as `{{name}}`); invoking with a required one unsupplied creates nothing and reports exactly which are still missing, directing the agent to ask via `discuss` with `live:true` rather than guess
21
21
 
22
22
  Every tool operation is registered in the daemon's `/api/v1/ops` registry; parity is verified in tests. The task consumer uses the `tasks.graph` operation, which returns task nodes with explicit parent, child, and dependency IDs rather than leaking SQLite rows or asking the UI to reconstruct relationships.
23
23
 
@@ -10,7 +10,7 @@ import {
10
10
  type GateResult,
11
11
  type NoteHistoryPage,
12
12
  type OperationName,
13
- type SkillWorkflowRunResult,
13
+ type WorkflowRunResult,
14
14
  type TaskCompletion,
15
15
  type TaskExecutionPlan,
16
16
  type TaskGraph,
@@ -656,7 +656,7 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
656
656
  pi.registerTool({
657
657
  name: "playbooks",
658
658
  label: "Playbooks",
659
- description: "Playbook domain tool -- a completely different beast from the skills tool, not a subtype of it. A Playbook is a trigger and an ordered list of steps an agent reads and follows; it is never mechanically instantiated the way a Skill's artifact-template or workflow blueprint is. Like Tasks, a Playbook can be nested or chained with another Playbook: contain/uncontain nest a child Playbook inside a parent (invoking the parent recursively runs the child's steps as part of it); depend/undepend chain a prerequisite Playbook before another (invoking the dependent recursively runs the prerequisite's steps first). Both are bounded and cycle-safe -- a composition cycle degrades to a marker instead of infinite-looping, rather than being rejected up front the way a real Task dependency cycle is. ACTIONS: create, list, show, invoke, enable, disable, assign_project, update, contain, uncontain, depend, undepend, remove, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. On create, `arguments` declares named inputs the Playbook needs: [{name, description?, required?}] (required defaults true). On invoke, `arguments` supplies known values as {name: value}; invoke renders which declared REQUIRED arguments are still missing and directs you to ask the human for them via the discuss tool with live:true -- never guess or invent a value for a missing required argument. update changes title/body/labels (at least one required) and is refused for a read-only external projection. remove moves a Playbook to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the playbook's exact title) over `id`, and `parent_name`/`child_name`/`dependency_name` over `parent_id`/`child_id`/`dependency_id` for contain/uncontain/depend/undepend -- all are backend implementation details, resolved from name automatically.",
659
+ description: "Playbook domain tool -- a completely different beast from the skills tool at the AUTHORING level (a Playbook is prose: a trigger and an ordered list of steps), but invoke recycles the exact same materialization engine workflow Skills use: it compiles the Playbook's steps and its contains/depends_on composition tree into real Tasks (one per step, plus one container task per playbook in the tree), wires them with dependsOn so completing one auto-focuses the next, and focuses the first one -- no text dump, one step (page) surfaces at a time as it becomes the focused task, exactly like any other Task. contain/uncontain nest a child Playbook inside a parent (its steps run AFTER the parent's own, as part of it); depend/undepend chain a prerequisite Playbook before another (it must fully complete FIRST). Both are bounded; a composition cycle is a hard invoke-time error (real Tasks would otherwise be created in a loop), unlike preview's degrade-to-a-marker. ACTIONS: create, list, show, invoke, preview, enable, disable, assign_project, update, contain, uncontain, depend, undepend, remove, restore. project_root is optional everywhere (omitted = unscoped). On create, `arguments` declares named inputs the Playbook needs: [{name, description?, required?}] (required defaults true) -- referenced in step text as `{{name}}`, substituted at invoke time. On invoke, `arguments` supplies known values as {name: value}; if any declared REQUIRED argument is still missing, invoke creates nothing and returns `missingArguments` -- ask the human for these (discuss tool, live:true) and invoke again, never guess or invent a value. A successful invoke returns `entryTaskId` (now focused) and `created.tasks` -- drive it forward with the tasks tool (start/submit/complete) like any other Task; contains/depends_on wiring auto-focuses each next step on completion. preview renders the whole tree as text with no side effects, for a human who just wants to read it first. update changes title/body/labels (at least one required) and is refused for a read-only external projection. remove moves a Playbook to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. PREFER `name` (the playbook's exact title) over `id`, and `parent_name`/`child_name`/`dependency_name` over `parent_id`/`child_id`/`dependency_id` for contain/uncontain/depend/undepend -- all are backend implementation details, resolved from name automatically.",
660
660
  parameters: Type.Object({
661
661
  action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
662
662
  body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
@@ -682,7 +682,7 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
682
682
  { nameKey: "child_name", idKey: "child_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
683
683
  { nameKey: "dependency_name", idKey: "dependency_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
684
684
  ]);
685
- if (action === "create" || action === "invoke") normalizeJsonEncodedField(params, "arguments");
685
+ if (action === "create" || action === "invoke" || action === "preview") normalizeJsonEncodedField(params, "arguments");
686
686
  if (action === "create") {
687
687
  const artifact = await callService<Record<string, unknown>, Artifact>("playbooks.create", params);
688
688
  return text(`Created playbook ${artifactLine(artifact)}`, createArtifactDetails("playbooks.create", artifact));
@@ -691,9 +691,18 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
691
691
  const rows = await callService<Record<string, unknown>, Artifact[]>("playbooks.list", params);
692
692
  return text(rows.length ? artifactLines(rows).join("\n") : "No playbooks found.", createArtifactListDetails("playbooks.list", rows));
693
693
  }
694
+ if (action === "preview") {
695
+ const rendered = await callService<Record<string, unknown>, string>("playbooks.preview", params);
696
+ return text(rendered, createPreviewDetails("playbooks.preview", "Playbook preview", rendered));
697
+ }
694
698
  if (action === "invoke") {
695
- const invocation = await callService<Record<string, unknown>, string>("playbooks.invoke", params);
696
- return text(invocation, createPreviewDetails("playbooks.invoke", "Playbook invocation", invocation));
699
+ const invocation = await callService<Record<string, unknown>, { entryTaskId?: string; rootTaskIds?: string[]; created?: { tasks: string[] }; missingArguments?: string[] }>("playbooks.invoke", params);
700
+ if (invocation.missingArguments) {
701
+ const message = `Missing required argument(s): ${invocation.missingArguments.join(", ")}. Nothing was created -- ask the human for these (discuss tool, live:true), then invoke again.`;
702
+ return text(message, createPreviewDetails("playbooks.invoke", "Playbook invocation", message));
703
+ }
704
+ const message = `Invoked: ${invocation.created?.tasks.length ?? 0} task(s) created, entry task ${invocation.entryTaskId} now focused. Drive it forward with the tasks tool (start/submit/complete) -- contains/depends_on wiring auto-focuses each next step.`;
705
+ return text(message, createPreviewDetails("playbooks.invoke", "Playbook invocation", JSON.stringify(invocation, null, 2)));
697
706
  }
698
707
  const trashResult = await handleArtifactRemoveRestore(action, params);
699
708
  if (trashResult) return trashResult;
@@ -755,7 +764,7 @@ export function registerSkillsTool(pi: ExtensionAPI): void {
755
764
  return text(invocation, createPreviewDetails("skills.invoke", "Skill invocation", invocation));
756
765
  }
757
766
  if (action === "run") {
758
- const run = await callService<Record<string, unknown>, SkillWorkflowRunResult>("skills.run", request);
767
+ const run = await callService<Record<string, unknown>, WorkflowRunResult>("skills.run", request);
759
768
  const runTitleCounts = new Map<string, number>();
760
769
  for (const node of run.execution.nodes) runTitleCounts.set(node.title, (runTitleCounts.get(node.title) ?? 0) + 1);
761
770
  const execution = run.execution.nodes.map((node) => (runTitleCounts.get(node.title) ?? 0) > 1
@@ -73,9 +73,17 @@ export function registerPlaybookBridge(pi: ExtensionAPI): void {
73
73
  // Re-fetched live, not captured at registration time: a lingering stale
74
74
  // command (renamed or disabled since, since registerCommand can't be
75
75
  // unregistered) must fail cleanly, never run deleted/stale content.
76
- const invocation = await callService<Record<string, unknown>, string>("playbooks.invoke", { id });
77
- ctx.ui.setEditorText(invocation);
78
- ctx.ui.notify(`"${title}" invocation placed in the editor`, "info");
76
+ // invoke materializes real Tasks and focuses the entry one -- it no longer
77
+ // returns rendered text to drop into the editor (that's playbooks.preview
78
+ // now). The editor gets a short kickoff prompt instead; the actual step
79
+ // content surfaces via the normal Task Focus system-prompt pointer.
80
+ const invocation = await callService<Record<string, unknown>, { entryTaskId?: string; missingArguments?: string[] }>("playbooks.invoke", { id });
81
+ if (invocation.missingArguments) {
82
+ ctx.ui.notify(`"${title}" needs: ${invocation.missingArguments.join(", ")}`, "error");
83
+ return;
84
+ }
85
+ ctx.ui.setEditorText(`Run the "${title}" playbook -- work on the currently focused task.`);
86
+ ctx.ui.notify(`"${title}" invoked: entry task ${invocation.entryTaskId} focused`, "info");
79
87
  } catch (error) {
80
88
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
81
89
  }
@@ -26,14 +26,28 @@ export async function playbookArgumentCompletions(argumentPrefix: string): Promi
26
26
  }
27
27
  }
28
28
 
29
- /** `/playbook <name>` (no args opens the full browser instead): resolves by exact title, then places its invocation directly in the editor -- one step, not browse-then-select-then-invoke. */
29
+ interface PlaybookInvocationResponse {
30
+ entryTaskId?: string;
31
+ missingArguments?: string[];
32
+ }
33
+
34
+ /** Shared by /playbook <name> and the browser's own "Invoke" action: materializes real Tasks and focuses the entry one, then reports it -- invoke no longer returns rendered text (that's playbooks.preview now). */
35
+ async function invokeAndReport(id: string, label: string, ctx: ExtensionCommandContext): Promise<void> {
36
+ const invocation = await callService<Record<string, unknown>, PlaybookInvocationResponse>("playbooks.invoke", { id });
37
+ if (invocation.missingArguments) {
38
+ ctx.ui.notify(`"${label}" needs: ${invocation.missingArguments.join(", ")}`, "error");
39
+ return;
40
+ }
41
+ ctx.ui.setEditorText(`Run the "${label}" playbook -- work on the currently focused task.`);
42
+ ctx.ui.notify(`"${label}" invoked: entry task ${invocation.entryTaskId} focused`, "info");
43
+ }
44
+
45
+ /** `/playbook <name>` (no args opens the full browser instead): resolves by exact title, then invokes it directly -- one step, not browse-then-select-then-invoke. */
30
46
  export async function openPlaybookByName(name: string, ctx: ExtensionCommandContext): Promise<void> {
31
47
  if (!name.trim()) { await showPlaybooks(ctx); return; }
32
48
  try {
33
49
  const id = matchArtifactByName(await activePlaybooks(), name);
34
- const invocation = await callService<Record<string, unknown>, string>("playbooks.invoke", { id });
35
- ctx.ui.setEditorText(invocation);
36
- ctx.ui.notify(`"${name.trim()}" invocation placed in the editor`, "info");
50
+ await invokeAndReport(id, name.trim(), ctx);
37
51
  } catch (error) {
38
52
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
39
53
  }
@@ -75,9 +89,7 @@ export async function showPlaybooks(ctx: ExtensionCommandContext): Promise<void>
75
89
  return;
76
90
  }
77
91
  if (choice === "Invoke") {
78
- const invocation = await callService<Record<string, unknown>, string>("playbooks.invoke", { id: playbook.id });
79
- commandCtx.ui.setEditorText(invocation);
80
- commandCtx.ui.notify("Invocation placed in the editor", "info");
92
+ await invokeAndReport(playbook.id, playbook.title, commandCtx);
81
93
  return;
82
94
  }
83
95
  if (choice === "Link artifact") {
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import type { Artifact, SkillWorkflowRunResult, TaskGraph } from "@danypops/papyrus";
2
+ import type { Artifact, WorkflowRunResult, TaskGraph } from "@danypops/papyrus";
3
3
  import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
4
4
  import { SKILL_STATUS_PRESENTATION } from "./artifact-status-presentation.ts";
5
5
  import { callService } from "./service-client.ts";
@@ -50,7 +50,7 @@ export function skillInvocationPrompt(skill: Artifact): string {
50
50
  ].join("\n");
51
51
  }
52
52
 
53
- export function skillRunTaskGraph(run: SkillWorkflowRunResult, taskArtifacts: Artifact[]): TaskGraph {
53
+ export function skillRunTaskGraph(run: WorkflowRunResult, taskArtifacts: Artifact[]): TaskGraph {
54
54
  const executionById = new Map(run.execution.nodes.map((node) => [node.id, node]));
55
55
  return {
56
56
  nodes: taskArtifacts.map((task) => ({
@@ -95,7 +95,7 @@ export async function showSkills(ctx: ExtensionCommandContext): Promise<void> {
95
95
  if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
96
96
  throw new Error("arguments must be a JSON object");
97
97
  }
98
- const run = await callService<Record<string, unknown>, SkillWorkflowRunResult>("skills.run", {
98
+ const run = await callService<Record<string, unknown>, WorkflowRunResult>("skills.run", {
99
99
  id: skill.id,
100
100
  arguments: arguments_ as Record<string, unknown>,
101
101
  project_root: commandCtx.cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-papyrus",
3
- "version": "0.35.2",
3
+ "version": "0.36.0",
4
4
  "description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@danypops/daemon-kit": "^0.10.0",
21
- "@danypops/papyrus": "^0.35.1",
21
+ "@danypops/papyrus": "^0.36.0",
22
22
  "beautiful-mermaid": "1.1.3"
23
23
  },
24
24
  "devDependencies": {