@danypops/papyrus 0.21.2 → 0.21.3
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 +4 -2
- package/extension/src/domain-tools.ts +166 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,10 @@ Every tool operation is registered in the daemon’s `/api/v1/ops` registry; par
|
|
|
127
127
|
|
|
128
128
|
Internally, application services depend on the `ArtifactStore` and `GateRunner` ports. SQLite and subprocess execution are adapters composed only by the daemon; task behavior is unit-tested against fakes without a database. Task visualization projects the same `TaskGraph` into semantic display graphs and sends them through a `GraphRenderer` port; the Pi adapter uses `beautiful-mermaid` for terminal Unicode output without leaking Mermaid syntax into the task domain.
|
|
129
129
|
|
|
130
|
+
### Naming vs. ids
|
|
131
|
+
|
|
132
|
+
Every agent domain tool (tasks, docs, rules, skills, notes, discuss) addresses its artifacts by `name` (the exact title) wherever `id` would otherwise be required -- `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` (tasks), `target_name` (docs link, searches every kind since a link target can be any of them), `task_name` (rules gate, discuss block/unblock), `template_name` (skills instantiate), and `blocks_task_names` (discuss open) are the name-based equivalents of their `*_id` counterparts. Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an unmatched or ambiguous name fails with a clear error (ambiguous names list the real ids, since that's the one point disambiguation genuinely needs them). Results returned to the agent likewise lead with name and status, never id, unless two artifacts in the same result share a title -- id is a backend implementation detail, not a conversational handle. `id` itself still works exactly as before for every action, in every tool.
|
|
133
|
+
|
|
130
134
|
## Interactive frontends
|
|
131
135
|
|
|
132
136
|
- `/tasks` — project/focused-graph scope, task lifecycle, append-only history, gates, dependencies, and nested metadata
|
|
@@ -179,8 +183,6 @@ papyrus discuss show <discussion-id> --json
|
|
|
179
183
|
|
|
180
184
|
## Tasks
|
|
181
185
|
|
|
182
|
-
The `tasks` agent tool addresses a task by `name` (its exact title) wherever `id` would otherwise be required -- `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` are the name-based equivalents of `dependency_id`/`parent_id`/`child_id`/`root_task_id`/`depends_on`. Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an unmatched or ambiguous name fails with a clear error (ambiguous names list the real ids, since that's the one point disambiguation genuinely needs them). Task results returned to the agent likewise lead with name and status, never id, unless two tasks in the same result share a title -- id is a backend implementation detail, not a conversational handle. `id` itself still works exactly as before for every action.
|
|
183
|
-
|
|
184
186
|
Run `/tasks` for the interactive task panel:
|
|
185
187
|
|
|
186
188
|
- `/` filters; arrow keys navigate; Enter opens task actions; `s` switches among the persisted current-project, focused-root graph, and explicit all-projects views
|
|
@@ -9,6 +9,7 @@ import type { TaskCompletion, TaskGraph } from "../../src/task-service.ts";
|
|
|
9
9
|
import type { SkillWorkflowRunResult } from "../../src/skill-execution.ts";
|
|
10
10
|
import type { DiscussionAndRounds } from "../../src/discussion-service.ts";
|
|
11
11
|
import type { DiscussionRound } from "../../src/domain/discussion.ts";
|
|
12
|
+
import type { OperationName } from "../../src/service.ts";
|
|
12
13
|
import { emitTaskFocusEvent } from "./task-focus-events.ts";
|
|
13
14
|
import { sessionSecretField } from "./session-identity.ts";
|
|
14
15
|
import { NOTE_DISPOSITIONS } from "../../src/note-service.ts";
|
|
@@ -29,27 +30,22 @@ function text(message: string, details: unknown = {}) {
|
|
|
29
30
|
return { content: [{ type: "text" as const, text: modelContent.text }], details };
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function artifactLine(artifact: Artifact): string {
|
|
33
|
-
return `${artifact.id} [${artifact.status}] ${artifact.title}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
33
|
/**
|
|
37
|
-
*
|
|
38
|
-
* backend detail (a stable key other operations need, and titles aren't
|
|
39
|
-
* it stays out of what the model reads by default. It only resurfaces
|
|
40
|
-
* tell two same-titled
|
|
41
|
-
* error, never as a matter of course.
|
|
42
|
-
* Rules/Skills/Discuss keep the shared artifactLine above unless a similar request covers them.
|
|
34
|
+
* Every domain tool's primary interfacing point is an artifact's NAME, not its id -- id is a
|
|
35
|
+
* backend implementation detail (a stable key other operations need, and titles aren't
|
|
36
|
+
* guaranteed unique), so it stays out of what the model reads by default. It only resurfaces
|
|
37
|
+
* when genuinely needed to tell two same-titled artifacts apart (artifactLines below), or in a
|
|
38
|
+
* matchArtifactByName disambiguation error, never as a matter of course.
|
|
43
39
|
*/
|
|
44
|
-
export function
|
|
45
|
-
return `[${
|
|
40
|
+
export function artifactLine(artifact: Artifact): string {
|
|
41
|
+
return `[${artifact.status}] ${artifact.title}`;
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
/** Appends " (id)" only for
|
|
49
|
-
export function
|
|
44
|
+
/** Appends " (id)" only for artifacts whose title collides with another in this same result set. */
|
|
45
|
+
export function artifactLines(artifacts: Artifact[]): string[] {
|
|
50
46
|
const titleCounts = new Map<string, number>();
|
|
51
|
-
for (const
|
|
52
|
-
return
|
|
47
|
+
for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
|
|
48
|
+
return artifacts.map((artifact) => (titleCounts.get(artifact.title)! > 1 ? `${artifactLine(artifact)} (${artifact.id})` : artifactLine(artifact)));
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
/**
|
|
@@ -58,20 +54,53 @@ export function taskLines(tasks: Artifact[]): string[] {
|
|
|
58
54
|
* remains the one truly unambiguous key, so ambiguity is exactly where it's allowed to resurface.
|
|
59
55
|
* Pure and synchronous so it's directly testable without a service round-trip.
|
|
60
56
|
*/
|
|
61
|
-
export function
|
|
57
|
+
export function matchArtifactByName(candidates: Artifact[], name: string): string {
|
|
62
58
|
const needle = name.trim().toLowerCase();
|
|
63
|
-
const matches = candidates.filter((
|
|
64
|
-
if (matches.length === 0) throw new Error(`no
|
|
59
|
+
const matches = candidates.filter((artifact) => artifact.title.trim().toLowerCase() === needle);
|
|
60
|
+
if (matches.length === 0) throw new Error(`no artifact named "${name}" found in this scope`);
|
|
65
61
|
if (matches.length > 1) {
|
|
66
|
-
throw new Error(`${matches.length}
|
|
62
|
+
throw new Error(`${matches.length} artifacts are named "${name}": ${matches.map((artifact) => `${artifact.title} (${artifact.id})`).join(", ")} -- use id to disambiguate`);
|
|
67
63
|
}
|
|
68
64
|
return matches[0]!.id;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Resolves a name to its id via `listOperation` (whichever kind's list call is the right search
|
|
69
|
+
* scope -- tasks.list, docs.list, rules.list, skills.list, notes.list, discuss.list, or the
|
|
70
|
+
* kind-agnostic artifact.query for a cross-kind reference like a link target). `baseRequest`
|
|
71
|
+
* should mirror whatever scoping (project_root, etc.) that operation's own "list" action already
|
|
72
|
+
* uses, so resolution never searches a wider or narrower scope than a plain list call would.
|
|
73
|
+
*/
|
|
74
|
+
async function resolveArtifactIdByName(listOperation: OperationName, baseRequest: Record<string, unknown>, name: string): Promise<string> {
|
|
75
|
+
const candidates = await callService<Record<string, unknown>, Artifact[]>(listOperation, { ...baseRequest, text: name });
|
|
76
|
+
return matchArtifactByName(candidates, name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolves every {nameKey -> idKey} pair present and not already satisfied by an explicit id, in place. */
|
|
80
|
+
async function resolveNameFields(
|
|
81
|
+
params: Record<string, unknown>,
|
|
82
|
+
fields: ReadonlyArray<{ nameKey: string; idKey: string; listOperation: OperationName; baseRequest: Record<string, unknown> }>,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
for (const { nameKey, idKey, listOperation, baseRequest } of fields) {
|
|
85
|
+
const nameValue = params[nameKey];
|
|
86
|
+
if (typeof nameValue === "string" && nameValue.length > 0 && !params[idKey]) {
|
|
87
|
+
params[idKey] = await resolveArtifactIdByName(listOperation, baseRequest, nameValue);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Resolves a `namesKey` string array to an `idsKey` id array, only when idsKey isn't already explicitly given. */
|
|
93
|
+
async function resolveNameArrayField(
|
|
94
|
+
params: Record<string, unknown>,
|
|
95
|
+
namesKey: string,
|
|
96
|
+
idsKey: string,
|
|
97
|
+
listOperation: OperationName,
|
|
98
|
+
baseRequest: Record<string, unknown>,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
const names = params[namesKey];
|
|
101
|
+
if (Array.isArray(names) && names.length > 0 && !params[idsKey]) {
|
|
102
|
+
params[idsKey] = await Promise.all(names.map((entry) => resolveArtifactIdByName(listOperation, baseRequest, String(entry))));
|
|
103
|
+
}
|
|
75
104
|
}
|
|
76
105
|
|
|
77
106
|
/**
|
|
@@ -82,13 +111,27 @@ async function resolveTaskIdByName(baseRequest: Record<string, unknown>, name: s
|
|
|
82
111
|
* Returns null when action is neither, so callers fall through to their own dispatch.
|
|
83
112
|
*/
|
|
84
113
|
async function handleArtifactRemoveRestore(action: unknown, params: Record<string, unknown>): Promise<ReturnType<typeof text> | null> {
|
|
114
|
+
// Trashed/restored are still directly showable by id (see artifact-trash.ts), so the title is
|
|
115
|
+
// available either side of the action -- fetched here purely for a name-primary message; falls
|
|
116
|
+
// back to the raw id only if the artifact genuinely can't be shown (e.g. an unknown id).
|
|
117
|
+
const titleOf = async (): Promise<string> => {
|
|
118
|
+
try {
|
|
119
|
+
const artifact = await callService<Record<string, unknown>, Artifact | null>("artifact.show", { id: params["id"] });
|
|
120
|
+
return artifact ? `"${artifact.title}"` : String(params["id"]);
|
|
121
|
+
} catch {
|
|
122
|
+
return String(params["id"]);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
85
125
|
if (action === "remove") {
|
|
126
|
+
const label = await titleOf();
|
|
86
127
|
const record = await callService<Record<string, unknown>, { artifactId: string; trashedAt: string; purgeAfter: string; reason?: string }>("artifact.remove", params);
|
|
87
|
-
|
|
128
|
+
const message = `Trashed ${label}, eligible for purge at ${record.purgeAfter}.`;
|
|
129
|
+
return text(message, createPreviewDetails("artifact.remove", "Trashed", record.artifactId));
|
|
88
130
|
}
|
|
89
131
|
if (action === "restore") {
|
|
132
|
+
const label = await titleOf();
|
|
90
133
|
const outcome = await callService<Record<string, unknown>, { restored: boolean }>("artifact.restore", params);
|
|
91
|
-
const output = outcome.restored ? `Restored ${
|
|
134
|
+
const output = outcome.restored ? `Restored ${label}.` : `${label} was not trashed.`;
|
|
92
135
|
return text(output, createPreviewDetails("artifact.restore", "Restored", output));
|
|
93
136
|
}
|
|
94
137
|
return null;
|
|
@@ -157,33 +200,26 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
157
200
|
// Resolves every *_name field to its *_id counterpart before dispatch, so every action
|
|
158
201
|
// below can go on reading id/dependency_id/parent_id/child_id/root_task_id exactly as
|
|
159
202
|
// before -- id-based calls are unaffected; name-based ones are transparently rewritten.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
await
|
|
168
|
-
await resolveField("parent_name", "parent_id");
|
|
169
|
-
await resolveField("child_name", "child_id");
|
|
170
|
-
await resolveField("root_task_name", "root_task_id");
|
|
171
|
-
const dependsOnNames = params["depends_on_names"];
|
|
172
|
-
if (Array.isArray(dependsOnNames) && dependsOnNames.length > 0 && !params["depends_on"]) {
|
|
173
|
-
params["depends_on"] = await Promise.all(dependsOnNames.map((entry) => resolveTaskIdByName(baseRequest, String(entry))));
|
|
174
|
-
}
|
|
203
|
+
await resolveNameFields(params, [
|
|
204
|
+
{ nameKey: "name", idKey: "id", listOperation: "tasks.list", baseRequest },
|
|
205
|
+
{ nameKey: "dependency_name", idKey: "dependency_id", listOperation: "tasks.list", baseRequest },
|
|
206
|
+
{ nameKey: "parent_name", idKey: "parent_id", listOperation: "tasks.list", baseRequest },
|
|
207
|
+
{ nameKey: "child_name", idKey: "child_id", listOperation: "tasks.list", baseRequest },
|
|
208
|
+
{ nameKey: "root_task_name", idKey: "root_task_id", listOperation: "tasks.list", baseRequest },
|
|
209
|
+
]);
|
|
210
|
+
await resolveNameArrayField(params, "depends_on_names", "depends_on", "tasks.list", baseRequest);
|
|
175
211
|
const request = { ...params, ...baseRequest };
|
|
176
212
|
if (action === "create") {
|
|
177
213
|
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create", request);
|
|
178
|
-
return text(`Created task ${
|
|
214
|
+
return text(`Created task ${artifactLine(artifact)}`, createArtifactDetails("tasks.create", artifact));
|
|
179
215
|
}
|
|
180
216
|
if (action === "list") {
|
|
181
217
|
const rows = await callService<Record<string, unknown>, Artifact[]>("tasks.list", request);
|
|
182
|
-
return text(rows.length ?
|
|
218
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No tasks found.", createArtifactListDetails("tasks.list", rows));
|
|
183
219
|
}
|
|
184
220
|
if (action === "show") {
|
|
185
221
|
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.show", params);
|
|
186
|
-
return text(`${
|
|
222
|
+
return text(`${artifactLine(artifact)}\n\n${artifact.body}`, createArtifactDetails("tasks.show", artifact));
|
|
187
223
|
}
|
|
188
224
|
if (action === "history") {
|
|
189
225
|
const page = await callService<Record<string, unknown>, TaskHistoryPage>("tasks.history", request);
|
|
@@ -198,20 +234,20 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
198
234
|
if (action === "active") {
|
|
199
235
|
const artifact = await callService<Record<string, unknown>, Artifact | null>("tasks.active", request);
|
|
200
236
|
return artifact
|
|
201
|
-
? text(`Active: ${
|
|
237
|
+
? text(`Active: ${artifactLine(artifact)}`, createArtifactDetails("tasks.active", artifact))
|
|
202
238
|
: text("No active task.", createPreviewDetails("tasks.active", "Active task", "No active task."));
|
|
203
239
|
}
|
|
204
240
|
if (action === "focused") {
|
|
205
241
|
const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string } | null>("tasks.focused", request);
|
|
206
242
|
return focus
|
|
207
|
-
? text(`Focused (${focus.status}): ${
|
|
243
|
+
? text(`Focused (${focus.status}): ${artifactLine(focus.artifact)}`, createArtifactDetails("tasks.focused", focus.artifact))
|
|
208
244
|
: text("No focused task.", createPreviewDetails("tasks.focused", "Focused task", "No focused task."));
|
|
209
245
|
}
|
|
210
246
|
if (action === "pause" || action === "unpause") {
|
|
211
247
|
const operation = action === "pause" ? "tasks.pause" : "tasks.unpause";
|
|
212
248
|
const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string }>(operation, request);
|
|
213
249
|
emitTaskFocusEvent({ taskId: focus.artifact.id, sessionId: request.session_id as string, status: action === "pause" ? "paused" : "unpaused" });
|
|
214
|
-
return text(`Focused (${focus.status}): ${
|
|
250
|
+
return text(`Focused (${focus.status}): ${artifactLine(focus.artifact)}`, createArtifactDetails(operation, focus.artifact));
|
|
215
251
|
}
|
|
216
252
|
if (action === "clear_focus") {
|
|
217
253
|
const result = await callService<Record<string, unknown>, { cleared: boolean }>("tasks.clear_focus", request);
|
|
@@ -253,18 +289,18 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
253
289
|
}
|
|
254
290
|
if (action === "set_checklist") {
|
|
255
291
|
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.set_checklist", params);
|
|
256
|
-
return text(`Updated checklist: ${
|
|
292
|
+
return text(`Updated checklist: ${artifactLine(artifact)}`, createArtifactDetails("tasks.set_checklist", artifact));
|
|
257
293
|
}
|
|
258
294
|
if (action === "complete") {
|
|
259
295
|
const result = await callService<Record<string, unknown>, TaskCompletion>("tasks.complete", request);
|
|
260
296
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
|
|
261
297
|
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}${item.reason ? ` — ${item.reason}` : ""}`).join("\n");
|
|
262
|
-
const focused = result.focused ? `\nActive: ${
|
|
263
|
-
const blockedLines =
|
|
298
|
+
const focused = result.focused ? `\nActive: ${artifactLine(result.focused)}` : "";
|
|
299
|
+
const blockedLines = artifactLines(result.blocked.map((entry) => entry.artifact));
|
|
264
300
|
const blocked = result.blocked.length > 0
|
|
265
301
|
? `\nBlocked: ${result.blocked.map((entry, index) => `${blockedLines[index]} waits for ${entry.dependencyIds.join(", ")}`).join("; ")}`
|
|
266
302
|
: "";
|
|
267
|
-
const output = `${result.completed ? "Completed" : "Rejected"}: ${
|
|
303
|
+
const output = `${result.completed ? "Completed" : "Rejected"}: ${artifactLine(result.artifact)}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`;
|
|
268
304
|
return text(output, createPreviewDetails("tasks.complete", "Task completion", output));
|
|
269
305
|
}
|
|
270
306
|
if (action === "run_gates") {
|
|
@@ -297,7 +333,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
297
333
|
if (!operation) throw new Error(`unknown tasks action: ${action}`);
|
|
298
334
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
|
|
299
335
|
if (operation === "tasks.focus") emitTaskFocusEvent({ taskId: artifact.id, sessionId: request.session_id as string, status: "focused" });
|
|
300
|
-
return text(
|
|
336
|
+
return text(artifactLine(artifact), createArtifactDetails(operation, artifact));
|
|
301
337
|
} catch (error) {
|
|
302
338
|
throw new Error(`tasks failed: ${error instanceof Error ? error.message : error}`);
|
|
303
339
|
}
|
|
@@ -307,10 +343,11 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
307
343
|
pi.registerTool({
|
|
308
344
|
name: "notes",
|
|
309
345
|
label: "Notes",
|
|
310
|
-
description: "Deferred human-intent inbox. ACTIONS: capture, list, show, consume, promote, archive. Capture stores a request without creating work. Consume marks it considered. To promote, first create the resulting Task, Doc, Rule, or Skill through its domain tool, then link it with target_id. Archive requires an explicit disposition.",
|
|
346
|
+
description: "Deferred human-intent inbox. ACTIONS: capture, list, show, consume, promote, archive. Capture stores a request without creating work. Consume marks it considered. To promote, first create the resulting Task, Doc, Rule, or Skill through its domain tool, then link it with target_id. Archive requires an explicit disposition. PREFER `name` (the note's exact title) over `id` for show/consume/promote/archive -- id is a backend implementation detail, resolved from name automatically.",
|
|
311
347
|
parameters: Type.Object({
|
|
312
348
|
action: Type.String(),
|
|
313
349
|
id: Type.Optional(Type.String()),
|
|
350
|
+
name: Type.Optional(Type.String()),
|
|
314
351
|
body: Type.Optional(Type.String()),
|
|
315
352
|
title: Type.Optional(Type.String()),
|
|
316
353
|
status: Type.Optional(Type.Union([Type.Literal("draft"), Type.Literal("active"), Type.Literal("archived")])),
|
|
@@ -324,17 +361,20 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
324
361
|
}),
|
|
325
362
|
renderCall(args, theme) { return renderPapyrusToolCall("Notes", args, theme); },
|
|
326
363
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
327
|
-
async execute(_id,
|
|
364
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
328
365
|
try {
|
|
366
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
329
367
|
const action = params.action;
|
|
330
|
-
const
|
|
368
|
+
const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "notes-tool" };
|
|
369
|
+
await resolveNameFields(params, [{ nameKey: "name", idKey: "id", listOperation: "notes.list", baseRequest }]);
|
|
370
|
+
const request = { ...params, ...baseRequest };
|
|
331
371
|
if (action === "capture") {
|
|
332
372
|
const artifact = await callService<Record<string, unknown>, Artifact>("notes.capture", request);
|
|
333
373
|
return text(`Captured note ${artifactLine(artifact)}`, createArtifactDetails("notes.capture", artifact));
|
|
334
374
|
}
|
|
335
375
|
if (action === "list") {
|
|
336
376
|
const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", request);
|
|
337
|
-
return text(rows.length ? rows
|
|
377
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No open notes.", createArtifactListDetails("notes.list", rows));
|
|
338
378
|
}
|
|
339
379
|
if (action === "show") {
|
|
340
380
|
const artifact = await callService<Record<string, unknown>, Artifact>("notes.show", request);
|
|
@@ -354,10 +394,11 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
354
394
|
pi.registerTool({
|
|
355
395
|
name: "docs",
|
|
356
396
|
label: "Documents",
|
|
357
|
-
description: "Document domain tool. ACTIONS: create, list, show, activate, archive, reopen, link, assign_project, remove, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. remove moves a Doc to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. Prefer this over low-level papyrus_* tools for document work.",
|
|
397
|
+
description: "Document domain tool. ACTIONS: create, list, show, activate, archive, reopen, link, assign_project, remove, restore. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. remove moves a Doc to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. 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.",
|
|
358
398
|
parameters: Type.Object({
|
|
359
399
|
action: Type.String(),
|
|
360
400
|
id: Type.Optional(Type.String()),
|
|
401
|
+
name: Type.Optional(Type.String()),
|
|
361
402
|
title: Type.Optional(Type.String()),
|
|
362
403
|
body: Type.Optional(Type.String()),
|
|
363
404
|
subtype: Type.Optional(Type.String()),
|
|
@@ -369,21 +410,29 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
369
410
|
template_id: Type.Optional(Type.String()),
|
|
370
411
|
relation: Type.Optional(Type.String()),
|
|
371
412
|
target_id: Type.Optional(Type.String()),
|
|
413
|
+
target_name: Type.Optional(Type.String()),
|
|
372
414
|
project_root: Type.Optional(Type.String()),
|
|
373
415
|
reason: Type.Optional(Type.String()),
|
|
374
416
|
}),
|
|
375
417
|
renderCall(args, theme) { return renderPapyrusToolCall("Documents", args, theme); },
|
|
376
418
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
377
|
-
async execute(_id,
|
|
419
|
+
async execute(_id, rawParams) {
|
|
378
420
|
try {
|
|
421
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
379
422
|
const action = params.action;
|
|
423
|
+
const scopeRequest = { project_root: params.project_root };
|
|
424
|
+
await resolveNameFields(params, [
|
|
425
|
+
{ nameKey: "name", idKey: "id", listOperation: "docs.list", baseRequest: scopeRequest },
|
|
426
|
+
// Kind-agnostic: a link target can be a doc, task, rule, or skill, so this searches every kind rather than only docs.
|
|
427
|
+
{ nameKey: "target_name", idKey: "target_id", listOperation: "artifact.query", baseRequest: scopeRequest },
|
|
428
|
+
]);
|
|
380
429
|
if (action === "create") {
|
|
381
430
|
const artifact = await callService<Record<string, unknown>, Artifact>("docs.create", params);
|
|
382
431
|
return text(`Created document ${artifactLine(artifact)}`, createArtifactDetails("docs.create", artifact));
|
|
383
432
|
}
|
|
384
433
|
if (action === "list") {
|
|
385
434
|
const rows = await callService<Record<string, unknown>, Artifact[]>("docs.list", params);
|
|
386
|
-
return text(rows.length ? rows
|
|
435
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No documents found.", createArtifactListDetails("docs.list", rows));
|
|
387
436
|
}
|
|
388
437
|
if (action === "show") {
|
|
389
438
|
const artifact = await callService<Record<string, unknown>, Artifact>("docs.show", params);
|
|
@@ -405,27 +454,33 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
405
454
|
pi.registerTool({
|
|
406
455
|
name: "rules",
|
|
407
456
|
label: "Rules",
|
|
408
|
-
description: "Rule domain tool. ACTIONS: create, list, show, preview, enable, disable, gate, assign_project, remove, 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. remove moves a Rule to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline.",
|
|
457
|
+
description: "Rule domain tool. ACTIONS: create, list, show, preview, enable, disable, gate, assign_project, remove, 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. remove moves a Rule to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. 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.",
|
|
409
458
|
parameters: Type.Object({
|
|
410
|
-
action: Type.String(), id: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
459
|
+
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
411
460
|
body: Type.Optional(Type.String()), condition: Type.Optional(Type.String()), rule_action: Type.Optional(Type.String()),
|
|
412
461
|
severity: Type.Optional(Type.String()), labels: Type.Optional(Type.Array(Type.String())),
|
|
413
462
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
414
463
|
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), task_id: Type.Optional(Type.String()),
|
|
464
|
+
task_name: Type.Optional(Type.String()),
|
|
415
465
|
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
416
466
|
}),
|
|
417
467
|
renderCall(args, theme) { return renderPapyrusToolCall("Rules", args, theme); },
|
|
418
468
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
419
|
-
async execute(_id,
|
|
469
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
420
470
|
try {
|
|
471
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
421
472
|
const action = params.action;
|
|
473
|
+
await resolveNameFields(params, [
|
|
474
|
+
{ nameKey: "name", idKey: "id", listOperation: "rules.list", baseRequest: { project_root: params.project_root } },
|
|
475
|
+
{ nameKey: "task_name", idKey: "task_id", listOperation: "tasks.list", baseRequest: { project_root: params.project_root ?? ctx.cwd } },
|
|
476
|
+
]);
|
|
422
477
|
if (action === "create") {
|
|
423
478
|
const artifact = await callService<Record<string, unknown>, Artifact>("rules.create", params);
|
|
424
479
|
return text(`Created rule ${artifactLine(artifact)}`, createArtifactDetails("rules.create", artifact));
|
|
425
480
|
}
|
|
426
481
|
if (action === "list") {
|
|
427
482
|
const rows = await callService<Record<string, unknown>, Artifact[]>("rules.list", params);
|
|
428
|
-
return text(rows.length ? rows
|
|
483
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No rules found.", createArtifactListDetails("rules.list", rows));
|
|
429
484
|
}
|
|
430
485
|
if (action === "preview") {
|
|
431
486
|
const preview = await callService<Record<string, unknown>, string>("rules.preview", params);
|
|
@@ -447,25 +502,31 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
447
502
|
pi.registerTool({
|
|
448
503
|
name: "skills",
|
|
449
504
|
label: "Skills",
|
|
450
|
-
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, remove, 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. remove moves a Skill to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline.",
|
|
505
|
+
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, remove, 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. remove moves a Skill to a time-gated trash, excluded from list/query but still directly showable, restorable via restore until the purge deadline. 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.",
|
|
451
506
|
parameters: Type.Object({
|
|
452
|
-
action: Type.String(), id: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
507
|
+
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
453
508
|
body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
|
|
454
509
|
tools: Type.Optional(Type.Array(Type.String())), definition: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
455
510
|
arguments: Type.Optional(Type.Record(Type.String(), Type.Unknown())), run_id: Type.Optional(Type.String()),
|
|
456
511
|
labels: Type.Optional(Type.Array(Type.String())),
|
|
457
512
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
458
513
|
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), template_id: Type.Optional(Type.String()),
|
|
514
|
+
template_name: Type.Optional(Type.String()),
|
|
459
515
|
target_kind: Type.Optional(Type.String()), defaults: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
460
516
|
required: Type.Optional(Type.Array(Type.String())), kind: Type.Optional(Type.String()), subtype: Type.Optional(Type.String()),
|
|
461
517
|
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
462
518
|
}),
|
|
463
519
|
renderCall(args, theme) { return renderPapyrusToolCall("Skills", args, theme); },
|
|
464
520
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
465
|
-
async execute(_id,
|
|
521
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
466
522
|
try {
|
|
523
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
467
524
|
const action = params.action;
|
|
468
525
|
const request = { ...params, project_root: params.project_root ?? ctx.cwd };
|
|
526
|
+
await resolveNameFields(params, [
|
|
527
|
+
{ nameKey: "name", idKey: "id", listOperation: "skills.list", baseRequest: { project_root: params.project_root } },
|
|
528
|
+
{ nameKey: "template_name", idKey: "template_id", listOperation: "skills.list", baseRequest: { project_root: params.project_root } },
|
|
529
|
+
]);
|
|
469
530
|
if (action === "create" || action === "create_template") {
|
|
470
531
|
const operation = action === "create" ? "skills.create" : "skills.create_template";
|
|
471
532
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
@@ -473,7 +534,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
473
534
|
}
|
|
474
535
|
if (action === "list") {
|
|
475
536
|
const rows = await callService<Record<string, unknown>, Artifact[]>("skills.list", params);
|
|
476
|
-
return text(rows.length ? rows
|
|
537
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No skills found.", createArtifactListDetails("skills.list", rows));
|
|
477
538
|
}
|
|
478
539
|
if (action === "invoke") {
|
|
479
540
|
const invocation = await callService<Record<string, unknown>, string>("skills.invoke", params);
|
|
@@ -481,10 +542,19 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
481
542
|
}
|
|
482
543
|
if (action === "run") {
|
|
483
544
|
const run = await callService<Record<string, unknown>, SkillWorkflowRunResult>("skills.run", request);
|
|
484
|
-
const
|
|
545
|
+
const runTitleCounts = new Map<string, number>();
|
|
546
|
+
for (const node of run.execution.nodes) runTitleCounts.set(node.title, (runTitleCounts.get(node.title) ?? 0) + 1);
|
|
547
|
+
const execution = run.execution.nodes.map((node) => (runTitleCounts.get(node.title) ?? 0) > 1
|
|
548
|
+
? ` [${node.state}] ${node.title} (${node.id})`
|
|
549
|
+
: ` [${node.state}] ${node.title}`).join("\n");
|
|
550
|
+
// Root task titles are free here (already present in execution.nodes); created docs/rules
|
|
551
|
+
// are a different kind not covered by this run's own execution nodes, so those still list by
|
|
552
|
+
// id below -- fetching their titles would mean an extra round-trip per artifact.
|
|
553
|
+
const nodeById = new Map(run.execution.nodes.map((node) => [node.id, node]));
|
|
554
|
+
const rootLabels = run.rootTaskIds.map((id) => nodeById.get(id)?.title ?? id);
|
|
485
555
|
return text([
|
|
486
556
|
`Created Skill run ${run.runId}: ${run.created.tasks.length} tasks, ${run.created.rules.length} rules, ${run.created.docs.length} docs.`,
|
|
487
|
-
`Ready roots: ${
|
|
557
|
+
`Ready roots: ${rootLabels.join(", ") || "none"}.`,
|
|
488
558
|
`Context docs: ${run.created.docs.join(", ") || "none"}.`,
|
|
489
559
|
`Scoped rules: ${run.created.rules.join(", ") || "none"}.`,
|
|
490
560
|
...(execution ? ["Execution:", execution] : []),
|
|
@@ -511,17 +581,20 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
511
581
|
pi.registerTool({
|
|
512
582
|
name: "discuss",
|
|
513
583
|
label: "Discuss",
|
|
514
|
-
description: "Native Papyrus deliberation with a real lifecycle -- distinct from a one-shot ask: a Discussion persists, takes multiple rounds, and can genuinely block a Task's completion until settled or deferred. ACTIONS: open, reply, defer, resume, settle, block, unblock, show, rounds, list. open starts round 1 and optionally blocks_task_ids immediately. reply is refused once deferred or settled -- resume first. defer is explicitly non-blocking (paused, resumable); settle is terminal and archives the discussion. block/unblock manage the blocking relationship to a task independently of open. A task's completion is refused while any active Discussion blocks it. open/reply can pose a structured choice via options (2-10 entries) + options_mode ('single' mutually exclusive, 'multi' allows several); reply answers a currently pending choice via selected, validated against it.",
|
|
584
|
+
description: "Native Papyrus deliberation with a real lifecycle -- distinct from a one-shot ask: a Discussion persists, takes multiple rounds, and can genuinely block a Task's completion until settled or deferred. ACTIONS: open, reply, defer, resume, settle, block, unblock, show, rounds, list. open starts round 1 and optionally blocks_task_ids immediately. reply is refused once deferred or settled -- resume first. defer is explicitly non-blocking (paused, resumable); settle is terminal and archives the discussion. block/unblock manage the blocking relationship to a task independently of open. A task's completion is refused while any active Discussion blocks it. open/reply can pose a structured choice via options (2-10 entries) + options_mode ('single' mutually exclusive, 'multi' allows several); reply answers a currently pending choice via selected, validated against it. PREFER `name` (the discussion's exact title) over `id`, `task_name`/`blocks_task_names` over `task_id`/`blocks_task_ids` -- all are backend implementation details, resolved from name automatically.",
|
|
515
585
|
parameters: Type.Object({
|
|
516
586
|
action: Type.String(),
|
|
517
587
|
id: Type.Optional(Type.String()),
|
|
588
|
+
name: Type.Optional(Type.String()),
|
|
518
589
|
title: Type.Optional(Type.String()),
|
|
519
590
|
actor: Type.Optional(Type.String()),
|
|
520
591
|
content: Type.Optional(Type.String()),
|
|
521
592
|
body: Type.Optional(Type.String()),
|
|
522
593
|
labels: Type.Optional(Type.Array(Type.String())),
|
|
523
594
|
blocks_task_ids: Type.Optional(Type.Array(Type.String())),
|
|
595
|
+
blocks_task_names: Type.Optional(Type.Array(Type.String())),
|
|
524
596
|
task_id: Type.Optional(Type.String()),
|
|
597
|
+
task_name: Type.Optional(Type.String()),
|
|
525
598
|
reason: Type.Optional(Type.String()),
|
|
526
599
|
settlement: Type.Optional(Type.String()),
|
|
527
600
|
state: Type.Optional(Type.String()),
|
|
@@ -533,26 +606,36 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
533
606
|
}),
|
|
534
607
|
renderCall(args, theme) { return renderPapyrusToolCall("Discuss", args, theme); },
|
|
535
608
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
536
|
-
async execute(_id,
|
|
609
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
537
610
|
try {
|
|
611
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
538
612
|
const action = params.action;
|
|
613
|
+
const taskScope = { project_root: ctx.cwd };
|
|
614
|
+
await resolveNameFields(params, [
|
|
615
|
+
{ nameKey: "name", idKey: "id", listOperation: "discuss.list", baseRequest: {} },
|
|
616
|
+
{ nameKey: "task_name", idKey: "task_id", listOperation: "tasks.list", baseRequest: taskScope },
|
|
617
|
+
]);
|
|
618
|
+
await resolveNameArrayField(params, "blocks_task_names", "blocks_task_ids", "tasks.list", taskScope);
|
|
539
619
|
if (action === "open") {
|
|
540
620
|
const result = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.open", params);
|
|
541
621
|
return text(`Opened discussion ${artifactLine(result.discussion)}`, createArtifactDetails("discuss.open", result.discussion));
|
|
542
622
|
}
|
|
543
623
|
if (action === "reply") {
|
|
544
624
|
const result = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.reply", params);
|
|
545
|
-
return text(`Round ${result.rounds[0]?.roundNumber} added to ${result.discussion.
|
|
546
|
-
}
|
|
547
|
-
if (action === "block") {
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
625
|
+
return text(`Round ${result.rounds[0]?.roundNumber} added to "${result.discussion.title}"`, createArtifactDetails("discuss.reply", result.discussion));
|
|
626
|
+
}
|
|
627
|
+
if (action === "block" || action === "unblock") {
|
|
628
|
+
const operation = action === "block" ? "discuss.block" : "discuss.unblock";
|
|
629
|
+
const [outcome, discussionAndRounds, task] = await Promise.all([
|
|
630
|
+
callService<Record<string, unknown>, { blocked?: boolean; unblocked?: boolean }>(operation, params),
|
|
631
|
+
callService<Record<string, unknown>, DiscussionAndRounds>("discuss.show", { id: params.id }),
|
|
632
|
+
callService<Record<string, unknown>, Artifact>("tasks.show", { id: params.task_id }),
|
|
633
|
+
]);
|
|
634
|
+
const discussion = discussionAndRounds.discussion;
|
|
635
|
+
const message = action === "unblock" && !outcome.unblocked
|
|
636
|
+
? "No such blocking relationship."
|
|
637
|
+
: `"${discussion.title}" ${action === "block" ? "now blocks" : "no longer blocks"} "${task.title}"`;
|
|
638
|
+
return text(message, createPreviewDetails(operation, action === "block" ? "Blocked" : "Unblocked", message));
|
|
556
639
|
}
|
|
557
640
|
if (action === "show") {
|
|
558
641
|
const result = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.show", params);
|
|
@@ -566,7 +649,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
566
649
|
}
|
|
567
650
|
if (action === "list") {
|
|
568
651
|
const rows = await callService<Record<string, unknown>, Artifact[]>("discuss.list", params);
|
|
569
|
-
return text(rows.length ? rows
|
|
652
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No discussions found.", createArtifactListDetails("discuss.list", rows));
|
|
570
653
|
}
|
|
571
654
|
const operations = { defer: "discuss.defer", resume: "discuss.resume", settle: "discuss.settle" } as const;
|
|
572
655
|
const operation = operations[action as keyof typeof operations];
|
package/package.json
CHANGED