@danypops/pi-papyrus 0.35.1 → 0.35.2
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 +1 -1
- package/extension/src/domain-tools.ts +13 -3
- package/extension/src/index.ts +18 -1
- package/extension/src/service-client.ts +39 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
|
@@ -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,
|
|
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.",
|
|
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())),
|
|
@@ -664,6 +664,9 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
|
|
|
664
664
|
arguments: Type.Optional(Type.Unknown()),
|
|
665
665
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
666
666
|
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()),
|
|
667
|
+
parent_id: Type.Optional(Type.String()), parent_name: Type.Optional(Type.String()),
|
|
668
|
+
child_id: Type.Optional(Type.String()), child_name: Type.Optional(Type.String()),
|
|
669
|
+
dependency_id: Type.Optional(Type.String()), dependency_name: Type.Optional(Type.String()),
|
|
667
670
|
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
668
671
|
}),
|
|
669
672
|
renderCall(args, theme) { return renderPapyrusToolCall("Playbooks", args, theme); },
|
|
@@ -672,8 +675,12 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
|
|
|
672
675
|
try {
|
|
673
676
|
const params: Record<string, unknown> = { ...rawParams };
|
|
674
677
|
const action = params.action;
|
|
678
|
+
const resolutionRequest = { project_root: params.project_root };
|
|
675
679
|
await resolveNameFields(params, [
|
|
676
|
-
{ nameKey: "name", idKey: "id", listOperation: "playbooks.list", baseRequest:
|
|
680
|
+
{ nameKey: "name", idKey: "id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
681
|
+
{ nameKey: "parent_name", idKey: "parent_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
682
|
+
{ nameKey: "child_name", idKey: "child_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
683
|
+
{ nameKey: "dependency_name", idKey: "dependency_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
|
|
677
684
|
]);
|
|
678
685
|
if (action === "create" || action === "invoke") normalizeJsonEncodedField(params, "arguments");
|
|
679
686
|
if (action === "create") {
|
|
@@ -690,7 +697,10 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
|
|
|
690
697
|
}
|
|
691
698
|
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
692
699
|
if (trashResult) return trashResult;
|
|
693
|
-
const operations = {
|
|
700
|
+
const operations = {
|
|
701
|
+
show: "playbooks.show", enable: "playbooks.enable", disable: "playbooks.disable", assign_project: "playbooks.assign_project", update: "playbooks.update",
|
|
702
|
+
contain: "playbooks.contain", uncontain: "playbooks.uncontain", depend: "playbooks.depend", undepend: "playbooks.undepend",
|
|
703
|
+
} as const;
|
|
694
704
|
const operation = operations[action as keyof typeof operations];
|
|
695
705
|
if (!operation) throw new Error(`unknown playbooks action: ${action}`);
|
|
696
706
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
package/extension/src/index.ts
CHANGED
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
type TaskStatus,
|
|
26
26
|
} from "@danypops/papyrus";
|
|
27
27
|
import { formatMetadata } from "./artifact-format.ts";
|
|
28
|
-
import { callService } from "./service-client.ts";
|
|
28
|
+
import { callService, subscribeTaskPushChannel } from "./service-client.ts";
|
|
29
|
+
import type { PushChannelClient } from "@danypops/daemon-kit/pi-client";
|
|
29
30
|
import { registerDomainTools, resolveNameFields } from "./domain-tools.ts";
|
|
30
31
|
import { BoundedPoll } from "./bounded-poll.ts";
|
|
31
32
|
import { renderNoteWidgetLines } from "./note-widget.ts";
|
|
@@ -109,6 +110,7 @@ export class TaskOverlay {
|
|
|
109
110
|
private projectRoot: string | undefined;
|
|
110
111
|
private sessionId: string | undefined;
|
|
111
112
|
private readonly poll = new BoundedPoll();
|
|
113
|
+
private pushChannel: PushChannelClient | undefined;
|
|
112
114
|
|
|
113
115
|
setUI(ctx: ExtensionUIContext): void {
|
|
114
116
|
if (ctx !== this.uiCtx) {
|
|
@@ -141,6 +143,19 @@ export class TaskOverlay {
|
|
|
141
143
|
} catch {
|
|
142
144
|
// A rendering bug must not crash the extension host over a best-effort status widget.
|
|
143
145
|
}
|
|
146
|
+
this.ensurePushChannel();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Lazily (re)establishes the push subscription -- a no-op once already connected.
|
|
151
|
+
* Retried on every poll-driven refresh() call rather than once at startup: the
|
|
152
|
+
* daemon may not have been running yet when this session started (subscribeTaskPushChannel
|
|
153
|
+
* returns undefined with no token/port on disk), and this piggybacks on the existing
|
|
154
|
+
* poll cadence as the natural retry point instead of a second timer.
|
|
155
|
+
*/
|
|
156
|
+
private ensurePushChannel(): void {
|
|
157
|
+
if (this.pushChannel && this.pushChannel.state() !== "closed") return;
|
|
158
|
+
this.pushChannel = subscribeTaskPushChannel(() => { void this.refresh(); });
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
private render(): void {
|
|
@@ -196,6 +211,8 @@ export class TaskOverlay {
|
|
|
196
211
|
|
|
197
212
|
dispose(): void {
|
|
198
213
|
this.stopPolling();
|
|
214
|
+
this.pushChannel?.close();
|
|
215
|
+
this.pushChannel = undefined;
|
|
199
216
|
this.uiCtx?.setWidget(WIDGET_KEY, undefined);
|
|
200
217
|
this.registered = false;
|
|
201
218
|
this.tui = undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createRetryingClient, type RetryingClient } from "@danypops/daemon-kit/pi-client";
|
|
2
|
-
import { connectPapyrusClient, type OperationName, type PapyrusClient } from "@danypops/papyrus";
|
|
1
|
+
import { connectPushChannel, createRetryingClient, type PushChannelClient, type PushChannelState, type RetryingClient } from "@danypops/daemon-kit/pi-client";
|
|
2
|
+
import { connectPapyrusClient, resolvePushChannelTarget, type OperationName, type PapyrusClient } from "@danypops/papyrus";
|
|
3
3
|
|
|
4
4
|
type ClientConnector = () => Promise<PapyrusClient>;
|
|
5
5
|
|
|
@@ -26,3 +26,40 @@ export function resetPapyrusClientForTests(): void {
|
|
|
26
26
|
connector = () => connectPapyrusClient();
|
|
27
27
|
client.reset();
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
let pushChannelTargetResolver: typeof resolvePushChannelTarget = resolvePushChannelTarget;
|
|
31
|
+
|
|
32
|
+
export function setPushChannelTargetResolverForTests(value: typeof resolvePushChannelTarget): void {
|
|
33
|
+
pushChannelTargetResolver = value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resetPushChannelTargetResolverForTests(): void {
|
|
37
|
+
pushChannelTargetResolver = resolvePushChannelTarget;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Subscribes to the daemon's "tasks" push topic so a widget can refresh the moment
|
|
42
|
+
* a mutation happens, instead of waiting for its next poll tick. Returns undefined
|
|
43
|
+
* (no-op) rather than throwing when the daemon has never started -- no token/port
|
|
44
|
+
* on disk yet -- matching how the widget's own fetch-based refresh() already
|
|
45
|
+
* tolerates "daemon not running" and falls back to its existing poll. A caller
|
|
46
|
+
* should retry this on a later poll tick once the daemon is confirmed reachable.
|
|
47
|
+
*/
|
|
48
|
+
export function subscribeTaskPushChannel(onMessage: () => void, onStateChange?: (state: PushChannelState) => void): PushChannelClient | undefined {
|
|
49
|
+
const target = pushChannelTargetResolver();
|
|
50
|
+
if (!target) return undefined;
|
|
51
|
+
return connectPushChannel({
|
|
52
|
+
url: () => {
|
|
53
|
+
// Re-resolved on every reconnect attempt: the daemon rebinds a new random
|
|
54
|
+
// port on every restart, exactly the problem connectWithPolicy solves for
|
|
55
|
+
// one-shot RPC by re-reading the handle file each time.
|
|
56
|
+
const resolved = pushChannelTargetResolver();
|
|
57
|
+
if (!resolved) throw new Error("Papyrus daemon is not running");
|
|
58
|
+
return resolved.url;
|
|
59
|
+
},
|
|
60
|
+
token: target.token,
|
|
61
|
+
topics: ["tasks"],
|
|
62
|
+
onMessage: (topic) => { if (topic === "tasks") onMessage(); },
|
|
63
|
+
onStateChange,
|
|
64
|
+
});
|
|
65
|
+
}
|
package/package.json
CHANGED