@danypops/pi-papyrus 0.40.0 → 0.41.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 +6 -6
- package/extension/src/domain-tools.ts +7 -220
- package/extension/src/vehicle-notes-client.ts +27 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@ 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:
|
|
13
|
+
Agent-facing domain tools own lifecycle invariants and sit above this store API. `tasks` and `discuss` are still single tools with an `action` parameter; `notes`, `docs`, `rules`, `skills`, and `playbooks` are projected from Papyrus's own Vehicle as one real tool per operation (`notes_capture`, `rules_create`, `skills_run`, `playbooks_invoke`, and so on) -- no `action` dispatch, each with its own schema:
|
|
14
14
|
|
|
15
15
|
- **`tasks`** — create/update/list/show/plan, manage the singleton active focus, replace evidence-bearing checklists, hierarchy/dependencies, lifecycle transitions, non-blocking gates, and review completion that focuses one deterministic ready successor without claiming effort
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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/Skill, or archive it with an explicit disposition
|
|
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
|
+
- **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
|
+
- **skills** (`skills_create`, `skills_create_template`, `skills_list`, `skills_show`, `skills_invoke`, `skills_run`, `skills_enable`, `skills_disable`, `skills_instantiate`, `skills_assign_project`, `skills_update`) — `skills_run` atomically instantiates a parameterized workflow run; `skills_instantiate` instantiates a compatibility artifact-template
|
|
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
|
|
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
|
|
|
@@ -8,9 +8,6 @@ import {
|
|
|
8
8
|
type DiscussionRound,
|
|
9
9
|
type GateResult,
|
|
10
10
|
type OperationName,
|
|
11
|
-
type PlaybookInvocationResult,
|
|
12
|
-
type PlaybookMissingArguments,
|
|
13
|
-
type WorkflowRunResult,
|
|
14
11
|
type TaskCompletion,
|
|
15
12
|
type TaskExecutionPlan,
|
|
16
13
|
type TaskGraph,
|
|
@@ -185,23 +182,6 @@ async function resolveArtifactIdByName(listOperation: OperationName, baseRequest
|
|
|
185
182
|
}
|
|
186
183
|
}
|
|
187
184
|
|
|
188
|
-
/**
|
|
189
|
-
* Playbook `arguments` is intentionally untyped in this tool's schema (an array on create, a
|
|
190
|
-
* {name: value} map on invoke) -- unlike every other JSON-shaped field here, which has a concrete
|
|
191
|
-
* array/record schema the calling layer can serialize correctly. A genuinely schema-less field can
|
|
192
|
-
* arrive pre-serialized as JSON text instead of a parsed value; parse it back in place before it
|
|
193
|
-
* reaches the service, the same tolerance the CLI's own --arguments-json/--*-json flags already give.
|
|
194
|
-
*/
|
|
195
|
-
export function normalizeJsonEncodedField(params: Record<string, unknown>, key: string): void {
|
|
196
|
-
const value = params[key];
|
|
197
|
-
if (typeof value !== "string") return;
|
|
198
|
-
try {
|
|
199
|
-
params[key] = JSON.parse(value);
|
|
200
|
-
} catch {
|
|
201
|
-
throw new Error(`${key} must be valid JSON`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
185
|
/**
|
|
206
186
|
* Resolves every {nameKey -> idKey} pair present and not already satisfied by an explicit id, in
|
|
207
187
|
* place. `notes`, when given, collects a message for each name that only resolved by widening
|
|
@@ -540,200 +520,9 @@ export function registerTasksTool(pi: ExtensionAPI): void {
|
|
|
540
520
|
});
|
|
541
521
|
}
|
|
542
522
|
|
|
543
|
-
// notes.*, rules.*, docs.*, and the shared artifact.* are
|
|
544
|
-
// (see ../vehicle-notes-client.ts and @danypops/papyrus's
|
|
545
|
-
// not pi.registerTool()s in this file.
|
|
546
|
-
|
|
547
|
-
export function registerPlaybooksTool(pi: ExtensionAPI): void {
|
|
548
|
-
pi.registerTool({
|
|
549
|
-
name: "playbooks",
|
|
550
|
-
label: "Playbooks",
|
|
551
|
-
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, remove_subtree, 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; remove_subtree extends this to the whole nested-Playbook tree in one call. 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.",
|
|
552
|
-
parameters: Type.Object({
|
|
553
|
-
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
554
|
-
body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
|
|
555
|
-
tools: Type.Optional(Type.Array(Type.String())), labels: Type.Optional(Type.Array(Type.String())),
|
|
556
|
-
arguments: Type.Optional(Type.Unknown()),
|
|
557
|
-
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
558
|
-
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()),
|
|
559
|
-
parent_id: Type.Optional(Type.String()), parent_name: Type.Optional(Type.String()),
|
|
560
|
-
child_id: Type.Optional(Type.String()), child_name: Type.Optional(Type.String()),
|
|
561
|
-
dependency_id: Type.Optional(Type.String()), dependency_name: Type.Optional(Type.String()),
|
|
562
|
-
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
563
|
-
}),
|
|
564
|
-
renderCall(args, theme) { return renderPapyrusToolCall("Playbooks", args, theme); },
|
|
565
|
-
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
566
|
-
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
567
|
-
try {
|
|
568
|
-
const params: Record<string, unknown> = { ...rawParams };
|
|
569
|
-
const action = params.action;
|
|
570
|
-
// Name resolution must search regardless of project scope -- a Playbook itself is
|
|
571
|
-
// commonly unscoped (e.g. a cross-repo lab-deploy playbook), so resolutionRequest
|
|
572
|
-
// uses the caller's ORIGINAL project_root (undefined unless explicitly given), never
|
|
573
|
-
// the invoke-specific default applied below -- that default is only for where the
|
|
574
|
-
// resulting TASKS land, not for finding the playbook artifact itself.
|
|
575
|
-
const resolutionRequest = { project_root: params.project_root };
|
|
576
|
-
await resolveNameFields(params, [
|
|
577
|
-
{ nameKey: "name", idKey: "id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
578
|
-
{ nameKey: "parent_name", idKey: "parent_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
579
|
-
{ nameKey: "child_name", idKey: "child_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
580
|
-
{ nameKey: "dependency_name", idKey: "dependency_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
581
|
-
]);
|
|
582
|
-
// invoke ends by calling tasks.focus server-side -- that focus write must land in the
|
|
583
|
-
// SAME session scope the tasks tool reads from (ctx.sessionManager.getSessionId()),
|
|
584
|
-
// the same resolution the tasks tool itself always applies, or the entry task's focus
|
|
585
|
-
// is invisible to tasks(action=focused/active) despite invoke reporting it as focused.
|
|
586
|
-
// project_root defaults to ctx.cwd for the same reason: the tasks tool always scopes
|
|
587
|
-
// its OWN reads to ctx.cwd unless told otherwise, so an unscoped playbook-materialized
|
|
588
|
-
// task is invisible to tasks(action=focused) even with the right session -- confirmed
|
|
589
|
-
// live (the focus_set event existed with the correct sessionId, but Tasks.focused's own
|
|
590
|
-
// project-scope filter silently excluded the unscoped task from a cwd-scoped read).
|
|
591
|
-
// Applied AFTER name resolution: it must never affect finding the playbook itself.
|
|
592
|
-
if (action === "invoke") {
|
|
593
|
-
const resolvedSessionId = params.session_id ?? ctx.sessionManager.getSessionId();
|
|
594
|
-
Object.assign(params, {
|
|
595
|
-
project_root: params.project_root ?? ctx.cwd,
|
|
596
|
-
session_id: resolvedSessionId,
|
|
597
|
-
...sessionSecretField(resolvedSessionId as string),
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
if (action === "create" || action === "invoke" || action === "preview") normalizeJsonEncodedField(params, "arguments");
|
|
601
|
-
if (action === "create") {
|
|
602
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("playbooks.create", params);
|
|
603
|
-
return text(`Created playbook ${artifactLine(artifact)}`, createArtifactDetails("playbooks.create", artifact));
|
|
604
|
-
}
|
|
605
|
-
if (action === "list") {
|
|
606
|
-
const rows = await callService<Record<string, unknown>, Artifact[]>("playbooks.list", params);
|
|
607
|
-
return text(rows.length ? artifactLines(rows).join("\n") : "No playbooks found.", createArtifactListDetails("playbooks.list", rows));
|
|
608
|
-
}
|
|
609
|
-
if (action === "preview") {
|
|
610
|
-
const rendered = await callService<Record<string, unknown>, string>("playbooks.preview", params);
|
|
611
|
-
return text(rendered, createPreviewDetails("playbooks.preview", "Playbook preview", rendered));
|
|
612
|
-
}
|
|
613
|
-
if (action === "invoke") {
|
|
614
|
-
const invocation = await callService<Record<string, unknown>, PlaybookInvocationResult | PlaybookMissingArguments>("playbooks.invoke", params);
|
|
615
|
-
if ("missingArguments" in invocation) {
|
|
616
|
-
const message = `Missing required argument(s): ${invocation.missingArguments.join(", ")}. Nothing was created -- ask the human for these (discuss tool, live:true), then invoke again.`;
|
|
617
|
-
return text(message, createInvocationDetails("playbooks.invoke", invocation.playbookId, { tasks: [], docs: [], rules: [], roots: [] }));
|
|
618
|
-
}
|
|
619
|
-
const nodeTitleCounts = new Map<string, number>();
|
|
620
|
-
for (const node of invocation.execution.nodes) nodeTitleCounts.set(node.title, (nodeTitleCounts.get(node.title) ?? 0) + 1);
|
|
621
|
-
const execution = invocation.execution.nodes.map((node) => (nodeTitleCounts.get(node.title) ?? 0) > 1
|
|
622
|
-
? ` [${node.state}] ${node.title} (${node.id})`
|
|
623
|
-
: ` [${node.state}] ${node.title}`).join("\n");
|
|
624
|
-
const nodeById = new Map(invocation.execution.nodes.map((node) => [node.id, node]));
|
|
625
|
-
const rootLabels = invocation.rootTaskIds.map((id) => nodeById.get(id)?.title ?? "unknown task");
|
|
626
|
-
const entryLabel = nodeById.get(invocation.entryTaskId)?.title ?? invocation.entryTaskId;
|
|
627
|
-
const createdLabels = await artifactLabelsById([...invocation.created.docs, ...invocation.created.rules]);
|
|
628
|
-
return text([
|
|
629
|
-
`Invoked playbook run ${invocation.runId}: ${invocation.created.tasks.length} task(s), ${invocation.created.rules.length} rule(s), ${invocation.created.docs.length} doc(s) created.`,
|
|
630
|
-
`Entry task now focused: ${entryLabel}. Drive it forward with the tasks tool (start/submit/complete) -- contains/depends_on wiring auto-focuses each next step.`,
|
|
631
|
-
`Ready roots: ${rootLabels.join(", ") || "none"}.`,
|
|
632
|
-
`Context docs: ${invocation.created.docs.map((id) => createdLabels.get(id) ?? "unknown document").join(", ") || "none"}.`,
|
|
633
|
-
`Scoped rules: ${invocation.created.rules.map((id) => createdLabels.get(id) ?? "unknown rule").join(", ") || "none"}.`,
|
|
634
|
-
...(execution ? ["Execution:", execution] : []),
|
|
635
|
-
].join("\n"), createInvocationDetails("playbooks.invoke", invocation.runId, {
|
|
636
|
-
tasks: invocation.created.tasks,
|
|
637
|
-
docs: invocation.created.docs,
|
|
638
|
-
rules: invocation.created.rules,
|
|
639
|
-
roots: invocation.rootTaskIds,
|
|
640
|
-
}));
|
|
641
|
-
}
|
|
642
|
-
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
643
|
-
if (trashResult) return trashResult;
|
|
644
|
-
const operations = {
|
|
645
|
-
show: "playbooks.show", enable: "playbooks.enable", disable: "playbooks.disable", assign_project: "playbooks.assign_project", update: "playbooks.update",
|
|
646
|
-
contain: "playbooks.contain", uncontain: "playbooks.uncontain", depend: "playbooks.depend", undepend: "playbooks.undepend",
|
|
647
|
-
} as const;
|
|
648
|
-
const operation = operations[action as keyof typeof operations];
|
|
649
|
-
if (!operation) throw new Error(`unknown playbooks action: ${action}`);
|
|
650
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
651
|
-
return text(`${artifactLine(artifact)}${action === "show" ? `\n\n${artifact.body}` : ""}`, createArtifactDetails(operation, artifact));
|
|
652
|
-
} catch (error) {
|
|
653
|
-
throw new Error(`playbooks failed: ${error instanceof Error ? error.message : error}`);
|
|
654
|
-
}
|
|
655
|
-
},
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
export function registerSkillsTool(pi: ExtensionAPI): void {
|
|
660
|
-
pi.registerTool({
|
|
661
|
-
name: "skills",
|
|
662
|
-
label: "Skills",
|
|
663
|
-
description: "Papyrus Skill workflow and compatibility-template domain tool. Papyrus Skills are parameterized Task/Rule/Doc bundles, distinct from prompt-only skills. ACTIONS: create, create_template, list, show, invoke, run, enable, disable, instantiate, assign_project, update, remove, remove_subtree, restore. run validates arguments and atomically creates one scoped workflow run. project_root is optional at creation (omitted = unscoped) for create/create_template; assign_project reassigns it later, or unscopes when project_root is omitted. update changes title/body/labels (at least one required) and is refused for a read-only external projection. remove moves a Skill to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline; remove_subtree extends this to a whole `contains` subtree in one call. PREFER `name` (the skill's exact title) over `id`, and `template_name` over `template_id` for instantiate -- both are backend implementation details, resolved from name automatically.",
|
|
664
|
-
parameters: Type.Object({
|
|
665
|
-
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
666
|
-
body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
|
|
667
|
-
tools: Type.Optional(Type.Array(Type.String())), definition: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
668
|
-
arguments: Type.Optional(Type.Record(Type.String(), Type.Unknown())), run_id: Type.Optional(Type.String()),
|
|
669
|
-
labels: Type.Optional(Type.Array(Type.String())),
|
|
670
|
-
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
671
|
-
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), template_id: Type.Optional(Type.String()),
|
|
672
|
-
template_name: Type.Optional(Type.String()),
|
|
673
|
-
target_kind: Type.Optional(Type.String()), defaults: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
674
|
-
required: Type.Optional(Type.Array(Type.String())), kind: Type.Optional(Type.String()), subtype: Type.Optional(Type.String()),
|
|
675
|
-
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
676
|
-
}),
|
|
677
|
-
renderCall(args, theme) { return renderPapyrusToolCall("Skills", args, theme); },
|
|
678
|
-
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
679
|
-
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
680
|
-
try {
|
|
681
|
-
const params: Record<string, unknown> = { ...rawParams };
|
|
682
|
-
const action = params.action;
|
|
683
|
-
const request = { ...params, project_root: params.project_root ?? ctx.cwd };
|
|
684
|
-
await resolveNameFields(params, [
|
|
685
|
-
{ nameKey: "name", idKey: "id", listOperation: "skills.list", baseRequest: { project_root: params.project_root } },
|
|
686
|
-
{ nameKey: "template_name", idKey: "template_id", listOperation: "skills.list", baseRequest: { project_root: params.project_root } },
|
|
687
|
-
]);
|
|
688
|
-
if (action === "create" || action === "create_template") {
|
|
689
|
-
const operation = action === "create" ? "skills.create" : "skills.create_template";
|
|
690
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
691
|
-
return text(`Created skill ${artifactLine(artifact)}`, createArtifactDetails(operation, artifact));
|
|
692
|
-
}
|
|
693
|
-
if (action === "list") {
|
|
694
|
-
const rows = await callService<Record<string, unknown>, Artifact[]>("skills.list", params);
|
|
695
|
-
return text(rows.length ? artifactLines(rows).join("\n") : "No skills found.", createArtifactListDetails("skills.list", rows));
|
|
696
|
-
}
|
|
697
|
-
if (action === "invoke") {
|
|
698
|
-
const invocation = await callService<Record<string, unknown>, string>("skills.invoke", params);
|
|
699
|
-
return text(invocation, createPreviewDetails("skills.invoke", "Skill invocation", invocation));
|
|
700
|
-
}
|
|
701
|
-
if (action === "run") {
|
|
702
|
-
const run = await callService<Record<string, unknown>, WorkflowRunResult>("skills.run", request);
|
|
703
|
-
const runTitleCounts = new Map<string, number>();
|
|
704
|
-
for (const node of run.execution.nodes) runTitleCounts.set(node.title, (runTitleCounts.get(node.title) ?? 0) + 1);
|
|
705
|
-
const execution = run.execution.nodes.map((node) => (runTitleCounts.get(node.title) ?? 0) > 1
|
|
706
|
-
? ` [${node.state}] ${node.title} (${node.id})`
|
|
707
|
-
: ` [${node.state}] ${node.title}`).join("\n");
|
|
708
|
-
const nodeById = new Map(run.execution.nodes.map((node) => [node.id, node]));
|
|
709
|
-
const rootLabels = run.rootTaskIds.map((id) => nodeById.get(id)?.title ?? "unknown task");
|
|
710
|
-
const createdLabels = await artifactLabelsById([...run.created.docs, ...run.created.rules]);
|
|
711
|
-
return text([
|
|
712
|
-
`Created Skill run ${run.runId}: ${run.created.tasks.length} tasks, ${run.created.rules.length} rules, ${run.created.docs.length} docs.`,
|
|
713
|
-
`Ready roots: ${rootLabels.join(", ") || "none"}.`,
|
|
714
|
-
`Context docs: ${run.created.docs.map((id) => createdLabels.get(id) ?? "unknown document").join(", ") || "none"}.`,
|
|
715
|
-
`Scoped rules: ${run.created.rules.map((id) => createdLabels.get(id) ?? "unknown rule").join(", ") || "none"}.`,
|
|
716
|
-
...(execution ? ["Execution:", execution] : []),
|
|
717
|
-
].join("\n"), createInvocationDetails("skills.run", run.runId, {
|
|
718
|
-
tasks: run.created.tasks,
|
|
719
|
-
docs: run.created.docs,
|
|
720
|
-
rules: run.created.rules,
|
|
721
|
-
roots: run.rootTaskIds,
|
|
722
|
-
}));
|
|
723
|
-
}
|
|
724
|
-
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
725
|
-
if (trashResult) return trashResult;
|
|
726
|
-
const operations = { show: "skills.show", enable: "skills.enable", disable: "skills.disable", instantiate: "skills.instantiate", assign_project: "skills.assign_project", update: "skills.update" } as const;
|
|
727
|
-
const operation = operations[action as keyof typeof operations];
|
|
728
|
-
if (!operation) throw new Error(`unknown skills action: ${action}`);
|
|
729
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation, action === "instantiate" ? request : params);
|
|
730
|
-
return text(`${artifactLine(artifact)}${action === "show" ? `\n\n${artifact.body}` : ""}`, createArtifactDetails(operation, artifact));
|
|
731
|
-
} catch (error) {
|
|
732
|
-
throw new Error(`skills failed: ${error instanceof Error ? error.message : error}`);
|
|
733
|
-
}
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
}
|
|
523
|
+
// notes.*, rules.*, docs.*, skills.*, playbooks.*, and the shared artifact.* are
|
|
524
|
+
// registered as Vehicles (see ../vehicle-notes-client.ts and @danypops/papyrus's
|
|
525
|
+
// src/vehicle/papyrus-vehicle.ts), not pi.registerTool()s in this file.
|
|
737
526
|
|
|
738
527
|
export function registerDiscussTool(pi: ExtensionAPI): void {
|
|
739
528
|
pi.registerTool({
|
|
@@ -838,13 +627,11 @@ export function registerDiscussTool(pi: ExtensionAPI): void {
|
|
|
838
627
|
}
|
|
839
628
|
|
|
840
629
|
/** Thin orchestrator: each domain's tool is independently navigable/testable via its own registerXTool function. */
|
|
841
|
-
// docs and
|
|
842
|
-
// (registerNotesVehicle in vehicle-notes-client.ts, wired at session_start in
|
|
843
|
-
//
|
|
844
|
-
//
|
|
630
|
+
// notes, rules, docs, skills, and playbooks are no longer registered here -- all migrated onto
|
|
631
|
+
// Vehicle (registerNotesVehicle in vehicle-notes-client.ts, wired at session_start in index.ts),
|
|
632
|
+
// replacing their own pi.registerTool() mega-tools. See @danypops/papyrus's
|
|
633
|
+
// src/vehicle/papyrus-vehicle.ts for the server side.
|
|
845
634
|
export function registerDomainTools(pi: ExtensionAPI): void {
|
|
846
635
|
registerTasksTool(pi);
|
|
847
|
-
registerPlaybooksTool(pi);
|
|
848
|
-
registerSkillsTool(pi);
|
|
849
636
|
registerDiscussTool(pi);
|
|
850
637
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registers every Vehicle-projected domain (notes.*, rules.*, docs.*,
|
|
3
|
-
* as real Pi tools -- see @danypops/papyrus's
|
|
2
|
+
* Registers every Vehicle-projected domain (notes.*, rules.*, docs.*, skills.*,
|
|
3
|
+
* playbooks.*, artifact.*) as real Pi tools -- see @danypops/papyrus's
|
|
4
|
+
* src/vehicle/papyrus-vehicle.ts.
|
|
4
5
|
*
|
|
5
6
|
* Fails silently on a stale/unreachable daemon handle instead of aborting extension
|
|
6
7
|
* setup: Papyrus's daemon doesn't auto-spawn, and a tool that failed to register
|
|
@@ -14,8 +15,12 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
14
15
|
import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
|
|
15
16
|
import { registerVehicleTools } from "@danypops/vehicle-client-pi";
|
|
16
17
|
import { currentVehicleClientTarget } from "./service-client.ts";
|
|
18
|
+
import { sessionSecretField } from "./session-identity.ts";
|
|
17
19
|
|
|
18
|
-
const REGISTERED_PERMISSIONS = [
|
|
20
|
+
const REGISTERED_PERMISSIONS = [
|
|
21
|
+
"notes:read", "notes:write", "rules:read", "rules:write", "docs:read", "docs:write",
|
|
22
|
+
"skills:read", "skills:write", "playbooks:read", "playbooks:write", "artifact:read", "artifact:write",
|
|
23
|
+
];
|
|
19
24
|
|
|
20
25
|
export async function registerNotesVehicle(pi: ExtensionAPI): Promise<void> {
|
|
21
26
|
const target = currentVehicleClientTarget();
|
|
@@ -25,6 +30,25 @@ export async function registerNotesVehicle(pi: ExtensionAPI): Promise<void> {
|
|
|
25
30
|
await registerVehicleTools(pi, client, {
|
|
26
31
|
permissions: REGISTERED_PERMISSIONS,
|
|
27
32
|
principal: { id: "pi-papyrus" },
|
|
33
|
+
// playbooks.invoke's own module handler authorizes an internal Task Focus write via
|
|
34
|
+
// sessionIdentity.assertAuthorized(session_id, session_secret) -- see
|
|
35
|
+
// @danypops/papyrus's src/vehicle/playbooks-vehicle.ts. That secret must never be a
|
|
36
|
+
// model-visible input field (the model has no business knowing or supplying it), so
|
|
37
|
+
// it travels here instead, in principal.claims, from this extension's own already-
|
|
38
|
+
// cached secret (registered at session_start -- see index.ts) -- the same value
|
|
39
|
+
// sessionSecretField() used to thread through as a raw RPC input field before this
|
|
40
|
+
// operation moved onto Vehicle.
|
|
41
|
+
resolveInvocation: ({ descriptor, context }) => {
|
|
42
|
+
if (descriptor.name !== "playbooks.invoke") return {};
|
|
43
|
+
const sessionId = context.sessionManager.getSessionId();
|
|
44
|
+
const { session_secret: sessionSecret } = sessionSecretField(sessionId);
|
|
45
|
+
// Omit sessionSecret entirely when nothing is cached (unregistered session) --
|
|
46
|
+
// {sessionSecret: null} would fail the module's own optionalString(input,
|
|
47
|
+
// "session_secret") check (undefined-or-string, not null), a real regression from
|
|
48
|
+
// sessionSecretField()'s own {} (key omitted) return for the same case.
|
|
49
|
+
const claims: Record<string, string> = sessionSecret ? { sessionId, sessionSecret } : { sessionId };
|
|
50
|
+
return { principal: { id: "pi-papyrus", claims } };
|
|
51
|
+
},
|
|
28
52
|
});
|
|
29
53
|
} catch {
|
|
30
54
|
// Daemon state is stale/unreachable -- degrade silently, matching
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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/jittor": "^0.14.0",
|
|
21
|
-
"@danypops/papyrus": "^0.
|
|
21
|
+
"@danypops/papyrus": "^0.40.0",
|
|
22
22
|
"@danypops/vehicle-core": "^0.2.0",
|
|
23
23
|
"@danypops/vehicle-server": "^0.1.1",
|
|
24
24
|
"@danypops/vehicle-client": "^0.1.1",
|