@danypops/papyrus 0.25.0 → 0.26.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.
@@ -43,7 +43,7 @@ function continuationPrompt(task: ActiveTaskMarker): string {
43
43
  "Reconcile its lifecycle, take the next concrete action, use tools, submit it for review when implementation effort is ready, and run gates plus checklist review before completion.",
44
44
  "Do not shrink the task's scope to whatever fits in this turn, and do not treat a status update or summary as a substitute for doing the work or as proof of completion.",
45
45
  "If something blocks progress, do not reject or pause on the first obstacle -- only after it genuinely recurs, and only when the task truly cannot proceed without external input.",
46
- `Active task: ${task.id}: ${task.title.slice(0, TITLE_LIMIT)}`,
46
+ `Active task: ${task.title.slice(0, TITLE_LIMIT)}`,
47
47
  ].join("\n");
48
48
  }
49
49
 
@@ -78,7 +78,7 @@ export async function showArtifactDetails(
78
78
  depth: DETAIL_GRAPH_DEPTH,
79
79
  max_nodes: DETAIL_GRAPH_NODES,
80
80
  });
81
- if (!artifact) { ctx.ui.notify(`Artifact ${id} not found`, "error"); return; }
81
+ if (!artifact) { ctx.ui.notify("Artifact not found", "error"); return; }
82
82
  await showArtifactDetailView(ctx, artifact);
83
83
  } catch (error) {
84
84
  ctx.ui.notify(`Show details failed: ${error instanceof Error ? error.message : error}`, "error");
@@ -92,7 +92,7 @@ export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: str
92
92
  if (!relation) return;
93
93
  try {
94
94
  await callService("graph.link", { from: fromId, relation, to: target });
95
- ctx.ui.notify(`Linked ${fromId} --${relation}--> ${target}`, "info");
95
+ ctx.ui.notify(`Artifacts linked via ${relation}`, "info");
96
96
  } catch (error) {
97
97
  ctx.ui.notify(`Link failed: ${error instanceof Error ? error.message : error}`, "error");
98
98
  }
@@ -101,8 +101,8 @@ export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: str
101
101
  export async function setArtifactStatus(ctx: ExtensionCommandContext, id: string, status: string): Promise<void> {
102
102
  try {
103
103
  const artifact = await callService<Record<string, unknown>, Artifact | null>("graph.status", { id, status });
104
- if (!artifact) { ctx.ui.notify(`Artifact ${id} not found`, "error"); return; }
105
- ctx.ui.notify(`${artifact.id} → [${artifact.status}]`, "info");
104
+ if (!artifact) { ctx.ui.notify("Artifact not found", "error"); return; }
105
+ ctx.ui.notify(`${artifact.title} → [${artifact.status}]`, "info");
106
106
  } catch (error) {
107
107
  ctx.ui.notify(`Status change failed: ${error instanceof Error ? error.message : error}`, "error");
108
108
  }
@@ -45,13 +45,13 @@ export async function showDocs(ctx: ExtensionCommandContext): Promise<void> {
45
45
  const relation = await commandCtx.ui.select("Relation", DOC_RELATIONS);
46
46
  if (!relation) return;
47
47
  await callService("docs.link", { id: document.id, relation, target_id: targetId });
48
- commandCtx.ui.notify(`Linked ${document.id} --${relation}--> ${targetId}`, "info");
48
+ commandCtx.ui.notify(`Linked "${document.title}" via ${relation}`, "info");
49
49
  return;
50
50
  }
51
51
  const operation = choice === "Activate" ? "docs.activate" : choice === "Archive" ? "docs.archive" : choice === "Reopen" ? "docs.reopen" : undefined;
52
52
  if (operation) {
53
53
  const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: document.id });
54
- commandCtx.ui.notify(`${updated.id} → [${updated.status}]`, "info");
54
+ commandCtx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
55
55
  }
56
56
  },
57
57
  });
@@ -69,6 +69,15 @@ export function artifactLines(artifacts: Artifact[]): string[] {
69
69
  return artifacts.map((artifact) => (titleCounts.get(artifact.title)! > 1 ? `${artifactLine(artifact)} (${artifact.id})` : artifactLine(artifact)));
70
70
  }
71
71
 
72
+ /** Resolves internal ids for model text; ids resurface only when equal titles need disambiguation. */
73
+ async function artifactLabelsById(ids: readonly string[]): Promise<Map<string, string>> {
74
+ const uniqueIds = [...new Set(ids)];
75
+ const artifacts = (await Promise.all(uniqueIds.map((id) => callService<Record<string, unknown>, Artifact | null>("artifact.show", { id })))).filter((artifact): artifact is Artifact => artifact !== null);
76
+ const titleCounts = new Map<string, number>();
77
+ for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
78
+ return new Map(artifacts.map((artifact) => [artifact.id, titleCounts.get(artifact.title)! > 1 ? `${artifact.title} (${artifact.id})` : artifact.title]));
79
+ }
80
+
72
81
  /**
73
82
  * Exact, case-insensitive, trimmed title match against an already-fetched candidate set. Throws
74
83
  * a clear "not found" or "ambiguous -- use id" error rather than guessing at a fuzzy match -- id
@@ -132,15 +141,15 @@ async function resolveNameArrayField(
132
141
  * Returns null when action is neither, so callers fall through to their own dispatch.
133
142
  */
134
143
  async function handleArtifactRemoveRestore(action: unknown, params: Record<string, unknown>): Promise<ReturnType<typeof text> | null> {
135
- // Trashed/restored are still directly showable by id (see artifact-trash.ts), so the title is
136
- // available either side of the action -- fetched here purely for a name-primary message; falls
137
- // back to the raw id only if the artifact genuinely can't be shown (e.g. an unknown id).
144
+ // Trashed/restored artifacts stay directly showable, so known identities render by title on
145
+ // either side of the action. An unresolved explicit id stays in structured/error channels;
146
+ // normal model text does not turn that backend key into the artifact's public name.
138
147
  const titleOf = async (): Promise<string> => {
139
148
  try {
140
149
  const artifact = await callService<Record<string, unknown>, Artifact | null>("artifact.show", { id: params["id"] });
141
- return artifact ? `"${artifact.title}"` : String(params["id"]);
150
+ return artifact ? `"${artifact.title}"` : "unknown artifact";
142
151
  } catch {
143
- return String(params["id"]);
152
+ return "unknown artifact";
144
153
  }
145
154
  };
146
155
  if (action === "remove") {
@@ -218,17 +227,26 @@ export function registerDomainTools(pi: ExtensionAPI): void {
218
227
  // ever holds this extension's own registered session anyway (see session-identity.ts).
219
228
  const resolvedSessionId = params.session_id ?? ctx.sessionManager.getSessionId();
220
229
  const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "pi-tool", session_id: resolvedSessionId, ...sessionSecretField(resolvedSessionId as string) };
221
- // Resolves every *_name field to its *_id counterpart before dispatch, so every action
222
- // below can go on reading id/dependency_id/parent_id/child_id/root_task_id exactly as
223
- // before -- id-based calls are unaffected; name-based ones are transparently rewritten.
230
+ // Resolve the graph root first: every other name lookup must use the caller's final
231
+ // project/scope/root selection, otherwise `scope: all|graph` silently collapses back
232
+ // to the current project and forces callers to reach for an id.
233
+ await resolveNameFields(params, [
234
+ { nameKey: "root_task_name", idKey: "root_task_id", listOperation: "tasks.list", baseRequest: { ...baseRequest, scope: "project" } },
235
+ ]);
236
+ const resolutionRequest = {
237
+ ...baseRequest,
238
+ ...(params.scope === undefined ? {} : { scope: params.scope }),
239
+ ...(params.root_task_id === undefined ? {} : { root_task_id: params.root_task_id }),
240
+ };
241
+ // The daemon remains keyed by stable ids; the agent facade resolves names against the
242
+ // exact requested view before dispatching those internal ids.
224
243
  await resolveNameFields(params, [
225
- { nameKey: "name", idKey: "id", listOperation: "tasks.list", baseRequest },
226
- { nameKey: "dependency_name", idKey: "dependency_id", listOperation: "tasks.list", baseRequest },
227
- { nameKey: "parent_name", idKey: "parent_id", listOperation: "tasks.list", baseRequest },
228
- { nameKey: "child_name", idKey: "child_id", listOperation: "tasks.list", baseRequest },
229
- { nameKey: "root_task_name", idKey: "root_task_id", listOperation: "tasks.list", baseRequest },
244
+ { nameKey: "name", idKey: "id", listOperation: "tasks.list", baseRequest: resolutionRequest },
245
+ { nameKey: "dependency_name", idKey: "dependency_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
246
+ { nameKey: "parent_name", idKey: "parent_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
247
+ { nameKey: "child_name", idKey: "child_id", listOperation: "tasks.list", baseRequest: resolutionRequest },
230
248
  ]);
231
- await resolveNameArrayField(params, "depends_on_names", "depends_on", "tasks.list", baseRequest);
249
+ await resolveNameArrayField(params, "depends_on_names", "depends_on", "tasks.list", resolutionRequest);
232
250
  const request = { ...params, ...baseRequest };
233
251
  if (action === "create") {
234
252
  const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create", request);
@@ -294,17 +312,19 @@ export function registerDomainTools(pi: ExtensionAPI): void {
294
312
  const byId = new Map(plan.nodes.map((node) => [node.id, node]));
295
313
  const titleCounts = new Map<string, number>();
296
314
  for (const node of plan.nodes) titleCounts.set(node.title, (titleCounts.get(node.title) ?? 0) + 1);
315
+ const nodeLabel = (id: string): string => {
316
+ const node = byId.get(id);
317
+ if (!node) return "unknown task";
318
+ return (titleCounts.get(node.title) ?? 0) > 1 ? `${node.title} (${node.id})` : node.title;
319
+ };
297
320
  const lines = plan.layers.flatMap((layer, index) => [
298
321
  `Layer ${index + 1}`,
299
322
  ...layer.map((id) => {
300
323
  const node = byId.get(id);
301
- if (!node) return ` [unknown] ${id}`;
302
- return (titleCounts.get(node.title) ?? 0) > 1
303
- ? ` [${node.state}] ${node.title} (${node.id})`
304
- : ` [${node.state}] ${node.title}`;
324
+ return ` [${node?.state ?? "unknown"}] ${nodeLabel(id)}`;
305
325
  }),
306
326
  ]);
307
- if (plan.cycleIds.length > 0) lines.push(`Invalid cycle: ${plan.cycleIds.join(", ")}`);
327
+ if (plan.cycleIds.length > 0) lines.push(`Invalid cycle: ${plan.cycleIds.map(nodeLabel).join(", ")}`);
308
328
  const output = lines.join("\n") || "No tasks in execution plan.";
309
329
  return text(output, createPreviewDetails("tasks.plan", "Task execution plan", output));
310
330
  }
@@ -318,8 +338,9 @@ export function registerDomainTools(pi: ExtensionAPI): void {
318
338
  const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}${item.reason ? ` — ${item.reason}` : ""}`).join("\n");
319
339
  const focused = result.focused ? `\nActive: ${artifactLine(result.focused)}` : "";
320
340
  const blockedLines = artifactLines(result.blocked.map((entry) => entry.artifact));
341
+ const dependencyLabels = await artifactLabelsById(result.blocked.flatMap((entry) => entry.dependencyIds));
321
342
  const blocked = result.blocked.length > 0
322
- ? `\nBlocked: ${result.blocked.map((entry, index) => `${blockedLines[index]} waits for ${entry.dependencyIds.join(", ")}`).join("; ")}`
343
+ ? `\nBlocked: ${result.blocked.map((entry, index) => `${blockedLines[index]} waits for ${entry.dependencyIds.map((id) => dependencyLabels.get(id) ?? "unknown task").join(", ")}`).join("; ")}`
323
344
  : "";
324
345
  const output = `${result.completed ? "Completed" : "Rejected"}: ${artifactLine(result.artifact)}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`;
325
346
  return text(output, createPreviewDetails("tasks.complete", "Task completion", output));
@@ -618,16 +639,14 @@ export function registerDomainTools(pi: ExtensionAPI): void {
618
639
  const execution = run.execution.nodes.map((node) => (runTitleCounts.get(node.title) ?? 0) > 1
619
640
  ? ` [${node.state}] ${node.title} (${node.id})`
620
641
  : ` [${node.state}] ${node.title}`).join("\n");
621
- // Root task titles are free here (already present in execution.nodes); created docs/rules
622
- // are a different kind not covered by this run's own execution nodes, so those still list by
623
- // id below -- fetching their titles would mean an extra round-trip per artifact.
624
642
  const nodeById = new Map(run.execution.nodes.map((node) => [node.id, node]));
625
- const rootLabels = run.rootTaskIds.map((id) => nodeById.get(id)?.title ?? id);
643
+ const rootLabels = run.rootTaskIds.map((id) => nodeById.get(id)?.title ?? "unknown task");
644
+ const createdLabels = await artifactLabelsById([...run.created.docs, ...run.created.rules]);
626
645
  return text([
627
646
  `Created Skill run ${run.runId}: ${run.created.tasks.length} tasks, ${run.created.rules.length} rules, ${run.created.docs.length} docs.`,
628
647
  `Ready roots: ${rootLabels.join(", ") || "none"}.`,
629
- `Context docs: ${run.created.docs.join(", ") || "none"}.`,
630
- `Scoped rules: ${run.created.rules.join(", ") || "none"}.`,
648
+ `Context docs: ${run.created.docs.map((id) => createdLabels.get(id) ?? "unknown document").join(", ") || "none"}.`,
649
+ `Scoped rules: ${run.created.rules.map((id) => createdLabels.get(id) ?? "unknown rule").join(", ") || "none"}.`,
631
650
  ...(execution ? ["Execution:", execution] : []),
632
651
  ].join("\n"), createInvocationDetails("skills.run", run.runId, {
633
652
  tasks: run.created.tasks,
@@ -47,6 +47,27 @@ function text(value: string, details: unknown = {}) {
47
47
  return { content: [{ type: "text" as const, text: modelContent.text }], details };
48
48
  }
49
49
 
50
+ function artifactTextLabel(artifact: Artifact): string {
51
+ return `[${artifact.kind}|${artifact.status}] ${artifact.title}`;
52
+ }
53
+
54
+ function artifactTextLines(artifacts: readonly Artifact[]): string[] {
55
+ const titleCounts = new Map<string, number>();
56
+ for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
57
+ return artifacts.map((artifact) => titleCounts.get(artifact.title)! > 1
58
+ ? `${artifactTextLabel(artifact)} (${artifact.id})`
59
+ : artifactTextLabel(artifact));
60
+ }
61
+
62
+ /** Resolves graph protocol ids into model-facing names; equal titles retain ids only to disambiguate. */
63
+ async function artifactNamesById(ids: readonly string[]): Promise<Map<string, string>> {
64
+ const uniqueIds = [...new Set(ids)];
65
+ const artifacts = (await Promise.all(uniqueIds.map((id) => callService<Record<string, unknown>, Artifact | null>("artifact.show", { id })))).filter((artifact): artifact is Artifact => artifact !== null);
66
+ const titleCounts = new Map<string, number>();
67
+ for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
68
+ return new Map(artifacts.map((artifact) => [artifact.id, titleCounts.get(artifact.title)! > 1 ? `${artifact.title} (${artifact.id})` : artifact.title]));
69
+ }
70
+
50
71
  // ---------------------------------------------------------------------------
51
72
  // Task widget (TodoOverlay pattern from rpiv-todo: factory form, requestRender)
52
73
  // ---------------------------------------------------------------------------
@@ -272,7 +293,7 @@ export default async function (pi: ExtensionAPI) {
272
293
  ...params,
273
294
  ...(params.kind === "task" ? { project_root: params.project_root ?? ctx.cwd } : {}),
274
295
  });
275
- return text(`Created ${a.id} [${a.kind}|${a.status}] ${a.title}`, createArtifactDetails("artifact.create", a));
296
+ return text(`Created ${artifactTextLabel(a)}`, createArtifactDetails("artifact.create", a));
276
297
  } catch (e) {
277
298
  throw new Error(`papyrus_create failed: ${e instanceof Error ? e.message : e}`);
278
299
  }
@@ -295,7 +316,7 @@ export default async function (pi: ExtensionAPI) {
295
316
  try {
296
317
  const rows = await callService<Record<string, unknown>, Artifact[]>("artifact.query", { ...params, limit: params.limit ?? 50 });
297
318
  if (rows.length === 0) return text("No artifacts found.", createArtifactListDetails("artifact.query", rows));
298
- const lines = rows.map((row, index) => `${index + 1}. ${row.id} [${row.kind}|${row.status}] ${row.title}`);
319
+ const lines = artifactTextLines(rows).map((line, index) => `${index + 1}. ${line}`);
299
320
  return text(`${rows.length} artifact(s):\n\n${lines.join("\n")}`, createArtifactListDetails("artifact.query", rows));
300
321
  } catch (e) {
301
322
  throw new Error(`papyrus_query failed: ${e instanceof Error ? e.message : e}`);
@@ -332,12 +353,15 @@ export default async function (pi: ExtensionAPI) {
332
353
  try {
333
354
  if (params.action === "link") {
334
355
  await callService("graph.link", { from: params.from!, relation: params.relation!, to: params.to! });
335
- const output = `Linked ${params.from} --${params.relation}--> ${params.to}`;
356
+ const names = await artifactNamesById([params.from!, params.to!]);
357
+ const output = `Linked "${names.get(params.from!) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to!) ?? "unknown artifact"}"`;
336
358
  return text(output, createPreviewDetails("graph.link", "Artifact relationship", output));
337
359
  }
338
360
  if (params.action === "unlink") {
339
361
  const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: params.from!, relation: params.relation!, to: params.to! });
340
- const output = result.removed ? `Unlinked ${params.from} --${params.relation}--> ${params.to}` : `No such relationship: ${params.from} --${params.relation}--> ${params.to}`;
362
+ const names = await artifactNamesById([params.from!, params.to!]);
363
+ const relationship = `"${names.get(params.from!) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to!) ?? "unknown artifact"}"`;
364
+ const output = result.removed ? `Unlinked ${relationship}` : `No such relationship: ${relationship}`;
341
365
  return text(output, createPreviewDetails("graph.unlink", "Artifact relationship", output));
342
366
  }
343
367
  if (params.action === "tree") {
@@ -351,22 +375,25 @@ export default async function (pi: ExtensionAPI) {
351
375
  if (!a) throw new Error(`artifact ${root} not found`);
352
376
  const edges = a.edges ?? [];
353
377
  if (edges.length === 0) return text(`${a.title} — no edges`, createGraphDetails("graph.tree", [a], []));
378
+ const names = await artifactNamesById(edges.flatMap((edge) => [edge.from, edge.to]));
354
379
  return text(
355
- `Subgraph from ${a.title} (${edges.length} edges):\n\n${edges.map((edge: any) => ` ${edge.from} --${edge.relation}--> ${edge.to}`).join("\n")}`,
380
+ `Subgraph from ${a.title} (${edges.length} edges):\n\n${edges.map((edge) => ` "${names.get(edge.from) ?? "unknown artifact"}" --${edge.relation}--> "${names.get(edge.to) ?? "unknown artifact"}"`).join("\n")}`,
356
381
  createGraphDetails("graph.tree", [a], edges),
357
382
  );
358
383
  }
359
384
  if (params.action === "status") {
360
385
  const a = await callService<Record<string, unknown>, Artifact | null>("graph.status", { id: params.id!, status: params.status! });
361
386
  if (!a) throw new Error(`artifact ${params.id} not found`);
362
- return text(`Updated ${a.id} → [${a.status}]`, createArtifactDetails("graph.status", a));
387
+ return text(`Updated "${a.title}" → [${a.status}]`, createArtifactDetails("graph.status", a));
363
388
  }
364
389
  if (params.action === "history") {
365
390
  const page = await callService<Record<string, unknown>, { events: Array<Record<string, unknown>> }>("graph.history", {
366
391
  id: params.id, actor: params.actor, session_id: params.session_id, since: params.since, limit: params.limit,
367
392
  });
368
393
  if (page.events.length === 0) return text("No recorded events.", createPreviewDetails("graph.history", "Mutation event log", "No recorded events."));
369
- const output = page.events.map((event) => `${event["occurredAt"]} ${event["artifactId"]} ${event["type"]} · ${event["actor"]}/${event["source"]}`).join("\n");
394
+ const eventIds = page.events.map((event) => event["artifactId"]).filter((id): id is string => typeof id === "string");
395
+ const names = await artifactNamesById(eventIds);
396
+ const output = page.events.map((event) => `${event["occurredAt"]} "${typeof event["artifactId"] === "string" ? names.get(event["artifactId"]) ?? "unknown artifact" : "unknown artifact"}" ${event["type"]} · ${event["actor"]}/${event["source"]}`).join("\n");
370
397
  return text(output, createPreviewDetails("graph.history", "Mutation event log", output));
371
398
  }
372
399
  throw new Error(`unknown action: ${params.action}; use link, tree, status, or history`);
@@ -397,12 +424,13 @@ export default async function (pi: ExtensionAPI) {
397
424
  max_nodes: params.max_nodes,
398
425
  });
399
426
  if (!a) throw new Error(`artifact ${params.id} not found`);
400
- let out = `${a.id} [${a.kind}|${a.status}]\n${a.title}\n\n${a.body}`;
427
+ let out = `${artifactTextLabel(a)}\n\n${a.body}`;
401
428
  if (Object.keys(a.extra).length > 0) {
402
429
  out += `\n\nMetadata:\n${formatMetadata(a.extra).map((line) => ` ${line}`).join("\n")}`;
403
430
  }
404
431
  if (a.edges?.length) {
405
- out += `\n\nEdges:\n${a.edges.map((edge) => ` ${edge.from} --${edge.relation}--> ${edge.to}`).join("\n")}`;
432
+ const names = await artifactNamesById(a.edges.flatMap((edge) => [edge.from, edge.to]));
433
+ out += `\n\nEdges:\n${a.edges.map((edge) => ` "${names.get(edge.from) ?? "unknown artifact"}" --${edge.relation}--> "${names.get(edge.to) ?? "unknown artifact"}"`).join("\n")}`;
406
434
  }
407
435
  if (params.run_gates) {
408
436
  const results = await callService<Record<string, unknown>, GateResult[]>("gates.run", { id: params.id });
@@ -86,12 +86,12 @@ export async function showPlaybooks(ctx: ExtensionCommandContext): Promise<void>
86
86
  const relation = await commandCtx.ui.select("Relation", PLAYBOOK_RELATIONS);
87
87
  if (!relation) return;
88
88
  await callService("graph.link", { from: playbook.id, relation, to: targetId });
89
- commandCtx.ui.notify(`Linked ${playbook.id} --${relation}--> ${targetId}`, "info");
89
+ commandCtx.ui.notify(`Linked "${playbook.title}" via ${relation}`, "info");
90
90
  return;
91
91
  }
92
92
  const operation = choice === "Disable" ? "playbooks.disable" : "playbooks.enable";
93
93
  const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: playbook.id });
94
- commandCtx.ui.notify(`${updated.id} \u2192 [${updated.status}]`, "info");
94
+ commandCtx.ui.notify(`${updated.title} \u2192 [${updated.status}]`, "info");
95
95
  },
96
96
  });
97
97
  }
@@ -44,7 +44,7 @@ export async function showRules(ctx: ExtensionCommandContext): Promise<void> {
44
44
  } else {
45
45
  const operation = choice === "Disable" ? "rules.disable" : "rules.enable";
46
46
  const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: rule.id });
47
- commandCtx.ui.notify(`${updated.id} → [${updated.status}]`, "info");
47
+ commandCtx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
48
48
  }
49
49
  },
50
50
  });
@@ -32,11 +32,11 @@ export function skillRowMeta(skill: Artifact): string {
32
32
 
33
33
  export function skillInvocationPrompt(skill: Artifact): string {
34
34
  if (skill.subtype === "artifact-template") {
35
- return [`Create an artifact using Papyrus template \"${skill.title}\".`, `template_id: ${skill.id}`, "Ask for or infer the title and all required template fields, then call papyrus_create."].join("\n");
35
+ return [`Create an artifact using Papyrus template \"${skill.title}\".`, `template_name: ${skill.title}`, "Ask for or infer the title and all required template fields, then call the skills domain tool with action=instantiate."].join("\n");
36
36
  }
37
37
  if (skill.subtype === "workflow") {
38
38
  return [
39
- `Run Papyrus workflow Skill \"${skill.title}\" (${skill.id}).`,
39
+ `Run Papyrus workflow Skill \"${skill.title}\".`,
40
40
  "Collect its required arguments, then call the skills domain tool with action=run.",
41
41
  ].join("\n");
42
42
  }
@@ -44,7 +44,7 @@ export function skillInvocationPrompt(skill: Artifact): string {
44
44
  const steps = strings(skill.extra["steps"]);
45
45
  const tools = strings(skill.extra["tools"]);
46
46
  return [
47
- `Apply Papyrus skill \"${skill.title}\" (${skill.id}).`,
47
+ `Apply Papyrus skill \"${skill.title}\".`,
48
48
  `Trigger: ${trigger}`,
49
49
  ...(skill.body ? [`Context: ${skill.body}`] : []),
50
50
  ...(steps.length > 0 ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
@@ -120,7 +120,7 @@ export async function showSkills(ctx: ExtensionCommandContext): Promise<void> {
120
120
  } else {
121
121
  const operation = choice === "Disable" ? "skills.disable" : "skills.enable";
122
122
  const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: skill.id });
123
- commandCtx.ui.notify(`${updated.id} → [${updated.status}]`, "info");
123
+ commandCtx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
124
124
  }
125
125
  },
126
126
  });
@@ -32,6 +32,12 @@ const STATUS_ACTIONS: Record<string, string[]> = {
32
32
 
33
33
  type TaskRow = Artifact;
34
34
 
35
+ function taskChoiceLabels(tasks: readonly Artifact[]): string[] {
36
+ const titleCounts = new Map<string, number>();
37
+ for (const task of tasks) titleCounts.set(task.title, (titleCounts.get(task.title) ?? 0) + 1);
38
+ return tasks.map((task) => titleCounts.get(task.title)! > 1 ? `${task.title} (${task.id})` : task.title);
39
+ }
40
+
35
41
  export interface TaskHierarchyRow {
36
42
  task: TaskRow;
37
43
  depth: number;
@@ -101,9 +107,10 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
101
107
  if (scope === "graph") {
102
108
  const projectGraph = await loadTaskGraph(ctx.cwd, sessionId, "project");
103
109
  const roots = projectGraph.rootIds.map((id) => projectGraph.nodes.find((node) => node.task.id === id)?.task).filter((task): task is Artifact => task !== undefined);
104
- const selected = await ctx.ui.select("Focused root or epic", roots.map((task) => `${task.title} · ${task.id}`));
110
+ const rootLabels = taskChoiceLabels(roots);
111
+ const selected = await ctx.ui.select("Focused root or epic", rootLabels);
105
112
  if (!selected) continue;
106
- rootTaskId = roots.find((task) => `${task.title} · ${task.id}` === selected)?.id;
113
+ rootTaskId = roots[rootLabels.indexOf(selected)]?.id;
107
114
  if (!rootTaskId) continue;
108
115
  }
109
116
  await callService("tasks.set_scope", { project_root: ctx.cwd, scope, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) });
@@ -131,17 +138,20 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
131
138
 
132
139
  if (choice === "Remove dependency" || choice === "Remove from parent") {
133
140
  const relatedIds = choice === "Remove dependency" ? node!.dependencyIds : node!.parentIds;
134
- const relatedTitles = relatedIds.map((relatedId) => `${graph.nodes.find((entry) => entry.task.id === relatedId)?.task.title ?? relatedId} · ${relatedId}`);
141
+ const relatedTasks = relatedIds.map((relatedId) => graph.nodes.find((entry) => entry.task.id === relatedId)?.task).filter((task): task is Artifact => task !== undefined);
142
+ const relatedTitles = taskChoiceLabels(relatedTasks);
135
143
  const selected = await ctx.ui.select(choice === "Remove dependency" ? "Remove which dependency?" : "Remove from which parent?", relatedTitles);
136
144
  if (!selected) continue;
137
- const relatedId = relatedIds[relatedTitles.indexOf(selected)]!;
145
+ const relatedTask = relatedTasks[relatedTitles.indexOf(selected)];
146
+ if (!relatedTask) continue;
147
+ const relatedId = relatedTask.id;
138
148
  try {
139
149
  if (choice === "Remove dependency") {
140
150
  await callService("tasks.undepend", { id: action.row.id, dependency_id: relatedId, actor: "user", source: "tasks-tui", session_id: sessionId });
141
- ctx.ui.notify(`Removed dependency on ${relatedId}`, "info");
151
+ ctx.ui.notify(`Removed dependency on ${relatedTask.title}`, "info");
142
152
  } else {
143
153
  await callService("tasks.uncontain", { parent_id: relatedId, child_id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId });
144
- ctx.ui.notify(`Removed from parent ${relatedId}`, "info");
154
+ ctx.ui.notify(`Removed from parent ${relatedTask.title}`, "info");
145
155
  }
146
156
  } catch (error) {
147
157
  ctx.ui.notify(`Relationship removal failed: ${error instanceof Error ? error.message : error}`, "error");
@@ -222,19 +232,20 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
222
232
  const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target}`).join("\n");
223
233
  const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}`).join("\n");
224
234
  const focused = result.focused ? `\nActive: ${result.focused.title}` : "";
235
+ const taskById = new Map(graph.nodes.map((entry) => [entry.task.id, entry.task]));
225
236
  const blocked = result.blocked.length > 0
226
- ? `\nWaiting: ${result.blocked.map((entry) => `${entry.artifact.title} needs ${entry.dependencyIds.join(", ")}`).join("; ")}`
237
+ ? `\nWaiting: ${result.blocked.map((entry) => `${entry.artifact.title} needs ${entry.dependencyIds.map((id) => taskById.get(id)?.title ?? "unknown task").join(", ")}`).join("; ")}`
227
238
  : "";
228
239
  ctx.ui.notify(
229
240
  result.completed
230
- ? `Completed ${result.artifact.id}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`
241
+ ? `Completed ${result.artifact.title}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`
231
242
  : `Review rejected${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`,
232
243
  result.completed ? "info" : "warning",
233
244
  );
234
245
  } else {
235
246
  const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: action.row.id, actor: "user", source: "tasks-tui", session_id: sessionId });
236
247
  action.row.status = updated.status;
237
- ctx.ui.notify(`${updated.id} → [${updated.status}]`, "info");
248
+ ctx.ui.notify(`${updated.title} → [${updated.status}]`, "info");
238
249
  }
239
250
  } catch (error) {
240
251
  ctx.ui.notify(`Task action failed: ${error instanceof Error ? error.message : error}`, "error");
@@ -85,7 +85,7 @@ export class ArtifactCard implements Component {
85
85
  const status = `${statusGlyph(artifact.status)} ${artifact.status}`;
86
86
  const header = [
87
87
  this.theme.fg("toolTitle", this.theme.bold(`${kindGlyph(artifact.kind)} ${artifact.kind.toUpperCase()}`)),
88
- this.theme.fg("accent", artifact.id),
88
+ ...(this.expanded ? [this.theme.fg("accent", artifact.id)] : []),
89
89
  this.theme.fg(statusColor(artifact.status), status),
90
90
  ].join(" ");
91
91
  const lines = [truncateToWidth(header, safeWidth)];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -443,7 +443,7 @@ export function updateSkill(artifacts: ArtifactStore, id: string, input: UpdateS
443
443
 
444
444
  function skillInvocationBody(skill: Artifact): string {
445
445
  if (skill.subtype === "artifact-template") {
446
- return `Create an artifact using Papyrus template "${skill.title}".\ntemplate_id: ${skill.id}\nAsk for or infer all required template fields, then call the skills domain tool instantiate action.`;
446
+ return `Create an artifact using Papyrus template "${skill.title}".\ntemplate_name: ${skill.title}\nAsk for or infer all required template fields, then call the skills domain tool instantiate action.`;
447
447
  }
448
448
  if (skill.subtype === "workflow") {
449
449
  const definition = validateSkillDefinition(skill.extra["definition"]);
@@ -451,7 +451,7 @@ function skillInvocationBody(skill: Artifact): string {
451
451
  .filter(([, input]) => input.required && input.default === undefined)
452
452
  .map(([name]) => name);
453
453
  return [
454
- `Run Papyrus workflow Skill "${skill.title}" (${skill.id}).`,
454
+ `Run Papyrus workflow Skill "${skill.title}".`,
455
455
  `Required arguments: ${required.length > 0 ? required.join(", ") : "none"}.`,
456
456
  "Call the skills domain tool with action=run and arguments after collecting required values.",
457
457
  ].join("\n");
@@ -460,7 +460,7 @@ function skillInvocationBody(skill: Artifact): string {
460
460
  const steps = Array.isArray(skill.extra["steps"]) ? skill.extra["steps"].filter((step): step is string => typeof step === "string") : [];
461
461
  const tools = Array.isArray(skill.extra["tools"]) ? skill.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
462
462
  return [
463
- `Apply Papyrus skill "${skill.title}" (${skill.id}).`,
463
+ `Apply Papyrus skill "${skill.title}".`,
464
464
  `Trigger: ${trigger}`,
465
465
  ...(skill.body ? [`Context: ${skill.body}`] : []),
466
466
  ...(steps.length ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
@@ -492,16 +492,16 @@ export function skillInvocation(artifacts: ArtifactStore, id: string, visited: S
492
492
  const target = artifacts.get(edge.to);
493
493
  if (!target) continue; // dangling edge -- defensive, should not happen
494
494
  if (target.kind !== "skill") {
495
- linkedArtifactLines.push(`- ${edge.relation} ${target.kind} "${target.title}" (${target.id})`);
495
+ linkedArtifactLines.push(`- ${edge.relation} ${target.kind} "${target.title}"`);
496
496
  continue;
497
497
  }
498
498
  if (visited.has(target.id)) {
499
- linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" (${target.id}) -- already invoked above in this chain, not repeated.`);
499
+ linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" -- already invoked above in this chain, not repeated.`);
500
500
  } else if (depth + 1 > SKILL_INVOCATION_MAX_CALL_DEPTH) {
501
- linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" (${target.id}) -- call depth limit reached, invoke it separately.`);
501
+ linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" -- call depth limit reached, invoke it separately.`);
502
502
  } else {
503
503
  const nested = skillInvocation(artifacts, target.id, visited, depth + 1);
504
- linkedSkillSections.push(`Also invoke linked skill (${edge.relation}) "${target.title}" (${target.id}):\n${nested}`);
504
+ linkedSkillSections.push(`Also invoke linked skill (${edge.relation}) "${target.title}":\n${nested}`);
505
505
  }
506
506
  }
507
507
  if (linkedArtifactLines.length > 0) {
@@ -647,7 +647,7 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
647
647
  });
648
648
  const missingRequired = declaredArguments.filter((argument) => argument.required && provided[argument.name] === undefined);
649
649
  const sections = [[
650
- `Apply Papyrus playbook "${playbook.title}" (${playbook.id}).`,
650
+ `Apply Papyrus playbook "${playbook.title}".`,
651
651
  `Trigger: ${trigger}`,
652
652
  ...(playbook.body ? [`Context: ${playbook.body}`] : []),
653
653
  ...(argumentLines.length > 0 ? ["Arguments:", ...argumentLines] : []),
@@ -659,7 +659,7 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
659
659
  ].join("\n")];
660
660
  const edges = artifacts.relationships({ artifactIds: [id] }).filter((edge) => edge.from === id).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
661
661
  const linkedLines = edges
662
- .map((edge) => { const target = artifacts.get(edge.to); return target ? `- ${edge.relation} ${target.kind} "${target.title}" (${target.id})` : undefined; })
662
+ .map((edge) => { const target = artifacts.get(edge.to); return target ? `- ${edge.relation} ${target.kind} "${target.title}"` : undefined; })
663
663
  .filter((line): line is string => line !== undefined);
664
664
  if (linkedLines.length > 0) sections.push(["Linked context (query Papyrus for full detail before proceeding):", ...linkedLines].join("\n"));
665
665
  return sections.join("\n\n");
@@ -29,7 +29,7 @@ function renderCurrent(task: Artifact): string[] {
29
29
  const desired = task.body.trim() || task.title;
30
30
  const gates = gatesFrom(task);
31
31
  return [
32
- `Current: ${task.title} (${task.id})`,
32
+ `Current: ${task.title}`,
33
33
  `Desired: ${desired}`,
34
34
  `Verify: ${gates.length > 0 ? gates.map(renderGate).join("; ") : "inspect the desired outcome; no automated gates configured"}`,
35
35
  ];
@@ -56,7 +56,7 @@ function deferredBlockingDiscussions(artifacts: ArtifactStore, activeTaskId: str
56
56
  if (!inScope(edge.to, activeTaskId, taskIds)) continue;
57
57
  const blockedTask = artifacts.get(edge.to);
58
58
  if (!blockedTask || blockedTask.status === "done" || blockedTask.status === "canceled") continue;
59
- lines.push(`${discussion.title} (${discussion.id}) -- blocks "${blockedTask.title}"`);
59
+ lines.push(`${discussion.title} -- blocks "${blockedTask.title}"`);
60
60
  }
61
61
  }
62
62
  return lines;
@@ -77,8 +77,8 @@ export function taskContext(artifacts: ArtifactStore, activeTaskId?: string, tas
77
77
  const rejected = open.filter((task) => task.status === "rejected").slice(0, TASK_CONTEXT_REJECTED_LIMIT);
78
78
  const lines = tasks.length > 0 ? [`Progress: ${done}/${tasks.length} done`] : [];
79
79
  for (const task of current) lines.push(...renderCurrent(task));
80
- if (next) lines.push(`Next: ${next.title} (${next.id})`);
81
- if (rejected.length > 0) lines.push(`Rejected: ${rejected.map((task) => `${task.title} (${task.id})`).join(", ")}`);
80
+ if (next) lines.push(`Next: ${next.title}`);
81
+ if (rejected.length > 0) lines.push(`Rejected: ${rejected.map((task) => task.title).join(", ")}`);
82
82
  if (deferredDiscussions.length > 0) {
83
83
  lines.push("", "Deferred discussions blocking this scope -- resume and re-surface these, do not leave them dormant:");
84
84
  for (const line of deferredDiscussions) lines.push(`• ${line}`);