@danypops/pi-papyrus 0.42.0 → 0.43.1
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 +10 -12
- package/extension/src/artifact-status-presentation.ts +2 -7
- package/extension/src/discuss.ts +1 -1
- package/extension/src/domain-tools.ts +5 -5
- package/extension/src/index.ts +5 -10
- package/extension/src/vehicle-notes-client.ts +3 -3
- package/package.json +5 -5
- package/extension/src/skills.ts +0 -125
package/README.md
CHANGED
|
@@ -10,14 +10,13 @@ The `papyrus_*` tools are the low-level graph-store API:
|
|
|
10
10
|
- **`papyrus_graph`** — link artifacts, perform bounded traversal, or read the mutation event log
|
|
11
11
|
- **`papyrus_show`** — read nested metadata and bounded edges, optionally running gates
|
|
12
12
|
|
|
13
|
-
Agent-facing domain tools own lifecycle invariants and sit above this store API. `discuss` is still a single tool with an `action` parameter -- `live:true` needs an interactive UI round-trip a stateless Vehicle operation can't express. `notes`, `docs`, `rules`, `
|
|
13
|
+
Agent-facing domain tools own lifecycle invariants and sit above this store API. `discuss` is still a single tool with an `action` parameter -- `live:true` needs an interactive UI round-trip a stateless Vehicle operation can't express. `notes`, `docs`, `rules`, `playbooks`, and `tasks` are projected from Papyrus's own Vehicle as one real tool per operation (`notes_capture`, `rules_create`, `playbooks_invoke`, `tasks_complete`, and so on) -- no `action` dispatch, each with its own schema:
|
|
14
14
|
|
|
15
15
|
- **tasks** (`tasks_create`, `tasks_update`, `tasks_list`, `tasks_show`, `tasks_plan`, `tasks_graph`, `tasks_focus`, `tasks_pause`, `tasks_unpause`, `tasks_clear_focus`, `tasks_start`, `tasks_submit`, `tasks_complete`, `tasks_reject`, `tasks_retry`, `tasks_cancel`, `tasks_cancel_subtree`, `tasks_run_gates`, `tasks_set_checklist`, `tasks_set_gates`, `tasks_depend`, `tasks_undepend`, `tasks_contain`, `tasks_uncontain`, `tasks_claim`, `tasks_heartbeat_lease`, `tasks_release_lease`, `tasks_lease`, `tasks_context`, `tasks_event_feed`, `tasks_scope`, `tasks_set_scope`, `tasks_assign_project`, `tasks_active`, `tasks_focused`, `tasks_history`) — manages the singleton active focus, evidence-bearing checklists, hierarchy/dependencies, lifecycle transitions, non-blocking gates, and review completion that focuses one deterministic ready successor without claiming effort. `project_root` is required wherever a plain `tasks` call would otherwise need one (list/graph/plan/active/focused/scope/context/create) -- there is no ambient Pi cwd server-side. `tasks_focus`/`tasks_pause`/`tasks_unpause`/`tasks_clear_focus` still authorize their write via this session's own cached secret, and still broadcast `papyrus.task-focus.v1` on Pi's own event bus for a sibling extension to observe, exactly as before
|
|
16
|
-
- **notes** (`notes_capture`, `notes_list`, `notes_show`, `notes_consume`, `notes_promote`, `notes_archive`) — capture/list/show deferred human intent, mark it consumed, promote it to an existing Task/Doc/Rule/
|
|
16
|
+
- **notes** (`notes_capture`, `notes_list`, `notes_show`, `notes_consume`, `notes_promote`, `notes_archive`) — capture/list/show deferred human intent, mark it consumed, promote it to an existing Task/Doc/Rule/Playbook, or archive it with an explicit disposition
|
|
17
17
|
- **docs** (`docs_create`, `docs_list`, `docs_show`, `docs_activate`, `docs_archive`, `docs_reopen`, `docs_link`, `docs_assign_project`, `docs_update`) — activate/archive/reopen and document-safe graph links; Note mutations remain behind the Notes facade
|
|
18
18
|
- **rules** (`rules_create`, `rules_list`, `rules_show`, `rules_preview`, `rules_enable`, `rules_disable`, `rules_gate`, `rules_assign_project`, `rules_update`) — enable/disable and attach governance gates to tasks
|
|
19
|
-
- **
|
|
20
|
-
- **playbooks** (`playbooks_create`, `playbooks_list`, `playbooks_show`, `playbooks_invoke`, `playbooks_preview`, `playbooks_enable`, `playbooks_disable`, `playbooks_assign_project`, `playbooks_update`, `playbooks_contain`, `playbooks_uncontain`, `playbooks_depend`, `playbooks_undepend`) — a completely different beast from Skills at the authoring level (a trigger and an ordered list of steps, written as prose), but `playbooks_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. `playbooks_contain`/`playbooks_uncontain` nest a child Playbook inside a parent (its steps run after the parent's own, as part of it); `playbooks_depend`/`playbooks_undepend` chain a prerequisite Playbook before another (it must fully complete first). `playbooks_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
|
|
19
|
+
- **playbooks** (`playbooks_create`, `playbooks_list`, `playbooks_show`, `playbooks_invoke`, `playbooks_preview`, `playbooks_enable`, `playbooks_disable`, `playbooks_assign_project`, `playbooks_update`, `playbooks_contain`, `playbooks_uncontain`, `playbooks_depend`, `playbooks_undepend`) — a trigger and an ordered list of steps. Each step is a plain prose string (a task), or a structured object: `{kind:'doc',title,body?,subtype?,labels?}` creates a Doc, `{kind:'rule',title,body?,condition?,action?,severity?,labels?}` creates a Rule, `{kind:'call',title,playbookId,arguments?}` nests another Playbook's own run as a pipeline step gated in the same sequence, `{kind:'task',title?,body}` is an explicit task step. `playbooks_invoke` recycles the shared blueprint materialization engine: it compiles the steps and any `contain`/`depend` composition into real artifacts (a Task per plain/task step, a Doc/Rule per doc/rule step, a nested run per call step), wires task-like steps with `dependsOn` so completing one auto-focuses the next, and focuses the first real task. No text dump — one step surfaces at a time, as it becomes the focused task, same as any other Task. `playbooks_contain`/`playbooks_uncontain` nest a child Playbook inside a parent (its steps run after the parent's own, as part of it); `playbooks_depend`/`playbooks_undepend` chain a prerequisite Playbook before another (it must fully complete first) -- both are whole-Playbook composition, distinct from a `call` step's finer-grained, single-step nesting. `playbooks_preview` renders the whole tree as text with no side effects, for reading before invoking. A Playbook can declare named arguments (`{name, description?, required?, type?('string'|'number'|'boolean', default 'string'), enum?, default?}`, required defaults true; referenced in step text/call arguments 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
20
|
|
|
22
21
|
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
22
|
|
|
@@ -28,15 +27,14 @@ Every tool operation is registered in the daemon's `/api/v1/ops` registry; parit
|
|
|
28
27
|
- `/notes` — searchable project Notes inbox with consume, promote, and disposition-aware archive actions
|
|
29
28
|
- `/docs` — searchable non-Note documents, lifecycle, details, edit, and graph links
|
|
30
29
|
- `/rules` — severity/condition rows, exact injection preview, edit, enable/disable, and task gating
|
|
31
|
-
- `/skills` — trigger/tools rows, edit, invocation into the editor, and artifact templates
|
|
32
30
|
- `/playbooks` — trigger/tools rows, edit, invocation into the editor, and graph links
|
|
33
31
|
- `/playbook <name>` — tab-completes active playbook titles and places that one's invocation directly in the editor, one step instead of browse-then-select; no argument falls back to the full `/playbooks` browser
|
|
34
32
|
|
|
35
|
-
All frontends use daemon-backed domain operations; none opens SQLite from the Pi process. **Show details** opens a bounded navigable view across Tasks, Notes, Docs, Rules,
|
|
33
|
+
All frontends use daemon-backed domain operations; none opens SQLite from the Pi process. **Show details** opens a bounded navigable view across Tasks, Notes, Docs, Rules, and Playbooks. User-authored bodies render as width-aware Markdown with headings, emphasis, links, quotes, lists, tables, inline/fenced code, syntax highlighting, and every color/decorative style derived dynamically from the active Pi theme. Generated lifecycle, metadata, checklist, gate, history, and relationship sections keep explicit semantic theme colors; relationships render as a small Unicode graph via `beautiful-mermaid` when the neighbor set is real and within the routed-rendering bound, falling back to a plain, still name-resolved arrow list otherwise. `↑/↓` scrolls, `←/→` pans wide relationships, and Esc returns to the browser; non-interactive clients receive stable source text.
|
|
36
34
|
|
|
37
35
|
## Notes
|
|
38
36
|
|
|
39
|
-
Notes are project-scoped `doc/note` artifacts for human requests that should be considered later. Capturing a Note does not create work, inject the entire inbox into prompts, or imply acceptance. The agent can use the `notes` domain tool to list and consume open Notes, decide whether to create a Task, Doc, Rule, or
|
|
37
|
+
Notes are project-scoped `doc/note` artifacts for human requests that should be considered later. Capturing a Note does not create work, inject the entire inbox into prompts, or imply acceptance. The agent can use the `notes` domain tool to list and consume open Notes, decide whether to create a Task, Doc, Rule, or Playbook through its owning domain tool, then promote the Note by linking that artifact. Archive requires one of `completed`, `duplicate`, `declined`, or `superseded`; promote archives with a `promoted` disposition and target ID. Capture, consumption, and disposition provenance remain in bounded Note history.
|
|
40
38
|
|
|
41
39
|
The default inbox contains draft and consumed/active Notes, is bounded to 50 rows, and has a hard limit of 200. Bodies are capped at 10,000 characters. Generic document and graph lifecycle operations reject Note mutations so they cannot bypass disposition provenance.
|
|
42
40
|
|
|
@@ -45,7 +43,7 @@ papyrus notes capture "Investigate the retry policy" --json
|
|
|
45
43
|
papyrus notes list --limit 25 --json
|
|
46
44
|
papyrus notes show <note-id> --json
|
|
47
45
|
papyrus notes consume <note-id> --json
|
|
48
|
-
# Create the resulting artifact with tasks/docs/rules/
|
|
46
|
+
# Create the resulting artifact with tasks/docs/rules/playbooks first, then:
|
|
49
47
|
papyrus notes promote <note-id> <target-id> --reason "Converted to tracked work" --json
|
|
50
48
|
papyrus notes archive <note-id> declined --reason "No longer relevant" --json
|
|
51
49
|
```
|
|
@@ -62,7 +60,7 @@ Blocking is real: `tasks.complete` is refused while any `active` Discussion has
|
|
|
62
60
|
|
|
63
61
|
`open`/`reply` can also pose a structured choice instead of (or alongside) free text: `options` (2-10 entries) plus `options_mode` -- `single` is mutually exclusive (exactly one pick), `multi` allows several. The Discussion remembers the pending choice (`extra.discussion.pendingOptions`/`pendingOptionsMode`) until a `reply` answers it with `selected`, validated against exactly what was offered and the mode's cardinality; a reply can also pose the *next* round's choice in the same call.
|
|
64
62
|
|
|
65
|
-
Run `/discuss` for the interactive panel: browse every Discussion (the real `active`/`deferred`/`settled` state shown per row, alongside any choice awaiting an answer), open a scrollable transcript showing what was posed and picked in each round, and reply/defer/resume/settle or block/unblock a task without leaving the TUI. Replying to a pending choice shows a real picker -- the native single-select list for `single`, or a checkbox multi-select for `multi`, since no built-in multi-select exists in the Pi extension UI. Both modes append a numbered "type your own answer" row -- a genuinely open answer is exactly as valid as any posed option. The multi-select picker supports a number key as a direct quick-select (jump straight to that row instead of scrolling), and steadily highlights checked rows while dimming the rest so the eye reads "what's chosen" independent of cursor position; its cursor row blinks to mark focus. It also auto-cancels after 30s of zero input -- the very first keystroke of any kind stops that countdown permanently for that prompt. Opening a *new* Discussion is left to the agent (same as Docs/Rules/
|
|
63
|
+
Run `/discuss` for the interactive panel: browse every Discussion (the real `active`/`deferred`/`settled` state shown per row, alongside any choice awaiting an answer), open a scrollable transcript showing what was posed and picked in each round, and reply/defer/resume/settle or block/unblock a task without leaving the TUI. Replying to a pending choice shows a real picker -- the native single-select list for `single`, or a checkbox multi-select for `multi`, since no built-in multi-select exists in the Pi extension UI. Both modes append a numbered "type your own answer" row -- a genuinely open answer is exactly as valid as any posed option. The multi-select picker supports a number key as a direct quick-select (jump straight to that row instead of scrolling), and steadily highlights checked rows while dimming the rest so the eye reads "what's chosen" independent of cursor position; its cursor row blinks to mark focus. It also auto-cancels after 30s of zero input -- the very first keystroke of any kind stops that countdown permanently for that prompt. Opening a *new* Discussion is left to the agent (same as Docs/Rules/Playbooks) -- `/discuss` browses and drives existing ones.
|
|
66
64
|
|
|
67
65
|
```bash
|
|
68
66
|
papyrus discuss open --title "Naming" --actor alice --content "Should we rename this?" --blocks-json '["task-id"]' --json
|
|
@@ -119,7 +117,7 @@ papyrus tasks cancel <id> --json
|
|
|
119
117
|
papyrus tasks cancel-subtree <id> --json
|
|
120
118
|
```
|
|
121
119
|
|
|
122
|
-
`cancel-subtree` cancels a Task and every Task in its containment (`contains`) subtree in one call -- for tearing down a whole materialized Playbook
|
|
120
|
+
`cancel-subtree` cancels a Task and every Task in its containment (`contains`) subtree in one call -- for tearing down a whole materialized Playbook run at once instead of canceling each Task id by hand. A Task already `done`/`canceled` is skipped, not treated as an error.
|
|
123
121
|
|
|
124
122
|
Task edits mutate the existing Papyrus-owned Task identity and append an `updated` event; title, body, and labels can be revised without canceling the Task or creating a replacement. Lifecycle, relationships, gates, checklist metadata, scope, and focus remain intact. The same `update` action provides a narrowly guarded recovery for Tasks accidentally created terminal by a legacy default: `status=todo` requires an audit reason, cannot be combined with content edits, only applies when `created` is the sole lifecycle event, and appends `creation_recovered` rather than rewriting history.
|
|
125
123
|
|
|
@@ -133,10 +131,10 @@ Checklist criteria are an item-to-proof map. Every new item requires one or more
|
|
|
133
131
|
|
|
134
132
|
```ts
|
|
135
133
|
checklist: {
|
|
136
|
-
"Write failing
|
|
134
|
+
"Write failing playbook-row tests": {
|
|
137
135
|
proof: [
|
|
138
136
|
{ type: "file", target: "test/frontends.test.ts" },
|
|
139
|
-
{ type: "symbol", target: "test/frontends.test.ts#
|
|
137
|
+
{ type: "symbol", target: "test/frontends.test.ts#playbook row test" }
|
|
140
138
|
]
|
|
141
139
|
}
|
|
142
140
|
}
|
|
@@ -3,8 +3,8 @@ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
/**
|
|
4
4
|
* Shared {label, glyph, color} shape, mirroring task-presentation.ts's TASK_STATUS_PRESENTATION
|
|
5
5
|
* for every other artifact kind's status. Centralizing this closes a real gap: every artifact
|
|
6
|
-
* browser (Rules, Docs, Notes,
|
|
7
|
-
* all, which is exactly why "hard to understand which rules are active" was a real complaint --
|
|
6
|
+
* browser (Rules, Docs, Notes, Playbooks) previously rendered status as a bare glyph with no color
|
|
7
|
+
* at all, which is exactly why "hard to understand which rules are active" was a real complaint --
|
|
8
8
|
* an active rule's "●" and a deprecated rule's "○" differ only by one filled-vs-hollow pixel shape,
|
|
9
9
|
* easy to miss at a glance across a scrolling list.
|
|
10
10
|
*/
|
|
@@ -31,11 +31,6 @@ export const NOTE_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
|
|
|
31
31
|
archived: { label: "archived", glyph: "■", color: "dim" },
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
export const SKILL_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
|
|
35
|
-
active: { label: "active", glyph: "●", color: "success" },
|
|
36
|
-
deprecated: { label: "deprecated", glyph: "○", color: "muted" },
|
|
37
|
-
};
|
|
38
|
-
|
|
39
34
|
export const PLAYBOOK_STATUS_PRESENTATION: Record<string, StatusPresentation> = {
|
|
40
35
|
active: { label: "active", glyph: "●", color: "success" },
|
|
41
36
|
deprecated: { label: "deprecated", glyph: "○", color: "muted" },
|
package/extension/src/discuss.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* surfaces severity and notes.ts surfaces history count, both also not the row glyph.
|
|
10
10
|
*
|
|
11
11
|
* Creating a new Discussion is left to the agent (the discuss tool), matching docs.ts/rules.ts/
|
|
12
|
-
*
|
|
12
|
+
* playbooks.ts precedent -- Notes is the one kind with a human-facing creation command (/note),
|
|
13
13
|
* because Notes exists specifically as a human-authored inbox.
|
|
14
14
|
*/
|
|
15
15
|
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -118,7 +118,7 @@ export function matchArtifactByName(candidates: Artifact[], name: string): strin
|
|
|
118
118
|
/**
|
|
119
119
|
* tasks.list is the one list operation that requires `project_root` and separately supports a
|
|
120
120
|
* `scope` ("project" | "graph" | "all") to widen or narrow the search. Every other list operation
|
|
121
|
-
* (docs.list, rules.list,
|
|
121
|
+
* (docs.list, rules.list, playbooks.list, artifact.query, ...) instead treats an
|
|
122
122
|
* omitted `project_root` as an unscoped/global search (domain-services.ts's listScoped) and has
|
|
123
123
|
* no `scope` concept at all -- so "search everywhere" means something different for each.
|
|
124
124
|
*/
|
|
@@ -133,7 +133,7 @@ function widenedRequest(listOperation: OperationName, baseRequest: Record<string
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* Resolves a name to its id via `listOperation` (whichever kind's list call is the right search
|
|
136
|
-
* scope -- tasks.list, docs.list, rules.list,
|
|
136
|
+
* scope -- tasks.list, docs.list, rules.list, playbooks.list, notes.list, discuss.list, or the
|
|
137
137
|
* kind-agnostic artifact.query for a cross-kind reference like a link target). `baseRequest`
|
|
138
138
|
* should mirror whatever scoping (project_root, etc.) that operation's own "list" action already
|
|
139
139
|
* uses, so resolution never searches a wider or narrower scope than a plain list call would.
|
|
@@ -195,7 +195,7 @@ async function resolveNameArrayField(
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
|
-
* Shared "remove"/"restore" dispatch for every domain tool (tasks/docs/rules/
|
|
198
|
+
* Shared "remove"/"restore" dispatch for every domain tool (tasks/docs/rules/playbooks) --
|
|
199
199
|
* artifact.remove/restore are kind-agnostic composition-root operations (see service.ts),
|
|
200
200
|
* not owned by any one domain module, so every domain tool exposes the same two actions
|
|
201
201
|
* over the same two operations rather than reinventing trash semantics four times.
|
|
@@ -234,7 +234,7 @@ async function handleArtifactRemoveRestore(action: unknown, params: Record<strin
|
|
|
234
234
|
return null;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
// notes.*, rules.*, docs.*,
|
|
237
|
+
// notes.*, rules.*, docs.*, playbooks.*, tasks.*, and the shared artifact.* are
|
|
238
238
|
// registered as Vehicles (see ../vehicle-notes-client.ts and @danypops/papyrus's
|
|
239
239
|
// src/vehicle/papyrus-vehicle.ts), not pi.registerTool()s in this file.
|
|
240
240
|
|
|
@@ -341,7 +341,7 @@ export function registerDiscussTool(pi: ExtensionAPI): void {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
/** Thin orchestrator: each domain's tool is independently navigable/testable via its own registerXTool function. */
|
|
344
|
-
// notes, rules, docs,
|
|
344
|
+
// notes, rules, docs, playbooks, and tasks are no longer registered here -- all migrated
|
|
345
345
|
// onto Vehicle (registerNotesVehicle in vehicle-notes-client.ts, wired at session_start in
|
|
346
346
|
// index.ts), replacing their own pi.registerTool() mega-tools. See @danypops/papyrus's
|
|
347
347
|
// src/vehicle/papyrus-vehicle.ts for the server side. discuss remains here -- live:true needs an
|
package/extension/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-papyrus — native Pi extension for the Papyrus graph store.
|
|
3
3
|
*
|
|
4
|
-
* Tools: papyrus_query/graph/show (low-level), plus one native tool per domain (docs/rules/
|
|
4
|
+
* Tools: papyrus_query/graph/show (low-level), plus one native tool per domain (docs/rules/playbooks/tasks/discuss/notes).
|
|
5
5
|
* Command: /tasks (interactive task panel).
|
|
6
6
|
* Widget: persistent task status above editor (rpiv-todo pattern).
|
|
7
7
|
* Injection: active rules + open tasks appended to system prompt every turn.
|
|
@@ -327,7 +327,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
327
327
|
const contextInjectionProducerId = randomUUID();
|
|
328
328
|
let previousContextInjectionFingerprint: string | undefined;
|
|
329
329
|
let logTurnSequence = 0;
|
|
330
|
-
// Papyrus's own Context Hub contribution (rules/tasks/
|
|
330
|
+
// Papyrus's own Context Hub contribution (rules/tasks/Pi's own skill catalog, bundled into one segment --
|
|
331
331
|
// see context-hub-contribution.ts) re-emits every turn alongside the existing injection
|
|
332
332
|
// observation, its own independent monotonic sequence, same cadence and shape as
|
|
333
333
|
// contextInjectionSequence but on a different channel/schema.
|
|
@@ -438,7 +438,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
438
438
|
"ACTIONS: link (from+relation+to), unlink (from+relation+to — idempotent, no error if already absent; for Task depends_on/contains prefer the tasks tool's undepend/uncontain), " +
|
|
439
439
|
"tree (id → bounded BFS subgraph), " +
|
|
440
440
|
"history (who did what, when — requires id, actor, or session_id). " +
|
|
441
|
-
"status (id+status) exists at the protocol level but is refused for every kind with its own lifecycle (Doc/Rule/
|
|
441
|
+
"status (id+status) exists at the protocol level but is refused for every kind with its own lifecycle (Doc/Rule/Playbook/Task/Note all reject it) -- use that kind's own domain tool for status changes (docs.activate, rules.enable, tasks.start, etc), never this. " +
|
|
442
442
|
"PREFER `from_name`/`to_name` over `from`/`to` for link/unlink -- both are backend implementation details, resolved from name automatically, searching across every kind since either end of an edge can be any artifact.",
|
|
443
443
|
parameters: Type.Object({
|
|
444
444
|
action: Type.String({ description: "link | unlink | tree | status | history" }),
|
|
@@ -462,7 +462,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
462
462
|
try {
|
|
463
463
|
const params: Record<string, unknown> = { ...rawParams };
|
|
464
464
|
if (params.action === "link" || params.action === "unlink") {
|
|
465
|
-
// Kind-agnostic: either end of an edge can be a task, doc, rule,
|
|
465
|
+
// Kind-agnostic: either end of an edge can be a task, doc, rule, or playbook.
|
|
466
466
|
await resolveNameFields(params, [
|
|
467
467
|
{ nameKey: "from_name", idKey: "from", listOperation: "artifact.query", baseRequest: {} },
|
|
468
468
|
{ nameKey: "to_name", idKey: "to", listOperation: "artifact.query", baseRequest: {} },
|
|
@@ -563,12 +563,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
563
563
|
// ── Interactive artifact browsers ──────────────────────────────────
|
|
564
564
|
|
|
565
565
|
// Lazy imports keep TUI components out of non-interactive startup paths.
|
|
566
|
-
const [tasksModule, docsModule, notesModule, rulesModule,
|
|
566
|
+
const [tasksModule, docsModule, notesModule, rulesModule, playbooksModule, discussModule] = await Promise.all([
|
|
567
567
|
import("./tasks.ts"),
|
|
568
568
|
import("./docs.ts"),
|
|
569
569
|
import("./notes.ts"),
|
|
570
570
|
import("./rules.ts"),
|
|
571
|
-
import("./skills.ts"),
|
|
572
571
|
import("./playbooks.ts"),
|
|
573
572
|
import("./discuss.ts"),
|
|
574
573
|
]);
|
|
@@ -607,10 +606,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
607
606
|
description: "Browse, preview, and toggle Papyrus rules (interactive)",
|
|
608
607
|
handler: async (_args, ctx) => { await rulesModule.showRules(ctx); },
|
|
609
608
|
});
|
|
610
|
-
pi.registerCommand("skills", {
|
|
611
|
-
description: "Browse and invoke Papyrus skills and templates (interactive)",
|
|
612
|
-
handler: async (_args, ctx) => { await skillsModule.showSkills(ctx); },
|
|
613
|
-
});
|
|
614
609
|
pi.registerCommand("playbooks", {
|
|
615
610
|
description: "Browse, edit, and invoke Papyrus playbooks -- trigger/steps guidance an agent reads and follows (interactive)",
|
|
616
611
|
handler: async (_args, ctx) => { await playbooksModule.showPlaybooks(ctx); },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registers every Vehicle-projected domain (notes.*, rules.*, docs.*,
|
|
3
|
-
*
|
|
2
|
+
* Registers every Vehicle-projected domain (notes.*, rules.*, docs.*, playbooks.*,
|
|
3
|
+
* tasks.*, artifact.*) as real Pi tools -- see @danypops/papyrus's
|
|
4
4
|
* src/vehicle/papyrus-vehicle.ts.
|
|
5
5
|
*
|
|
6
6
|
* Fails silently on a stale/unreachable daemon handle instead of aborting extension
|
|
@@ -20,7 +20,7 @@ import { emitTaskFocusEvent } from "./task-focus-events.ts";
|
|
|
20
20
|
|
|
21
21
|
const REGISTERED_PERMISSIONS = [
|
|
22
22
|
"notes:read", "notes:write", "rules:read", "rules:write", "docs:read", "docs:write",
|
|
23
|
-
"
|
|
23
|
+
"playbooks:read", "playbooks:write", "tasks:read", "tasks:write",
|
|
24
24
|
"artifact:read", "artifact:write",
|
|
25
25
|
];
|
|
26
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.1",
|
|
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,13 +18,13 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@danypops/jittor": "^0.14.0",
|
|
21
|
-
"@danypops/papyrus": "^0.
|
|
21
|
+
"@danypops/papyrus": "^0.42.0",
|
|
22
|
+
"@danypops/vehicle-client": "^0.1.1",
|
|
23
|
+
"@danypops/vehicle-client-pi": "^0.3.1",
|
|
22
24
|
"@danypops/vehicle-core": "^0.2.0",
|
|
23
25
|
"@danypops/vehicle-server": "^0.1.1",
|
|
24
|
-
"@danypops/vehicle-client": "^0.1.1",
|
|
25
|
-
"@danypops/vehicle-client-pi": "^0.3.0",
|
|
26
26
|
"beautiful-mermaid": "1.1.3",
|
|
27
|
-
"malevich-tui-components": "^0.
|
|
27
|
+
"malevich-tui-components": "^0.16.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
package/extension/src/skills.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { Artifact, WorkflowRunResult, TaskGraph } from "@danypops/papyrus";
|
|
3
|
-
import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
|
|
4
|
-
import { SKILL_STATUS_PRESENTATION } from "./artifact-status-presentation.ts";
|
|
5
|
-
import { callService } from "./service-client.ts";
|
|
6
|
-
import { showTaskGraph } from "./task-graph.ts";
|
|
7
|
-
|
|
8
|
-
function strings(value: unknown): string[] {
|
|
9
|
-
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function skillRowMeta(skill: Artifact): string {
|
|
13
|
-
if (skill.subtype === "artifact-template") {
|
|
14
|
-
const target = typeof skill.extra["targetKind"] === "string" ? skill.extra["targetKind"] : "artifact";
|
|
15
|
-
return `template → ${target}`;
|
|
16
|
-
}
|
|
17
|
-
if (skill.subtype === "workflow") {
|
|
18
|
-
const definition = skill.extra["definition"] as Record<string, unknown> | undefined;
|
|
19
|
-
const inputs = definition?.["inputs"] && typeof definition["inputs"] === "object"
|
|
20
|
-
? Object.keys(definition["inputs"] as Record<string, unknown>).length
|
|
21
|
-
: 0;
|
|
22
|
-
const blueprints = definition?.["blueprints"] as Record<string, unknown> | undefined;
|
|
23
|
-
const tasks = Array.isArray(blueprints?.["tasks"]) ? blueprints["tasks"].length : 0;
|
|
24
|
-
return `workflow · ${inputs} inputs · ${tasks} tasks`;
|
|
25
|
-
}
|
|
26
|
-
const trigger = typeof skill.extra["trigger"] === "string" ? `when ${skill.extra["trigger"]}` : "manual";
|
|
27
|
-
const tools = strings(skill.extra["tools"]);
|
|
28
|
-
return [trigger, tools.join(", ")].filter(Boolean).join(" · ");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function skillInvocationPrompt(skill: Artifact): string {
|
|
32
|
-
if (skill.subtype === "artifact-template") {
|
|
33
|
-
return [`Create an artifact using Papyrus template \"${skill.title}\".`, `template_name: ${skill.title}`, "Ask for or infer the title and all required template fields, then call the skills domain tool with action=instantiate."].join("\n");
|
|
34
|
-
}
|
|
35
|
-
if (skill.subtype === "workflow") {
|
|
36
|
-
return [
|
|
37
|
-
`Run Papyrus workflow Skill \"${skill.title}\".`,
|
|
38
|
-
"Collect its required arguments, then call the skills domain tool with action=run.",
|
|
39
|
-
].join("\n");
|
|
40
|
-
}
|
|
41
|
-
const trigger = typeof skill.extra["trigger"] === "string" ? skill.extra["trigger"] : "manual invocation";
|
|
42
|
-
const steps = strings(skill.extra["steps"]);
|
|
43
|
-
const tools = strings(skill.extra["tools"]);
|
|
44
|
-
return [
|
|
45
|
-
`Apply Papyrus skill \"${skill.title}\".`,
|
|
46
|
-
`Trigger: ${trigger}`,
|
|
47
|
-
...(skill.body ? [`Context: ${skill.body}`] : []),
|
|
48
|
-
...(steps.length > 0 ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
|
|
49
|
-
...(tools.length > 0 ? [`Tools: ${tools.join(", ")}`] : []),
|
|
50
|
-
].join("\n");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function skillRunTaskGraph(run: WorkflowRunResult, taskArtifacts: Artifact[]): TaskGraph {
|
|
54
|
-
const executionById = new Map(run.execution.nodes.map((node) => [node.id, node]));
|
|
55
|
-
return {
|
|
56
|
-
nodes: taskArtifacts.map((task) => ({
|
|
57
|
-
task,
|
|
58
|
-
active: executionById.get(task.id)?.active === true,
|
|
59
|
-
parentIds: [],
|
|
60
|
-
childIds: [],
|
|
61
|
-
dependencyIds: executionById.get(task.id)?.prerequisiteIds ?? [],
|
|
62
|
-
})),
|
|
63
|
-
rootIds: run.rootTaskIds,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function showSkills(ctx: ExtensionCommandContext): Promise<void> {
|
|
68
|
-
await showArtifactBrowser(ctx, {
|
|
69
|
-
kind: "skill",
|
|
70
|
-
title: "Skills",
|
|
71
|
-
listOperation: "skills.list",
|
|
72
|
-
statusOrder: ["active", "deprecated"],
|
|
73
|
-
presentation: SKILL_STATUS_PRESENTATION,
|
|
74
|
-
rowMeta: skillRowMeta,
|
|
75
|
-
actions: (skill) => [
|
|
76
|
-
"Show details",
|
|
77
|
-
"Edit",
|
|
78
|
-
skill.subtype === "artifact-template" ? "Use template" : skill.subtype === "workflow" ? "Run workflow" : "Invoke skill",
|
|
79
|
-
skill.status === "active" ? "Disable" : "Enable",
|
|
80
|
-
],
|
|
81
|
-
handleAction: async (choice, skill, commandCtx) => {
|
|
82
|
-
if (choice === "Show details") await showArtifactDetails(commandCtx, skill.id, "skills.show");
|
|
83
|
-
else if (choice === "Edit") {
|
|
84
|
-
const title = await commandCtx.ui.input("Title:", skill.title);
|
|
85
|
-
if (title === undefined) return; // canceled
|
|
86
|
-
const body = await commandCtx.ui.input("Body:", skill.body);
|
|
87
|
-
if (body === undefined) return; // canceled
|
|
88
|
-
const updated = await callService<Record<string, unknown>, Artifact>("skills.update", { id: skill.id, title, body });
|
|
89
|
-
commandCtx.ui.notify(`Updated "${updated.title}"`, "info");
|
|
90
|
-
} else if (choice === "Run workflow") {
|
|
91
|
-
const source = await commandCtx.ui.input("Workflow arguments JSON:", "{}");
|
|
92
|
-
if (source === undefined) return;
|
|
93
|
-
try {
|
|
94
|
-
const arguments_ = JSON.parse(source) as unknown;
|
|
95
|
-
if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
|
|
96
|
-
throw new Error("arguments must be a JSON object");
|
|
97
|
-
}
|
|
98
|
-
const run = await callService<Record<string, unknown>, WorkflowRunResult>("skills.run", {
|
|
99
|
-
id: skill.id,
|
|
100
|
-
arguments: arguments_ as Record<string, unknown>,
|
|
101
|
-
project_root: commandCtx.cwd,
|
|
102
|
-
});
|
|
103
|
-
commandCtx.ui.notify([
|
|
104
|
-
`Created ${run.runId} · ${run.created.tasks.length} tasks · ${run.rootTaskIds.length} ready roots`,
|
|
105
|
-
`Context docs: ${run.created.docs.join(", ") || "none"}`,
|
|
106
|
-
`Scoped rules: ${run.created.rules.join(", ") || "none"}`,
|
|
107
|
-
].join("\n"), "info");
|
|
108
|
-
const taskArtifacts = await Promise.all(run.execution.nodes.map((node) =>
|
|
109
|
-
callService<Record<string, unknown>, Artifact>("tasks.show", { id: node.id })));
|
|
110
|
-
await showTaskGraph(commandCtx, skillRunTaskGraph(run, taskArtifacts));
|
|
111
|
-
} catch (error) {
|
|
112
|
-
commandCtx.ui.notify(`Workflow run failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
113
|
-
}
|
|
114
|
-
} else if (choice === "Invoke skill" || choice === "Use template") {
|
|
115
|
-
const invocation = await callService<Record<string, unknown>, string>("skills.invoke", { id: skill.id });
|
|
116
|
-
commandCtx.ui.setEditorText(invocation);
|
|
117
|
-
commandCtx.ui.notify("Invocation placed in the editor", "info");
|
|
118
|
-
} else {
|
|
119
|
-
const operation = choice === "Disable" ? "skills.disable" : "skills.enable";
|
|
120
|
-
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: skill.id });
|
|
121
|
-
commandCtx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
}
|