@danypops/pi-papyrus 0.38.4 → 0.39.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.
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type DiscussionRound,
|
|
9
9
|
type GateResult,
|
|
10
10
|
type OperationName,
|
|
11
|
+
type PlaybookInvocationResult,
|
|
12
|
+
type PlaybookMissingArguments,
|
|
11
13
|
type WorkflowRunResult,
|
|
12
14
|
type TaskCompletion,
|
|
13
15
|
type TaskExecutionPlan,
|
|
@@ -138,16 +140,49 @@ export function matchArtifactByName(candidates: Artifact[], name: string): strin
|
|
|
138
140
|
return matches[0]!.id;
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
/**
|
|
144
|
+
* tasks.list is the one list operation that requires `project_root` and separately supports a
|
|
145
|
+
* `scope` ("project" | "graph" | "all") to widen or narrow the search. Every other list operation
|
|
146
|
+
* (docs.list, rules.list, skills.list, playbooks.list, artifact.query, ...) instead treats an
|
|
147
|
+
* omitted `project_root` as an unscoped/global search (domain-services.ts's listScoped) and has
|
|
148
|
+
* no `scope` concept at all -- so "search everywhere" means something different for each.
|
|
149
|
+
*/
|
|
150
|
+
const SCOPE_AWARE_LIST_OPERATIONS = new Set<OperationName>(["tasks.list"]);
|
|
151
|
+
|
|
152
|
+
/** The widened-scope request tried once when a name isn't found under the caller's current scope. */
|
|
153
|
+
function widenedRequest(listOperation: OperationName, baseRequest: Record<string, unknown>): Record<string, unknown> {
|
|
154
|
+
return SCOPE_AWARE_LIST_OPERATIONS.has(listOperation)
|
|
155
|
+
? { ...baseRequest, scope: "all" }
|
|
156
|
+
: { ...baseRequest, project_root: undefined };
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
/**
|
|
142
160
|
* Resolves a name to its id via `listOperation` (whichever kind's list call is the right search
|
|
143
161
|
* scope -- tasks.list, docs.list, rules.list, skills.list, notes.list, discuss.list, or the
|
|
144
162
|
* kind-agnostic artifact.query for a cross-kind reference like a link target). `baseRequest`
|
|
145
163
|
* should mirror whatever scoping (project_root, etc.) that operation's own "list" action already
|
|
146
164
|
* uses, so resolution never searches a wider or narrower scope than a plain list call would.
|
|
165
|
+
*
|
|
166
|
+
* A two-artifact action (depend/contain/gate/link) routinely names artifacts that live in two
|
|
167
|
+
* different projects, and one call has no way to give two different name fields two different
|
|
168
|
+
* scopes. When the first lookup finds nothing under the caller's current scope, retry exactly
|
|
169
|
+
* once against a global search before giving up -- but never when the caller already pinned an
|
|
170
|
+
* explicit `scope`, so a genuine "not found in the scope I asked for" stays a real error instead
|
|
171
|
+
* of being silently papered over. `notes`, when given, records that a name only resolved after
|
|
172
|
+
* widening, so the caller can surface that a search went wider than the caller's default scope
|
|
173
|
+
* rather than resolving silently.
|
|
147
174
|
*/
|
|
148
|
-
async function resolveArtifactIdByName(listOperation: OperationName, baseRequest: Record<string, unknown>, name: string): Promise<string> {
|
|
175
|
+
async function resolveArtifactIdByName(listOperation: OperationName, baseRequest: Record<string, unknown>, name: string, notes?: string[]): Promise<string> {
|
|
149
176
|
const candidates = await callService<Record<string, unknown>, Artifact[]>(listOperation, { ...baseRequest, text: name });
|
|
150
|
-
|
|
177
|
+
try {
|
|
178
|
+
return matchArtifactByName(candidates, name);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (!(error instanceof Error) || !error.message.startsWith("no artifact named") || baseRequest["scope"] !== undefined) throw error;
|
|
181
|
+
const widenedCandidates = await callService<Record<string, unknown>, Artifact[]>(listOperation, { ...widenedRequest(listOperation, baseRequest), text: name });
|
|
182
|
+
const id = matchArtifactByName(widenedCandidates, name);
|
|
183
|
+
notes?.push(`"${name}" was not found in the current project scope; resolved across all projects instead.`);
|
|
184
|
+
return id;
|
|
185
|
+
}
|
|
151
186
|
}
|
|
152
187
|
|
|
153
188
|
/**
|
|
@@ -167,15 +202,21 @@ export function normalizeJsonEncodedField(params: Record<string, unknown>, key:
|
|
|
167
202
|
}
|
|
168
203
|
}
|
|
169
204
|
|
|
170
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Resolves every {nameKey -> idKey} pair present and not already satisfied by an explicit id, in
|
|
207
|
+
* place. `notes`, when given, collects a message for each name that only resolved by widening
|
|
208
|
+
* past the caller's own scope (see resolveArtifactIdByName) -- callers that want that surfaced
|
|
209
|
+
* to the model/human pass an array here and append it to their own response text.
|
|
210
|
+
*/
|
|
171
211
|
export async function resolveNameFields(
|
|
172
212
|
params: Record<string, unknown>,
|
|
173
213
|
fields: ReadonlyArray<{ nameKey: string; idKey: string; listOperation: OperationName; baseRequest: Record<string, unknown> }>,
|
|
214
|
+
notes?: string[],
|
|
174
215
|
): Promise<void> {
|
|
175
216
|
for (const { nameKey, idKey, listOperation, baseRequest } of fields) {
|
|
176
217
|
const nameValue = params[nameKey];
|
|
177
218
|
if (typeof nameValue === "string" && nameValue.length > 0 && !params[idKey]) {
|
|
178
|
-
params[idKey] = await resolveArtifactIdByName(listOperation, baseRequest, nameValue);
|
|
219
|
+
params[idKey] = await resolveArtifactIdByName(listOperation, baseRequest, nameValue, notes);
|
|
179
220
|
}
|
|
180
221
|
}
|
|
181
222
|
}
|
|
@@ -187,10 +228,11 @@ async function resolveNameArrayField(
|
|
|
187
228
|
idsKey: string,
|
|
188
229
|
listOperation: OperationName,
|
|
189
230
|
baseRequest: Record<string, unknown>,
|
|
231
|
+
notes?: string[],
|
|
190
232
|
): Promise<void> {
|
|
191
233
|
const names = params[namesKey];
|
|
192
234
|
if (Array.isArray(names) && names.length > 0 && !params[idsKey]) {
|
|
193
|
-
params[idsKey] = await Promise.all(names.map((entry) => resolveArtifactIdByName(listOperation, baseRequest, String(entry))));
|
|
235
|
+
params[idsKey] = await Promise.all(names.map((entry) => resolveArtifactIdByName(listOperation, baseRequest, String(entry), notes)));
|
|
194
236
|
}
|
|
195
237
|
}
|
|
196
238
|
|
|
@@ -248,7 +290,7 @@ export function registerTasksTool(pi: ExtensionAPI): void {
|
|
|
248
290
|
pi.registerTool({
|
|
249
291
|
name: "tasks",
|
|
250
292
|
label: "Tasks",
|
|
251
|
-
description: "Task domain tool. ACTIONS: create, update, list, show, history, context, scope, set_scope, assign_project, graph, plan, active, focused, focus, pause, unpause, clear_focus, start, submit, complete, reject, retry, cancel, cancel_subtree, run_gates, set_checklist, set_gates, depend, undepend, contain, uncontain, remove, remove_subtree, restore, claim, heartbeat_lease, release_lease, lease, event_feed. Lifecycle: todo → in-progress → review → done; review failure → rejected → retry → in-progress; canceled is terminal. Focus and lease are independent of lifecycle and of each other -- multiple sessions can focus the same task while only one holds its lease (claim throws if a different owner already holds one; release/heartbeat need the exact token claim returned; owner defaults to this session's id). context returns the full plan (the system prompt itself only carries a one-line pointer) -- call it explicitly after a compaction or before reconciling. complete runs gates + checklist-proof review, then focuses one ready successor. cancel_subtree cancels a task and its whole containment subtree in one call, skipping tasks already done/canceled. remove/restore use a time-gated trash (refuses the live Focus); remove_subtree trashes a whole `contains` subtree in one call; undepend/uncontain are idempotent no-ops when the edge is already absent. update recovers an accidentally-terminal task via status=todo + reason, without rewriting real history; update never touches gates (title/body/labels/status only) -- use set_gates to replace a task's gate commands after creation. Prefer `name` (exact title) over `id` -- id is a backend detail, resolved automatically, needed only to disambiguate a shared title; `
|
|
293
|
+
description: "Task domain tool. ACTIONS: create, update, list, show, history, context, scope, set_scope, assign_project, graph, plan, active, focused, focus, pause, unpause, clear_focus, start, submit, complete, reject, retry, cancel, cancel_subtree, run_gates, set_checklist, set_gates, depend, undepend, contain, uncontain, remove, remove_subtree, restore, claim, heartbeat_lease, release_lease, lease, event_feed. Lifecycle: todo → in-progress → review → done; review failure → rejected → retry → in-progress; canceled is terminal. Focus and lease are independent of lifecycle and of each other -- multiple sessions can focus the same task while only one holds its lease (claim throws if a different owner already holds one; release/heartbeat need the exact token claim returned; owner defaults to this session's id). context returns the full plan (the system prompt itself only carries a one-line pointer) -- call it explicitly after a compaction or before reconciling. complete runs gates + checklist-proof review, then focuses one ready successor. cancel_subtree cancels a task and its whole containment subtree in one call, skipping tasks already done/canceled. remove/restore use a time-gated trash (refuses the live Focus); remove_subtree trashes a whole `contains` subtree in one call; undepend/uncontain are idempotent no-ops when the edge is already absent. update recovers an accidentally-terminal task via status=todo + reason, without rewriting real history; update never touches gates (title/body/labels/status only) -- use set_gates to replace a task's gate commands after creation. Prefer `name` (exact title) over `id` -- id is a backend detail, resolved automatically, needed only to disambiguate a shared title; `parent_name`/`child_name`/`root_task_name` are the same pattern for their `_id` counterparts. For a prerequisite, use `dependency_name` (singular, resolved to `dependency_id`) with the `depend`/`undepend` actions; `depends_on_names` (plural array, resolved to `depends_on`) is only for `create`'s initial dependency set -- passing the wrong one of the two to `depend` leaves `dependency_id` unset and fails with a `dependency_id is required` error. A name resolved outside this call's own project scope (e.g. depending on a task in a different project) is retried once against every project before failing, and the response notes when that happened.",
|
|
252
294
|
parameters: Type.Object({
|
|
253
295
|
action: Type.String(),
|
|
254
296
|
id: Type.Optional(Type.String()),
|
|
@@ -299,12 +341,17 @@ export function registerTasksTool(pi: ExtensionAPI): void {
|
|
|
299
341
|
// ever holds this extension's own registered session anyway (see session-identity.ts).
|
|
300
342
|
const resolvedSessionId = params.session_id ?? ctx.sessionManager.getSessionId();
|
|
301
343
|
const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "pi-tool", session_id: resolvedSessionId, ...sessionSecretField(resolvedSessionId as string) };
|
|
344
|
+
// Collects a note whenever a name field below only resolved by widening past this call's
|
|
345
|
+
// own project scope (see resolveArtifactIdByName) -- surfaced at the end of this action's
|
|
346
|
+
// own response text rather than resolved silently, since a cross-project depend/contain
|
|
347
|
+
// is exactly the case a shared per-call scope can't otherwise express.
|
|
348
|
+
const notes: string[] = [];
|
|
302
349
|
// Resolve the graph root first: every other name lookup must use the caller's final
|
|
303
350
|
// project/scope/root selection, otherwise `scope: all|graph` silently collapses back
|
|
304
351
|
// to the current project and forces callers to reach for an id.
|
|
305
352
|
await resolveNameFields(params, [
|
|
306
353
|
{ nameKey: "root_task_name", idKey: "root_task_id", listOperation: "tasks.list", baseRequest: { ...baseRequest, scope: "project" } },
|
|
307
|
-
]);
|
|
354
|
+
], notes);
|
|
308
355
|
const resolutionRequest = {
|
|
309
356
|
...baseRequest,
|
|
310
357
|
...(params.scope === undefined ? {} : { scope: params.scope }),
|
|
@@ -317,9 +364,10 @@ export function registerTasksTool(pi: ExtensionAPI): void {
|
|
|
317
364
|
{ nameKey: "dependency_name", idKey: "dependency_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
|
|
318
365
|
{ nameKey: "parent_name", idKey: "parent_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
|
|
319
366
|
{ nameKey: "child_name", idKey: "child_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
|
|
320
|
-
]);
|
|
321
|
-
await resolveNameArrayField(params, "depends_on_names", "depends_on", "tasks.list", resolutionRequest);
|
|
367
|
+
], notes);
|
|
368
|
+
await resolveNameArrayField(params, "depends_on_names", "depends_on", "tasks.list", resolutionRequest, notes);
|
|
322
369
|
const request = { ...params, ...baseRequest };
|
|
370
|
+
const result = await (async (): Promise<ReturnType<typeof text>> => {
|
|
323
371
|
if (action === "create") {
|
|
324
372
|
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create", request);
|
|
325
373
|
return text(`Created task ${artifactLine(artifact)}`, createArtifactDetails("tasks.create", artifact));
|
|
@@ -482,6 +530,9 @@ export function registerTasksTool(pi: ExtensionAPI): void {
|
|
|
482
530
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
|
|
483
531
|
if (operation === "tasks.focus") emitTaskFocusEvent({ taskId: artifact.id, sessionId: request.session_id as string, status: "focused" });
|
|
484
532
|
return text(artifactLine(artifact), createArtifactDetails(operation, artifact));
|
|
533
|
+
})();
|
|
534
|
+
if (notes.length > 0 && result.content[0]?.type === "text") result.content[0].text += `\n\n${notes.join("\n")}`;
|
|
535
|
+
return result;
|
|
485
536
|
} catch (error) {
|
|
486
537
|
throw new Error(`tasks failed: ${error instanceof Error ? error.message : error}`);
|
|
487
538
|
}
|
|
@@ -489,121 +540,9 @@ export function registerTasksTool(pi: ExtensionAPI): void {
|
|
|
489
540
|
});
|
|
490
541
|
}
|
|
491
542
|
|
|
492
|
-
// notes
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
export function registerDocsTool(pi: ExtensionAPI): void {
|
|
497
|
-
pi.registerTool({
|
|
498
|
-
name: "docs",
|
|
499
|
-
label: "Documents",
|
|
500
|
-
description: "Document domain tool. ACTIONS: create, list, show, activate, archive, reopen, link, assign_project, update, remove, remove_subtree, restore. project_root is optional at creation (omitted = unscoped); 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 (e.g. web-spider-ingested Docs) -- capture a correction as a new linked Doc instead. remove moves a Doc 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 doc's exact title) over `id`, and `target_name` over `target_id` for link -- both are backend implementation details, resolved from name automatically (target_name searches across every kind, since a link target can be a doc, task, rule, or skill). Prefer this over low-level papyrus_* tools for document work.",
|
|
501
|
-
parameters: Type.Object({
|
|
502
|
-
action: Type.String(),
|
|
503
|
-
id: Type.Optional(Type.String()),
|
|
504
|
-
name: Type.Optional(Type.String()),
|
|
505
|
-
title: Type.Optional(Type.String()),
|
|
506
|
-
body: Type.Optional(Type.String()),
|
|
507
|
-
subtype: Type.Optional(Type.String()),
|
|
508
|
-
status: Type.Optional(Type.String()),
|
|
509
|
-
text: Type.Optional(Type.String()),
|
|
510
|
-
limit: Type.Optional(Type.Number()),
|
|
511
|
-
labels: Type.Optional(Type.Array(Type.String())),
|
|
512
|
-
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
513
|
-
template_id: Type.Optional(Type.String()),
|
|
514
|
-
relation: Type.Optional(Type.String()),
|
|
515
|
-
target_id: Type.Optional(Type.String()),
|
|
516
|
-
target_name: Type.Optional(Type.String()),
|
|
517
|
-
project_root: Type.Optional(Type.String()),
|
|
518
|
-
reason: Type.Optional(Type.String()),
|
|
519
|
-
}),
|
|
520
|
-
renderCall(args, theme) { return renderPapyrusToolCall("Documents", args, theme); },
|
|
521
|
-
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
522
|
-
async execute(_id, rawParams) {
|
|
523
|
-
try {
|
|
524
|
-
const params: Record<string, unknown> = { ...rawParams };
|
|
525
|
-
const action = params.action;
|
|
526
|
-
const scopeRequest = { project_root: params.project_root };
|
|
527
|
-
await resolveNameFields(params, [
|
|
528
|
-
{ nameKey: "name", idKey: "id", listOperation: "docs.list", baseRequest: scopeRequest },
|
|
529
|
-
// Kind-agnostic: a link target can be a doc, task, rule, or skill, so this searches every kind rather than only docs.
|
|
530
|
-
{ nameKey: "target_name", idKey: "target_id", listOperation: "artifact.query", baseRequest: scopeRequest },
|
|
531
|
-
]);
|
|
532
|
-
if (action === "create") {
|
|
533
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("docs.create", params);
|
|
534
|
-
return text(`Created document ${artifactLine(artifact)}`, createArtifactDetails("docs.create", artifact));
|
|
535
|
-
}
|
|
536
|
-
if (action === "list") {
|
|
537
|
-
const rows = await callService<Record<string, unknown>, Artifact[]>("docs.list", params);
|
|
538
|
-
return text(rows.length ? artifactLines(rows).join("\n") : "No documents found.", createArtifactListDetails("docs.list", rows));
|
|
539
|
-
}
|
|
540
|
-
if (action === "show") {
|
|
541
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("docs.show", params);
|
|
542
|
-
return text(`${artifactLine(artifact)}\n\n${artifact.body}`, createArtifactDetails("docs.show", artifact));
|
|
543
|
-
}
|
|
544
|
-
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
545
|
-
if (trashResult) return trashResult;
|
|
546
|
-
const operations = { activate: "docs.activate", archive: "docs.archive", reopen: "docs.reopen", link: "docs.link", assign_project: "docs.assign_project", update: "docs.update" } as const;
|
|
547
|
-
const operation = operations[action as keyof typeof operations];
|
|
548
|
-
if (!operation) throw new Error(`unknown docs action: ${action}`);
|
|
549
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
550
|
-
return text(artifactLine(artifact), createArtifactDetails(operation, artifact));
|
|
551
|
-
} catch (error) {
|
|
552
|
-
throw new Error(`docs failed: ${error instanceof Error ? error.message : error}`);
|
|
553
|
-
}
|
|
554
|
-
},
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function registerRulesTool(pi: ExtensionAPI): void {
|
|
559
|
-
pi.registerTool({
|
|
560
|
-
name: "rules",
|
|
561
|
-
label: "Rules",
|
|
562
|
-
description: "Rule domain tool. ACTIONS: create, list, show, preview, enable, disable, gate, assign_project, update, remove, remove_subtree, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. Active rules inject into the agent system prompt. update changes title/body/labels (at least one required); body updates still enforce the same combined condition+action+body context-tax bound as creation, and are refused for a read-only external projection. remove moves a Rule 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 rule's exact title) over `id`, and `task_name` over `task_id` for gate -- both are backend implementation details, resolved from name automatically.",
|
|
563
|
-
parameters: Type.Object({
|
|
564
|
-
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
565
|
-
body: Type.Optional(Type.String()), condition: Type.Optional(Type.String()), rule_action: Type.Optional(Type.String()),
|
|
566
|
-
severity: Type.Optional(Type.String()), labels: Type.Optional(Type.Array(Type.String())),
|
|
567
|
-
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
568
|
-
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), task_id: Type.Optional(Type.String()),
|
|
569
|
-
task_name: Type.Optional(Type.String()),
|
|
570
|
-
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
571
|
-
}),
|
|
572
|
-
renderCall(args, theme) { return renderPapyrusToolCall("Rules", args, theme); },
|
|
573
|
-
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
574
|
-
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
575
|
-
try {
|
|
576
|
-
const params: Record<string, unknown> = { ...rawParams };
|
|
577
|
-
const action = params.action;
|
|
578
|
-
await resolveNameFields(params, [
|
|
579
|
-
{ nameKey: "name", idKey: "id", listOperation: "rules.list", baseRequest: { project_root: params.project_root } },
|
|
580
|
-
{ nameKey: "task_name", idKey: "task_id", listOperation: "tasks.list", baseRequest: { project_root: params.project_root ?? ctx.cwd } },
|
|
581
|
-
]);
|
|
582
|
-
if (action === "create") {
|
|
583
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("rules.create", params);
|
|
584
|
-
return text(`Created rule ${artifactLine(artifact)}`, createArtifactDetails("rules.create", artifact));
|
|
585
|
-
}
|
|
586
|
-
if (action === "list") {
|
|
587
|
-
const rows = await callService<Record<string, unknown>, Artifact[]>("rules.list", params);
|
|
588
|
-
return text(rows.length ? artifactLines(rows).join("\n") : "No rules found.", createArtifactListDetails("rules.list", rows));
|
|
589
|
-
}
|
|
590
|
-
if (action === "preview") {
|
|
591
|
-
const preview = await callService<Record<string, unknown>, string>("rules.preview", params);
|
|
592
|
-
return text(preview, createPreviewDetails("rules.preview", "Rule preview", preview));
|
|
593
|
-
}
|
|
594
|
-
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
595
|
-
if (trashResult) return trashResult;
|
|
596
|
-
const operations = { show: "rules.show", enable: "rules.enable", disable: "rules.disable", gate: "rules.gate", assign_project: "rules.assign_project", update: "rules.update" } as const;
|
|
597
|
-
const operation = operations[action as keyof typeof operations];
|
|
598
|
-
if (!operation) throw new Error(`unknown rules action: ${action}`);
|
|
599
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
600
|
-
return text(`${artifactLine(artifact)}${action === "show" ? `\n\n${artifact.body}` : ""}`, createArtifactDetails(operation, artifact));
|
|
601
|
-
} catch (error) {
|
|
602
|
-
throw new Error(`rules failed: ${error instanceof Error ? error.message : error}`);
|
|
603
|
-
}
|
|
604
|
-
},
|
|
605
|
-
});
|
|
606
|
-
}
|
|
543
|
+
// notes.*, rules.*, docs.*, and the shared artifact.* are registered as Vehicles
|
|
544
|
+
// (see ../vehicle-notes-client.ts and @danypops/papyrus's src/vehicle/papyrus-vehicle.ts),
|
|
545
|
+
// not pi.registerTool()s in this file.
|
|
607
546
|
|
|
608
547
|
export function registerPlaybooksTool(pi: ExtensionAPI): void {
|
|
609
548
|
pi.registerTool({
|
|
@@ -672,13 +611,33 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
|
|
|
672
611
|
return text(rendered, createPreviewDetails("playbooks.preview", "Playbook preview", rendered));
|
|
673
612
|
}
|
|
674
613
|
if (action === "invoke") {
|
|
675
|
-
const invocation = await callService<Record<string, unknown>,
|
|
676
|
-
if (invocation
|
|
614
|
+
const invocation = await callService<Record<string, unknown>, PlaybookInvocationResult | PlaybookMissingArguments>("playbooks.invoke", params);
|
|
615
|
+
if ("missingArguments" in invocation) {
|
|
677
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.`;
|
|
678
|
-
return text(message,
|
|
617
|
+
return text(message, createInvocationDetails("playbooks.invoke", invocation.playbookId, { tasks: [], docs: [], rules: [], roots: [] }));
|
|
679
618
|
}
|
|
680
|
-
const
|
|
681
|
-
|
|
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
|
+
}));
|
|
682
641
|
}
|
|
683
642
|
const trashResult = await handleArtifactRemoveRestore(action, params);
|
|
684
643
|
if (trashResult) return trashResult;
|
|
@@ -879,10 +838,12 @@ export function registerDiscussTool(pi: ExtensionAPI): void {
|
|
|
879
838
|
}
|
|
880
839
|
|
|
881
840
|
/** Thin orchestrator: each domain's tool is independently navigable/testable via its own registerXTool function. */
|
|
841
|
+
// docs and rules are no longer registered here -- both migrated onto Vehicle
|
|
842
|
+
// (registerNotesVehicle in vehicle-notes-client.ts, wired at session_start in
|
|
843
|
+
// index.ts), replacing their own pi.registerTool() mega-tools. See
|
|
844
|
+
// @danypops/papyrus's src/vehicle/papyrus-vehicle.ts for the server side.
|
|
882
845
|
export function registerDomainTools(pi: ExtensionAPI): void {
|
|
883
846
|
registerTasksTool(pi);
|
|
884
|
-
registerDocsTool(pi);
|
|
885
|
-
registerRulesTool(pi);
|
|
886
847
|
registerPlaybooksTool(pi);
|
|
887
848
|
registerSkillsTool(pi);
|
|
888
849
|
registerDiscussTool(pi);
|
|
@@ -1,39 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registers
|
|
3
|
-
*
|
|
4
|
-
* VehicleRegistry side. Same daemon, same handle file, same Bearer token every
|
|
5
|
-
* other Papyrus RPC call already uses (resolveVehicleClientTarget mirrors
|
|
6
|
-
* resolvePushChannelTarget's own resolution).
|
|
2
|
+
* Registers every Vehicle-projected domain (notes.*, rules.*, docs.*, artifact.*)
|
|
3
|
+
* as real Pi tools -- see @danypops/papyrus's src/vehicle/papyrus-vehicle.ts.
|
|
7
4
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* stale handle) is tolerated the same silent-degrade way
|
|
12
|
-
* subscribeTaskPushChannel already tolerates it, rather than letting a
|
|
13
|
-
* daemon-not-running condition abort the rest of extension setup. There is
|
|
14
|
-
* no retry-on-later-connect for a tool that was never registered at all --
|
|
15
|
-
* Pi has no way to add one after the fact outside the initial registration
|
|
16
|
-
* flow.
|
|
5
|
+
* Fails silently on a stale/unreachable daemon handle instead of aborting extension
|
|
6
|
+
* setup: Papyrus's daemon doesn't auto-spawn, and a tool that failed to register
|
|
7
|
+
* here has no later retry path.
|
|
17
8
|
*
|
|
18
|
-
*
|
|
19
|
-
* (
|
|
20
|
-
*
|
|
21
|
-
* the bare extension entrypoint on every registerPapyrus(api) call, so an
|
|
22
|
-
* un-injected default would resolve the real daemonStateDir() in any test
|
|
23
|
-
* exercising the full entrypoint, not just ones about notes.
|
|
9
|
+
* Uses service-client.ts's currentVehicleClientTarget() (test-injectable) rather
|
|
10
|
+
* than resolveVehicleClientTarget() directly, so a test exercising the full
|
|
11
|
+
* extension entrypoint doesn't resolve a real daemonStateDir().
|
|
24
12
|
*/
|
|
25
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
14
|
import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
|
|
27
15
|
import { registerVehicleTools } from "@danypops/vehicle-client-pi";
|
|
28
16
|
import { currentVehicleClientTarget } from "./service-client.ts";
|
|
29
17
|
|
|
18
|
+
const REGISTERED_PERMISSIONS = ["notes:read", "notes:write", "rules:read", "rules:write", "docs:read", "docs:write", "artifact:read", "artifact:write"];
|
|
19
|
+
|
|
30
20
|
export async function registerNotesVehicle(pi: ExtensionAPI): Promise<void> {
|
|
31
21
|
const target = currentVehicleClientTarget();
|
|
32
22
|
if (!target) return;
|
|
33
23
|
try {
|
|
34
24
|
const client = new RemoteVehicleClient({ baseUrl: target.baseUrl, token: target.token });
|
|
35
25
|
await registerVehicleTools(pi, client, {
|
|
36
|
-
permissions:
|
|
26
|
+
permissions: REGISTERED_PERMISSIONS,
|
|
37
27
|
principal: { id: "pi-papyrus" },
|
|
38
28
|
});
|
|
39
29
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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"],
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typebox": "*"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@danypops/papyrus": "^0.
|
|
20
|
+
"@danypops/papyrus": "^0.39.0",
|
|
21
21
|
"@danypops/vehicle-core": "^0.1.1",
|
|
22
22
|
"@danypops/vehicle-server": "^0.1.1",
|
|
23
23
|
"@danypops/vehicle-client": "^0.1.1",
|