@danypops/papyrus 0.21.2 → 0.21.4
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
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,25 +289,28 @@ 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") {
|
|
271
|
-
const gates = await
|
|
307
|
+
const [gates, task] = await Promise.all([
|
|
308
|
+
callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", request),
|
|
309
|
+
callService<Record<string, unknown>, Artifact>("tasks.show", { id: params.id }),
|
|
310
|
+
]);
|
|
272
311
|
return text(
|
|
273
312
|
gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n") || "No gates configured.",
|
|
274
|
-
createGateRunDetails("tasks.run_gates", (params.id as string | undefined) ?? "", gates.map((gate) => ({
|
|
313
|
+
createGateRunDetails("tasks.run_gates", (params.id as string | undefined) ?? "", task.title, gates.map((gate) => ({
|
|
275
314
|
passed: gate.passed, type: gate.gate.type, target: gate.gate.target, output: gate.output,
|
|
276
315
|
}))),
|
|
277
316
|
);
|
|
@@ -297,7 +336,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
297
336
|
if (!operation) throw new Error(`unknown tasks action: ${action}`);
|
|
298
337
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
|
|
299
338
|
if (operation === "tasks.focus") emitTaskFocusEvent({ taskId: artifact.id, sessionId: request.session_id as string, status: "focused" });
|
|
300
|
-
return text(
|
|
339
|
+
return text(artifactLine(artifact), createArtifactDetails(operation, artifact));
|
|
301
340
|
} catch (error) {
|
|
302
341
|
throw new Error(`tasks failed: ${error instanceof Error ? error.message : error}`);
|
|
303
342
|
}
|
|
@@ -307,10 +346,11 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
307
346
|
pi.registerTool({
|
|
308
347
|
name: "notes",
|
|
309
348
|
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.",
|
|
349
|
+
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
350
|
parameters: Type.Object({
|
|
312
351
|
action: Type.String(),
|
|
313
352
|
id: Type.Optional(Type.String()),
|
|
353
|
+
name: Type.Optional(Type.String()),
|
|
314
354
|
body: Type.Optional(Type.String()),
|
|
315
355
|
title: Type.Optional(Type.String()),
|
|
316
356
|
status: Type.Optional(Type.Union([Type.Literal("draft"), Type.Literal("active"), Type.Literal("archived")])),
|
|
@@ -324,17 +364,20 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
324
364
|
}),
|
|
325
365
|
renderCall(args, theme) { return renderPapyrusToolCall("Notes", args, theme); },
|
|
326
366
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
327
|
-
async execute(_id,
|
|
367
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
328
368
|
try {
|
|
369
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
329
370
|
const action = params.action;
|
|
330
|
-
const
|
|
371
|
+
const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "notes-tool" };
|
|
372
|
+
await resolveNameFields(params, [{ nameKey: "name", idKey: "id", listOperation: "notes.list", baseRequest }]);
|
|
373
|
+
const request = { ...params, ...baseRequest };
|
|
331
374
|
if (action === "capture") {
|
|
332
375
|
const artifact = await callService<Record<string, unknown>, Artifact>("notes.capture", request);
|
|
333
376
|
return text(`Captured note ${artifactLine(artifact)}`, createArtifactDetails("notes.capture", artifact));
|
|
334
377
|
}
|
|
335
378
|
if (action === "list") {
|
|
336
379
|
const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", request);
|
|
337
|
-
return text(rows.length ? rows
|
|
380
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No open notes.", createArtifactListDetails("notes.list", rows));
|
|
338
381
|
}
|
|
339
382
|
if (action === "show") {
|
|
340
383
|
const artifact = await callService<Record<string, unknown>, Artifact>("notes.show", request);
|
|
@@ -354,10 +397,11 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
354
397
|
pi.registerTool({
|
|
355
398
|
name: "docs",
|
|
356
399
|
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.",
|
|
400
|
+
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
401
|
parameters: Type.Object({
|
|
359
402
|
action: Type.String(),
|
|
360
403
|
id: Type.Optional(Type.String()),
|
|
404
|
+
name: Type.Optional(Type.String()),
|
|
361
405
|
title: Type.Optional(Type.String()),
|
|
362
406
|
body: Type.Optional(Type.String()),
|
|
363
407
|
subtype: Type.Optional(Type.String()),
|
|
@@ -369,21 +413,29 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
369
413
|
template_id: Type.Optional(Type.String()),
|
|
370
414
|
relation: Type.Optional(Type.String()),
|
|
371
415
|
target_id: Type.Optional(Type.String()),
|
|
416
|
+
target_name: Type.Optional(Type.String()),
|
|
372
417
|
project_root: Type.Optional(Type.String()),
|
|
373
418
|
reason: Type.Optional(Type.String()),
|
|
374
419
|
}),
|
|
375
420
|
renderCall(args, theme) { return renderPapyrusToolCall("Documents", args, theme); },
|
|
376
421
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
377
|
-
async execute(_id,
|
|
422
|
+
async execute(_id, rawParams) {
|
|
378
423
|
try {
|
|
424
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
379
425
|
const action = params.action;
|
|
426
|
+
const scopeRequest = { project_root: params.project_root };
|
|
427
|
+
await resolveNameFields(params, [
|
|
428
|
+
{ nameKey: "name", idKey: "id", listOperation: "docs.list", baseRequest: scopeRequest },
|
|
429
|
+
// Kind-agnostic: a link target can be a doc, task, rule, or skill, so this searches every kind rather than only docs.
|
|
430
|
+
{ nameKey: "target_name", idKey: "target_id", listOperation: "artifact.query", baseRequest: scopeRequest },
|
|
431
|
+
]);
|
|
380
432
|
if (action === "create") {
|
|
381
433
|
const artifact = await callService<Record<string, unknown>, Artifact>("docs.create", params);
|
|
382
434
|
return text(`Created document ${artifactLine(artifact)}`, createArtifactDetails("docs.create", artifact));
|
|
383
435
|
}
|
|
384
436
|
if (action === "list") {
|
|
385
437
|
const rows = await callService<Record<string, unknown>, Artifact[]>("docs.list", params);
|
|
386
|
-
return text(rows.length ? rows
|
|
438
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No documents found.", createArtifactListDetails("docs.list", rows));
|
|
387
439
|
}
|
|
388
440
|
if (action === "show") {
|
|
389
441
|
const artifact = await callService<Record<string, unknown>, Artifact>("docs.show", params);
|
|
@@ -405,27 +457,33 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
405
457
|
pi.registerTool({
|
|
406
458
|
name: "rules",
|
|
407
459
|
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.",
|
|
460
|
+
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
461
|
parameters: Type.Object({
|
|
410
|
-
action: Type.String(), id: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
462
|
+
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
411
463
|
body: Type.Optional(Type.String()), condition: Type.Optional(Type.String()), rule_action: Type.Optional(Type.String()),
|
|
412
464
|
severity: Type.Optional(Type.String()), labels: Type.Optional(Type.Array(Type.String())),
|
|
413
465
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
414
466
|
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), task_id: Type.Optional(Type.String()),
|
|
467
|
+
task_name: Type.Optional(Type.String()),
|
|
415
468
|
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
416
469
|
}),
|
|
417
470
|
renderCall(args, theme) { return renderPapyrusToolCall("Rules", args, theme); },
|
|
418
471
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
419
|
-
async execute(_id,
|
|
472
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
420
473
|
try {
|
|
474
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
421
475
|
const action = params.action;
|
|
476
|
+
await resolveNameFields(params, [
|
|
477
|
+
{ nameKey: "name", idKey: "id", listOperation: "rules.list", baseRequest: { project_root: params.project_root } },
|
|
478
|
+
{ nameKey: "task_name", idKey: "task_id", listOperation: "tasks.list", baseRequest: { project_root: params.project_root ?? ctx.cwd } },
|
|
479
|
+
]);
|
|
422
480
|
if (action === "create") {
|
|
423
481
|
const artifact = await callService<Record<string, unknown>, Artifact>("rules.create", params);
|
|
424
482
|
return text(`Created rule ${artifactLine(artifact)}`, createArtifactDetails("rules.create", artifact));
|
|
425
483
|
}
|
|
426
484
|
if (action === "list") {
|
|
427
485
|
const rows = await callService<Record<string, unknown>, Artifact[]>("rules.list", params);
|
|
428
|
-
return text(rows.length ? rows
|
|
486
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No rules found.", createArtifactListDetails("rules.list", rows));
|
|
429
487
|
}
|
|
430
488
|
if (action === "preview") {
|
|
431
489
|
const preview = await callService<Record<string, unknown>, string>("rules.preview", params);
|
|
@@ -447,25 +505,31 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
447
505
|
pi.registerTool({
|
|
448
506
|
name: "skills",
|
|
449
507
|
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.",
|
|
508
|
+
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
509
|
parameters: Type.Object({
|
|
452
|
-
action: Type.String(), id: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
510
|
+
action: Type.String(), id: Type.Optional(Type.String()), name: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
453
511
|
body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
|
|
454
512
|
tools: Type.Optional(Type.Array(Type.String())), definition: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
455
513
|
arguments: Type.Optional(Type.Record(Type.String(), Type.Unknown())), run_id: Type.Optional(Type.String()),
|
|
456
514
|
labels: Type.Optional(Type.Array(Type.String())),
|
|
457
515
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
458
516
|
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), template_id: Type.Optional(Type.String()),
|
|
517
|
+
template_name: Type.Optional(Type.String()),
|
|
459
518
|
target_kind: Type.Optional(Type.String()), defaults: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
460
519
|
required: Type.Optional(Type.Array(Type.String())), kind: Type.Optional(Type.String()), subtype: Type.Optional(Type.String()),
|
|
461
520
|
project_root: Type.Optional(Type.String()), reason: Type.Optional(Type.String()),
|
|
462
521
|
}),
|
|
463
522
|
renderCall(args, theme) { return renderPapyrusToolCall("Skills", args, theme); },
|
|
464
523
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
465
|
-
async execute(_id,
|
|
524
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
466
525
|
try {
|
|
526
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
467
527
|
const action = params.action;
|
|
468
528
|
const request = { ...params, project_root: params.project_root ?? ctx.cwd };
|
|
529
|
+
await resolveNameFields(params, [
|
|
530
|
+
{ nameKey: "name", idKey: "id", listOperation: "skills.list", baseRequest: { project_root: params.project_root } },
|
|
531
|
+
{ nameKey: "template_name", idKey: "template_id", listOperation: "skills.list", baseRequest: { project_root: params.project_root } },
|
|
532
|
+
]);
|
|
469
533
|
if (action === "create" || action === "create_template") {
|
|
470
534
|
const operation = action === "create" ? "skills.create" : "skills.create_template";
|
|
471
535
|
const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
|
|
@@ -473,7 +537,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
473
537
|
}
|
|
474
538
|
if (action === "list") {
|
|
475
539
|
const rows = await callService<Record<string, unknown>, Artifact[]>("skills.list", params);
|
|
476
|
-
return text(rows.length ? rows
|
|
540
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No skills found.", createArtifactListDetails("skills.list", rows));
|
|
477
541
|
}
|
|
478
542
|
if (action === "invoke") {
|
|
479
543
|
const invocation = await callService<Record<string, unknown>, string>("skills.invoke", params);
|
|
@@ -481,10 +545,19 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
481
545
|
}
|
|
482
546
|
if (action === "run") {
|
|
483
547
|
const run = await callService<Record<string, unknown>, SkillWorkflowRunResult>("skills.run", request);
|
|
484
|
-
const
|
|
548
|
+
const runTitleCounts = new Map<string, number>();
|
|
549
|
+
for (const node of run.execution.nodes) runTitleCounts.set(node.title, (runTitleCounts.get(node.title) ?? 0) + 1);
|
|
550
|
+
const execution = run.execution.nodes.map((node) => (runTitleCounts.get(node.title) ?? 0) > 1
|
|
551
|
+
? ` [${node.state}] ${node.title} (${node.id})`
|
|
552
|
+
: ` [${node.state}] ${node.title}`).join("\n");
|
|
553
|
+
// Root task titles are free here (already present in execution.nodes); created docs/rules
|
|
554
|
+
// are a different kind not covered by this run's own execution nodes, so those still list by
|
|
555
|
+
// id below -- fetching their titles would mean an extra round-trip per artifact.
|
|
556
|
+
const nodeById = new Map(run.execution.nodes.map((node) => [node.id, node]));
|
|
557
|
+
const rootLabels = run.rootTaskIds.map((id) => nodeById.get(id)?.title ?? id);
|
|
485
558
|
return text([
|
|
486
559
|
`Created Skill run ${run.runId}: ${run.created.tasks.length} tasks, ${run.created.rules.length} rules, ${run.created.docs.length} docs.`,
|
|
487
|
-
`Ready roots: ${
|
|
560
|
+
`Ready roots: ${rootLabels.join(", ") || "none"}.`,
|
|
488
561
|
`Context docs: ${run.created.docs.join(", ") || "none"}.`,
|
|
489
562
|
`Scoped rules: ${run.created.rules.join(", ") || "none"}.`,
|
|
490
563
|
...(execution ? ["Execution:", execution] : []),
|
|
@@ -511,17 +584,20 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
511
584
|
pi.registerTool({
|
|
512
585
|
name: "discuss",
|
|
513
586
|
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.",
|
|
587
|
+
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
588
|
parameters: Type.Object({
|
|
516
589
|
action: Type.String(),
|
|
517
590
|
id: Type.Optional(Type.String()),
|
|
591
|
+
name: Type.Optional(Type.String()),
|
|
518
592
|
title: Type.Optional(Type.String()),
|
|
519
593
|
actor: Type.Optional(Type.String()),
|
|
520
594
|
content: Type.Optional(Type.String()),
|
|
521
595
|
body: Type.Optional(Type.String()),
|
|
522
596
|
labels: Type.Optional(Type.Array(Type.String())),
|
|
523
597
|
blocks_task_ids: Type.Optional(Type.Array(Type.String())),
|
|
598
|
+
blocks_task_names: Type.Optional(Type.Array(Type.String())),
|
|
524
599
|
task_id: Type.Optional(Type.String()),
|
|
600
|
+
task_name: Type.Optional(Type.String()),
|
|
525
601
|
reason: Type.Optional(Type.String()),
|
|
526
602
|
settlement: Type.Optional(Type.String()),
|
|
527
603
|
state: Type.Optional(Type.String()),
|
|
@@ -533,26 +609,36 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
533
609
|
}),
|
|
534
610
|
renderCall(args, theme) { return renderPapyrusToolCall("Discuss", args, theme); },
|
|
535
611
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
536
|
-
async execute(_id,
|
|
612
|
+
async execute(_id, rawParams, _signal, _onUpdate, ctx) {
|
|
537
613
|
try {
|
|
614
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
538
615
|
const action = params.action;
|
|
616
|
+
const taskScope = { project_root: ctx.cwd };
|
|
617
|
+
await resolveNameFields(params, [
|
|
618
|
+
{ nameKey: "name", idKey: "id", listOperation: "discuss.list", baseRequest: {} },
|
|
619
|
+
{ nameKey: "task_name", idKey: "task_id", listOperation: "tasks.list", baseRequest: taskScope },
|
|
620
|
+
]);
|
|
621
|
+
await resolveNameArrayField(params, "blocks_task_names", "blocks_task_ids", "tasks.list", taskScope);
|
|
539
622
|
if (action === "open") {
|
|
540
623
|
const result = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.open", params);
|
|
541
624
|
return text(`Opened discussion ${artifactLine(result.discussion)}`, createArtifactDetails("discuss.open", result.discussion));
|
|
542
625
|
}
|
|
543
626
|
if (action === "reply") {
|
|
544
627
|
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
|
-
|
|
628
|
+
return text(`Round ${result.rounds[0]?.roundNumber} added to "${result.discussion.title}"`, createArtifactDetails("discuss.reply", result.discussion));
|
|
629
|
+
}
|
|
630
|
+
if (action === "block" || action === "unblock") {
|
|
631
|
+
const operation = action === "block" ? "discuss.block" : "discuss.unblock";
|
|
632
|
+
const [outcome, discussionAndRounds, task] = await Promise.all([
|
|
633
|
+
callService<Record<string, unknown>, { blocked?: boolean; unblocked?: boolean }>(operation, params),
|
|
634
|
+
callService<Record<string, unknown>, DiscussionAndRounds>("discuss.show", { id: params.id }),
|
|
635
|
+
callService<Record<string, unknown>, Artifact>("tasks.show", { id: params.task_id }),
|
|
636
|
+
]);
|
|
637
|
+
const discussion = discussionAndRounds.discussion;
|
|
638
|
+
const message = action === "unblock" && !outcome.unblocked
|
|
639
|
+
? "No such blocking relationship."
|
|
640
|
+
: `"${discussion.title}" ${action === "block" ? "now blocks" : "no longer blocks"} "${task.title}"`;
|
|
641
|
+
return text(message, createPreviewDetails(operation, action === "block" ? "Blocked" : "Unblocked", message));
|
|
556
642
|
}
|
|
557
643
|
if (action === "show") {
|
|
558
644
|
const result = await callService<Record<string, unknown>, DiscussionAndRounds>("discuss.show", params);
|
|
@@ -566,7 +652,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
566
652
|
}
|
|
567
653
|
if (action === "list") {
|
|
568
654
|
const rows = await callService<Record<string, unknown>, Artifact[]>("discuss.list", params);
|
|
569
|
-
return text(rows.length ? rows
|
|
655
|
+
return text(rows.length ? artifactLines(rows).join("\n") : "No discussions found.", createArtifactListDetails("discuss.list", rows));
|
|
570
656
|
}
|
|
571
657
|
const operations = { defer: "discuss.defer", resume: "discuss.resume", settle: "discuss.settle" } as const;
|
|
572
658
|
const operation = operations[action as keyof typeof operations];
|
|
@@ -16,7 +16,9 @@ export interface PapyrusToolRenderContext {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function primaryArgument(args: Record<string, unknown>): string | undefined {
|
|
19
|
-
|
|
19
|
+
// name/title before id: a caller that already knows the name shouldn't have the raw id echoed
|
|
20
|
+
// back at it; id only surfaces here when it's genuinely the only identifying argument given.
|
|
21
|
+
for (const key of ["name", "title", "id", "text", "query", "kind", "template_id"]) {
|
|
20
22
|
const value = args[key];
|
|
21
23
|
if (typeof value === "string" && value.trim()) return value.slice(0, CALL_VALUE_MAX_CHARACTERS);
|
|
22
24
|
}
|
|
@@ -45,11 +47,11 @@ function textContent(result: AgentToolResult<unknown>): string {
|
|
|
45
47
|
function simpleDetailsText(details: Exclude<PapyrusToolDetails, { kind: "artifact" | "artifact-list" | "graph" }>): string {
|
|
46
48
|
switch (details.kind) {
|
|
47
49
|
case "transition":
|
|
48
|
-
return `✓ ${details.
|
|
50
|
+
return `✓ ${details.fromStatus} → ${details.toStatus}\n${details.artifact.title}`;
|
|
49
51
|
case "gate-run": {
|
|
50
52
|
const passed = details.gates.filter((gate) => gate.passed).length;
|
|
51
53
|
return [
|
|
52
|
-
`${passed}/${details.gates.length} gates passed for ${details.
|
|
54
|
+
`${passed}/${details.gates.length} gates passed for "${details.artifactTitle}"`,
|
|
53
55
|
...details.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.type}: ${gate.target}${gate.output ? ` — ${gate.output}` : ""}`),
|
|
54
56
|
].join("\n");
|
|
55
57
|
}
|
|
@@ -81,6 +81,7 @@ export interface ToolGateRow {
|
|
|
81
81
|
export interface GateRunToolDetails extends ToolDetailsBase {
|
|
82
82
|
kind: "gate-run";
|
|
83
83
|
artifactId: string;
|
|
84
|
+
artifactTitle: string;
|
|
84
85
|
gates: ToolGateRow[];
|
|
85
86
|
completeness: ResultCompleteness;
|
|
86
87
|
}
|
|
@@ -218,6 +219,7 @@ export function createGraphDetails(
|
|
|
218
219
|
export function createGateRunDetails(
|
|
219
220
|
operation: string,
|
|
220
221
|
artifactId: string,
|
|
222
|
+
artifactTitle: string,
|
|
221
223
|
gates: readonly ToolGateRow[],
|
|
222
224
|
): GateRunToolDetails {
|
|
223
225
|
const boundedGates = gates.slice(0, TOOL_DETAILS_MAX_ITEMS).map((gate) => ({
|
|
@@ -229,6 +231,7 @@ export function createGateRunDetails(
|
|
|
229
231
|
kind: "gate-run",
|
|
230
232
|
operation,
|
|
231
233
|
artifactId,
|
|
234
|
+
artifactTitle,
|
|
232
235
|
gates: boundedGates,
|
|
233
236
|
completeness: completeness(gates.length, boundedGates.length),
|
|
234
237
|
};
|
|
@@ -383,6 +386,7 @@ export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | un
|
|
|
383
386
|
? value as unknown as GraphToolDetails : undefined;
|
|
384
387
|
case "gate-run":
|
|
385
388
|
return isBoundedString(value.artifactId)
|
|
389
|
+
&& isBoundedString(value.artifactTitle)
|
|
386
390
|
&& isBoundedArray(value.gates, TOOL_DETAILS_MAX_ITEMS, isGateRow)
|
|
387
391
|
&& isCompleteness(value.completeness)
|
|
388
392
|
? value as unknown as GateRunToolDetails : undefined;
|
package/package.json
CHANGED