@danypops/papyrus 0.11.4 → 0.12.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.
Files changed (46) hide show
  1. package/README.md +16 -2
  2. package/extension/src/active-task-continuation.ts +6 -0
  3. package/extension/src/domain-tools.ts +108 -52
  4. package/extension/src/index.ts +90 -37
  5. package/extension/src/notes.ts +14 -1
  6. package/extension/src/task-focus-events.ts +57 -0
  7. package/extension/src/tasks.ts +51 -15
  8. package/extension/src/tool-rendering/artifact-card.ts +117 -0
  9. package/extension/src/tool-rendering/artifact-list.ts +179 -0
  10. package/extension/src/tool-rendering/index.ts +107 -0
  11. package/extension/src/tool-rendering/render-model.ts +406 -0
  12. package/package.json +4 -2
  13. package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
  14. package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
  15. package/src/adapters/sqlite-artifact-store.ts +20 -11
  16. package/src/adapters/sqlite-discourse-store.ts +325 -0
  17. package/src/adapters/sqlite-graph-projection-store.ts +41 -0
  18. package/src/adapters/sqlite-task-focus-store.ts +34 -15
  19. package/src/authority-registry.ts +115 -0
  20. package/src/cli.ts +904 -124
  21. package/src/constants.ts +38 -5
  22. package/src/conversation-journal-service.ts +87 -0
  23. package/src/db.ts +285 -33
  24. package/src/domain/artifact-event.ts +99 -0
  25. package/src/domain/conversation-journal.ts +168 -0
  26. package/src/domain/discourse-store.ts +142 -0
  27. package/src/domain/graph-projection.ts +74 -0
  28. package/src/domain/task-event.ts +4 -0
  29. package/src/domain-services.ts +133 -38
  30. package/src/graph-projection-service.ts +103 -0
  31. package/src/id-migration.ts +200 -0
  32. package/src/module-registry.ts +53 -0
  33. package/src/modules/docs.ts +77 -0
  34. package/src/modules/graph-projection.ts +82 -0
  35. package/src/modules/notes.ts +76 -0
  36. package/src/modules/rules.ts +81 -0
  37. package/src/modules/skills.ts +113 -0
  38. package/src/modules/tasks.ts +164 -0
  39. package/src/ops.ts +142 -15
  40. package/src/ports/artifact-scope-store.ts +20 -0
  41. package/src/ports/artifact-store.ts +10 -5
  42. package/src/ports/conversation-journal-store.ts +17 -0
  43. package/src/ports/graph-projection-store.ts +15 -0
  44. package/src/ports/task-focus-store.ts +62 -20
  45. package/src/service.ts +218 -223
  46. package/src/task-service.ts +70 -38
package/README.md CHANGED
@@ -56,6 +56,20 @@ It blocks every Papyrus push whose destination is not `DanyPops/papyrus`, includ
56
56
 
57
57
  The daemon uses WAL, foreign keys, a bounded busy timeout, versioned migrations, periodic passive checkpoints, and periodic `PRAGMA optimize`. Keep the database on a local filesystem; SQLite WAL does not support network filesystems.
58
58
 
59
+ ### Context Mesh persistence model
60
+
61
+ `artifacts` is the shared graph-identity supertype, not a second copy of every application's database. `edges` references that single identity table at both endpoints, preserving foreign-key integrity for cross-domain links. Domain extension tables exist only where application invariants require indexed relational state: Task chronology/focus/scope and Discourse posts/events/session cursors/projection checkpoints. This is a class-table/table-per-type variant with explicit child-to-parent foreign keys; Papyrus does not use SQLite table inheritance or orphan-prone `(target_type, target_id)` links.
62
+
63
+ The owning application remains the mutation authority. Discourse commits its extension rows and `context-thread`/`context-message` Doc projections atomically through `discourse.store`; generic artifact, document, Skill-template, lifecycle, and graph-link operations reject those owned subtypes and the `reply_to`/`discusses` relations. SQLite triggers additionally verify that each extension row references the expected Doc subtype. Domain tables are canonical for domain invariants; graph bodies and metadata are read-oriented projections committed in the same transaction.
64
+
65
+ The authenticated CLI exposes the same operation for diagnostics and adapter parity:
66
+
67
+ ```bash
68
+ papyrus discourse store read_thread --store-id team-forum \
69
+ --input-json '{"forumId":"engineering","topicId":"reviews","threadId":"mesh","limit":25}' \
70
+ --json
71
+ ```
72
+
59
73
  ## Schema protocol (enforceable)
60
74
 
61
75
  Papyrus enforces four artifact kinds:
@@ -217,10 +231,10 @@ packed install npm:@danypops/papyrus
217
231
  ~/.pi/agent/npm/node_modules/.bin/papyrus service install
218
232
  ```
219
233
 
220
- Existing databases are never migrated on daemon boot. After upgrading to focus-driven Task continuation, run the authenticated CLI migration explicitly. Older databases receive prerequisite schemas in one transaction. Existing Tasks are deliberately marked **unscoped**: Papyrus does not guess ownership from titles, labels, historical cwd, or repository names. They remain visible in **All projects** until explicitly assigned with `papyrus tasks assign-project <task-id> [project-root]`:
234
+ Existing databases are never migrated on daemon boot. After upgrading to a newer schema, run the authenticated CLI migration explicitly. Older databases receive prerequisite schemas—including Task continuation and Context Mesh extensions—in one transaction. Existing Tasks are deliberately marked **unscoped**: Papyrus does not guess ownership from titles, labels, historical cwd, or repository names. They remain visible in **All projects** until explicitly assigned with `papyrus tasks assign-project <task-id> [project-root]`:
221
235
 
222
236
  ```bash
223
- ~/.pi/agent/npm/node_modules/.bin/papyrus migrate task-focus
237
+ ~/.pi/agent/npm/node_modules/.bin/papyrus migrate schema
224
238
  ```
225
239
 
226
240
  Until that command succeeds, health reports `migrationRequired` and normal domain operations are rejected with actionable guidance. Migration is not exposed as a Pi tool or MCP action. New empty databases bootstrap directly at the current schema.
@@ -41,6 +41,8 @@ function continuationPrompt(task: ActiveTaskMarker): string {
41
41
  return [
42
42
  "Continue the active Papyrus Task now; do not hand off merely because the previous Pi run settled.",
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
+ "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
+ "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.",
44
46
  `Active task: ${task.id}: ${task.title.slice(0, TITLE_LIMIT)}`,
45
47
  ].join("\n");
46
48
  }
@@ -97,6 +99,10 @@ export class ActiveTaskContinuation {
97
99
  this.queued = false;
98
100
  }
99
101
 
102
+ onCompaction(): void {
103
+ this.queued = false;
104
+ }
105
+
100
106
  onHumanInput(): void {
101
107
  this.resetProgress();
102
108
  }
@@ -7,11 +7,23 @@ import type { TaskExecutionPlan } from "../../src/task-execution.ts";
7
7
  import type { TaskHistoryPage } from "../../src/domain/task-event.ts";
8
8
  import type { TaskCompletion, TaskGraph } from "../../src/task-service.ts";
9
9
  import type { SkillWorkflowRunResult } from "../../src/skill-execution.ts";
10
+ import { emitTaskFocusEvent } from "./task-focus-events.ts";
10
11
  import { NOTE_DISPOSITIONS } from "../../src/note-service.ts";
11
12
  import { callService } from "./service-client.ts";
13
+ import { renderPapyrusToolCall, renderPapyrusToolResult } from "./tool-rendering/index.ts";
14
+ import {
15
+ createArtifactDetails,
16
+ createArtifactListDetails,
17
+ createGateRunDetails,
18
+ createGraphDetails,
19
+ createInvocationDetails,
20
+ createModelContent,
21
+ createPreviewDetails,
22
+ } from "./tool-rendering/render-model.ts";
12
23
 
13
- function text(message: string, details: Record<string, unknown> = {}) {
14
- return { content: [{ type: "text" as const, text: message }], details };
24
+ function text(message: string, details: unknown = {}) {
25
+ const modelContent = createModelContent(message);
26
+ return { content: [{ type: "text" as const, text: modelContent.text }], details };
15
27
  }
16
28
 
17
29
  function artifactLine(artifact: Artifact): string {
@@ -32,7 +44,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
32
44
  pi.registerTool({
33
45
  name: "tasks",
34
46
  label: "Tasks",
35
- description: "Task domain tool. ACTIONS: create, update, list, show, history, scope, set_scope, assign_project, graph, plan, active, focused, focus, pause, unpause, clear_focus, start, submit, complete, reject, retry, cancel, run_gates, set_checklist, depend, contain. Lifecycle is todo → in-progress → review → done, with review failure → rejected and retry → in-progress; canceled is terminal. update can recover a Task accidentally created terminal by setting status=todo with a reason, but cannot rewrite legitimate lifecycle history. Active focus is independent and identifies the one task auto-drive continues. Completion runs gates and checklist-proof review, then focuses one deterministic ready successor without claiming effort. Dependency cycles are rejected. Prefer this over low-level papyrus_* tools for task work.",
47
+ description: "Task domain tool. ACTIONS: create, update, list, show, history, scope, set_scope, assign_project, graph, plan, active, focused, focus, pause, unpause, clear_focus, start, submit, complete, reject, retry, cancel, run_gates, set_checklist, depend, undepend, contain, uncontain. Lifecycle is todo → in-progress → review → done, with review failure → rejected and retry → in-progress; canceled is terminal. update can recover a Task accidentally created terminal by setting status=todo with a reason, but cannot rewrite legitimate lifecycle history. Active focus is independent and identifies the one task auto-drive continues. Completion runs gates and checklist-proof review, then focuses one deterministic ready successor without claiming effort. Dependency cycles are rejected. undepend/uncontain are idempotent for an already-absent relationship and never start, complete, or focus work merely because an edge disappeared; uncontain removes both contains and part_of edges atomically. Prefer this over low-level papyrus_* tools for task work.",
36
48
  parameters: Type.Object({
37
49
  action: Type.String(),
38
50
  id: Type.Optional(Type.String()),
@@ -58,53 +70,72 @@ export function registerDomainTools(pi: ExtensionAPI): void {
58
70
  scope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("graph"), Type.Literal("all")])),
59
71
  root_task_id: Type.Optional(Type.String()),
60
72
  }),
73
+ renderCall(args, theme) { return renderPapyrusToolCall("Tasks", args, theme); },
74
+ renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
61
75
  async execute(_id, params, _signal, _onUpdate, ctx) {
62
76
  try {
63
77
  const action = params.action;
64
- const request = { ...params, project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "pi-tool" };
78
+ // Defaults to this Pi session's own id so Focus reads/writes are isolated per agent
79
+ // without depending on the model to know or supply its own session identity.
80
+ const request = { ...params, project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "pi-tool", session_id: params.session_id ?? ctx.sessionManager.getSessionId() };
65
81
  if (action === "create") {
66
82
  const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create", request);
67
- return text(`Created task ${artifactLine(artifact)}`, { artifact });
83
+ return text(`Created task ${artifactLine(artifact)}`, createArtifactDetails("tasks.create", artifact));
68
84
  }
69
85
  if (action === "list") {
70
86
  const rows = await callService<Record<string, unknown>, Artifact[]>("tasks.list", request);
71
- return text(rows.length ? rows.map(artifactLine).join("\n") : "No tasks found.", { rows });
87
+ return text(rows.length ? rows.map(artifactLine).join("\n") : "No tasks found.", createArtifactListDetails("tasks.list", rows));
72
88
  }
73
89
  if (action === "show") {
74
90
  const artifact = await callService<Record<string, unknown>, Artifact>("tasks.show", params);
75
- return text(`${artifactLine(artifact)}\n\n${artifact.body}`, { artifact });
91
+ return text(`${artifactLine(artifact)}\n\n${artifact.body}`, createArtifactDetails("tasks.show", artifact));
76
92
  }
77
93
  if (action === "history") {
78
94
  const page = await callService<Record<string, unknown>, TaskHistoryPage>("tasks.history", request);
79
95
  const lines = page.events.map((event) => `${event.occurredAt} ${event.type} ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"} · ${event.actor}/${event.source}${event.reason ? ` · ${event.reason}` : ""}`);
80
- return text(lines.join("\n") || "No recorded history for this task.", { page });
96
+ const output = lines.join("\n") || "No recorded history for this task.";
97
+ return text(output, createPreviewDetails("tasks.history", "Task history", output));
81
98
  }
82
99
  if (action === "scope") {
83
100
  const selection = await callService<Record<string, unknown>, import("../../src/domain/task-scope.ts").TaskViewSelection>("tasks.scope", request);
84
- return text(`Task scope: ${selection.label}`, { selection });
101
+ return text(`Task scope: ${selection.label}`, createPreviewDetails("tasks.scope", "Task scope", selection.label));
85
102
  }
86
103
  if (action === "active") {
87
104
  const artifact = await callService<Record<string, unknown>, Artifact | null>("tasks.active", request);
88
- return text(artifact ? `Active: ${artifactLine(artifact)}` : "No active task.", { artifact });
105
+ return artifact
106
+ ? text(`Active: ${artifactLine(artifact)}`, createArtifactDetails("tasks.active", artifact))
107
+ : text("No active task.", createPreviewDetails("tasks.active", "Active task", "No active task."));
89
108
  }
90
109
  if (action === "focused") {
91
110
  const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string } | null>("tasks.focused", request);
92
- return text(focus ? `Focused (${focus.status}): ${artifactLine(focus.artifact)}` : "No focused task.", { focus });
111
+ return focus
112
+ ? text(`Focused (${focus.status}): ${artifactLine(focus.artifact)}`, createArtifactDetails("tasks.focused", focus.artifact))
113
+ : text("No focused task.", createPreviewDetails("tasks.focused", "Focused task", "No focused task."));
93
114
  }
94
115
  if (action === "pause" || action === "unpause") {
95
116
  const operation = action === "pause" ? "tasks.pause" : "tasks.unpause";
96
117
  const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string }>(operation, request);
97
- return text(`Focused (${focus.status}): ${artifactLine(focus.artifact)}`, { focus });
118
+ emitTaskFocusEvent({ taskId: focus.artifact.id, sessionId: request.session_id as string, status: action === "pause" ? "paused" : "unpaused" });
119
+ return text(`Focused (${focus.status}): ${artifactLine(focus.artifact)}`, createArtifactDetails(operation, focus.artifact));
98
120
  }
99
121
  if (action === "clear_focus") {
100
122
  const result = await callService<Record<string, unknown>, { cleared: boolean }>("tasks.clear_focus", request);
101
- return text(result.cleared ? "Task focus cleared." : "No focused task.", result);
123
+ if (result.cleared) emitTaskFocusEvent({ taskId: null, sessionId: request.session_id as string, status: "cleared" });
124
+ const output = result.cleared ? "Task focus cleared." : "No focused task.";
125
+ return text(output, createPreviewDetails("tasks.clear_focus", "Task focus", output));
102
126
  }
103
127
  if (action === "graph") {
104
128
  const graph = await callService<Record<string, unknown>, TaskGraph>("tasks.graph", request);
105
129
  const dependencies = graph.nodes.reduce((count, node) => count + node.dependencyIds.length, 0);
106
130
  const containment = graph.nodes.reduce((count, node) => count + node.childIds.length, 0);
107
- return text(`Task graph: ${graph.nodes.length} nodes, ${graph.rootIds.length} roots, ${dependencies} dependencies, ${containment} containment edges.`, { graph });
131
+ const edges = graph.nodes.flatMap((node) => [
132
+ ...node.dependencyIds.map((dependencyId) => ({ from: node.task.id, relation: "depends_on", to: dependencyId })),
133
+ ...node.childIds.map((childId) => ({ from: node.task.id, relation: "contains", to: childId })),
134
+ ]);
135
+ return text(
136
+ `Task graph: ${graph.nodes.length} nodes, ${graph.rootIds.length} roots, ${dependencies} dependencies, ${containment} containment edges.`,
137
+ createGraphDetails("tasks.graph", graph.nodes.map((node) => node.task), edges),
138
+ );
108
139
  }
109
140
  if (action === "plan") {
110
141
  const plan = await callService<Record<string, unknown>, TaskExecutionPlan>("tasks.plan", request);
@@ -117,11 +148,12 @@ export function registerDomainTools(pi: ExtensionAPI): void {
117
148
  }),
118
149
  ]);
119
150
  if (plan.cycleIds.length > 0) lines.push(`Invalid cycle: ${plan.cycleIds.join(", ")}`);
120
- return text(lines.join("\n") || "No tasks in execution plan.", { plan });
151
+ const output = lines.join("\n") || "No tasks in execution plan.";
152
+ return text(output, createPreviewDetails("tasks.plan", "Task execution plan", output));
121
153
  }
122
154
  if (action === "set_checklist") {
123
155
  const artifact = await callService<Record<string, unknown>, Artifact>("tasks.set_checklist", params);
124
- return text(`Updated checklist: ${artifactLine(artifact)}`, { artifact });
156
+ return text(`Updated checklist: ${artifactLine(artifact)}`, createArtifactDetails("tasks.set_checklist", artifact));
125
157
  }
126
158
  if (action === "complete") {
127
159
  const result = await callService<Record<string, unknown>, TaskCompletion>("tasks.complete", request);
@@ -131,11 +163,17 @@ export function registerDomainTools(pi: ExtensionAPI): void {
131
163
  const blocked = result.blocked.length > 0
132
164
  ? `\nBlocked: ${result.blocked.map((entry) => `${artifactLine(entry.artifact)} waits for ${entry.dependencyIds.join(", ")}`).join("; ")}`
133
165
  : "";
134
- return text(`${result.completed ? "Completed" : "Rejected"}: ${artifactLine(result.artifact)}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`, { ...result });
166
+ const output = `${result.completed ? "Completed" : "Rejected"}: ${artifactLine(result.artifact)}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`;
167
+ return text(output, createPreviewDetails("tasks.complete", "Task completion", output));
135
168
  }
136
169
  if (action === "run_gates") {
137
170
  const gates = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", request);
138
- return text(gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n") || "No gates configured.", { gates });
171
+ return text(
172
+ gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n") || "No gates configured.",
173
+ createGateRunDetails("tasks.run_gates", params.id ?? "", gates.map((gate) => ({
174
+ passed: gate.passed, type: gate.gate.type, target: gate.gate.target, output: gate.output,
175
+ }))),
176
+ );
139
177
  }
140
178
  const operations = {
141
179
  focus: "tasks.focus",
@@ -148,14 +186,17 @@ export function registerDomainTools(pi: ExtensionAPI): void {
148
186
  set_scope: "tasks.set_scope",
149
187
  assign_project: "tasks.assign_project",
150
188
  depend: "tasks.depend",
189
+ undepend: "tasks.undepend",
151
190
  contain: "tasks.contain",
191
+ uncontain: "tasks.uncontain",
152
192
  } as const;
153
193
  const operation = operations[action as keyof typeof operations];
154
- if (!operation) return text(`Unknown tasks action: ${action}`);
194
+ if (!operation) throw new Error(`unknown tasks action: ${action}`);
155
195
  const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
156
- return text(artifactLine(artifact), { artifact });
196
+ if (operation === "tasks.focus") emitTaskFocusEvent({ taskId: artifact.id, sessionId: request.session_id as string, status: "focused" });
197
+ return text(artifactLine(artifact), createArtifactDetails(operation, artifact));
157
198
  } catch (error) {
158
- return text(`tasks failed: ${error instanceof Error ? error.message : error}`);
199
+ throw new Error(`tasks failed: ${error instanceof Error ? error.message : error}`);
159
200
  }
160
201
  },
161
202
  });
@@ -178,29 +219,31 @@ export function registerDomainTools(pi: ExtensionAPI): void {
178
219
  session_id: Type.Optional(Type.String()),
179
220
  project_root: Type.Optional(Type.String()),
180
221
  }),
222
+ renderCall(args, theme) { return renderPapyrusToolCall("Notes", args, theme); },
223
+ renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
181
224
  async execute(_id, params, _signal, _onUpdate, ctx) {
182
225
  try {
183
226
  const action = params.action;
184
227
  const request = { ...params, project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "notes-tool" };
185
228
  if (action === "capture") {
186
229
  const artifact = await callService<Record<string, unknown>, Artifact>("notes.capture", request);
187
- return text(`Captured note ${artifactLine(artifact)}`, { artifact });
230
+ return text(`Captured note ${artifactLine(artifact)}`, createArtifactDetails("notes.capture", artifact));
188
231
  }
189
232
  if (action === "list") {
190
233
  const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", request);
191
- return text(rows.length ? rows.map(artifactLine).join("\n") : "No open notes.", { rows });
234
+ return text(rows.length ? rows.map(artifactLine).join("\n") : "No open notes.", createArtifactListDetails("notes.list", rows));
192
235
  }
193
236
  if (action === "show") {
194
237
  const artifact = await callService<Record<string, unknown>, Artifact>("notes.show", request);
195
- return text(`${artifactLine(artifact)}\n\n${artifact.body}`, { artifact });
238
+ return text(`${artifactLine(artifact)}\n\n${artifact.body}`, createArtifactDetails("notes.show", artifact));
196
239
  }
197
240
  const operations = { consume: "notes.consume", promote: "notes.promote", archive: "notes.archive" } as const;
198
241
  const operation = operations[action as keyof typeof operations];
199
- if (!operation) return text(`Unknown notes action: ${action}`);
242
+ if (!operation) throw new Error(`unknown notes action: ${action}`);
200
243
  const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
201
- return text(`${action}: ${artifactLine(artifact)}`, { artifact });
244
+ return text(`${action}: ${artifactLine(artifact)}`, createArtifactDetails(operation, artifact));
202
245
  } catch (error) {
203
- return text(`notes failed: ${error instanceof Error ? error.message : error}`);
246
+ throw new Error(`notes failed: ${error instanceof Error ? error.message : error}`);
204
247
  }
205
248
  },
206
249
  });
@@ -208,7 +251,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
208
251
  pi.registerTool({
209
252
  name: "docs",
210
253
  label: "Documents",
211
- description: "Document domain tool. ACTIONS: create, list, show, activate, archive, reopen, link. Prefer this over low-level papyrus_* tools for document work.",
254
+ description: "Document domain tool. ACTIONS: create, list, show, activate, archive, reopen, link, assign_project. project_root is optional at creation (omitted = unscoped); assign_project reassigns it later, or unscopes when project_root is omitted. Prefer this over low-level papyrus_* tools for document work.",
212
255
  parameters: Type.Object({
213
256
  action: Type.String(),
214
257
  id: Type.Optional(Type.String()),
@@ -223,29 +266,32 @@ export function registerDomainTools(pi: ExtensionAPI): void {
223
266
  template_id: Type.Optional(Type.String()),
224
267
  relation: Type.Optional(Type.String()),
225
268
  target_id: Type.Optional(Type.String()),
269
+ project_root: Type.Optional(Type.String()),
226
270
  }),
271
+ renderCall(args, theme) { return renderPapyrusToolCall("Documents", args, theme); },
272
+ renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
227
273
  async execute(_id, params) {
228
274
  try {
229
275
  const action = params.action;
230
276
  if (action === "create") {
231
277
  const artifact = await callService<Record<string, unknown>, Artifact>("docs.create", params);
232
- return text(`Created document ${artifactLine(artifact)}`, { artifact });
278
+ return text(`Created document ${artifactLine(artifact)}`, createArtifactDetails("docs.create", artifact));
233
279
  }
234
280
  if (action === "list") {
235
281
  const rows = await callService<Record<string, unknown>, Artifact[]>("docs.list", params);
236
- return text(rows.length ? rows.map(artifactLine).join("\n") : "No documents found.", { rows });
282
+ return text(rows.length ? rows.map(artifactLine).join("\n") : "No documents found.", createArtifactListDetails("docs.list", rows));
237
283
  }
238
284
  if (action === "show") {
239
285
  const artifact = await callService<Record<string, unknown>, Artifact>("docs.show", params);
240
- return text(`${artifactLine(artifact)}\n\n${artifact.body}`, { artifact });
286
+ return text(`${artifactLine(artifact)}\n\n${artifact.body}`, createArtifactDetails("docs.show", artifact));
241
287
  }
242
- const operations = { activate: "docs.activate", archive: "docs.archive", reopen: "docs.reopen", link: "docs.link" } as const;
288
+ const operations = { activate: "docs.activate", archive: "docs.archive", reopen: "docs.reopen", link: "docs.link", assign_project: "docs.assign_project" } as const;
243
289
  const operation = operations[action as keyof typeof operations];
244
- if (!operation) return text(`Unknown docs action: ${action}`);
290
+ if (!operation) throw new Error(`unknown docs action: ${action}`);
245
291
  const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
246
- return text(artifactLine(artifact), { artifact });
292
+ return text(artifactLine(artifact), createArtifactDetails(operation, artifact));
247
293
  } catch (error) {
248
- return text(`docs failed: ${error instanceof Error ? error.message : error}`);
294
+ throw new Error(`docs failed: ${error instanceof Error ? error.message : error}`);
249
295
  }
250
296
  },
251
297
  });
@@ -253,36 +299,39 @@ export function registerDomainTools(pi: ExtensionAPI): void {
253
299
  pi.registerTool({
254
300
  name: "rules",
255
301
  label: "Rules",
256
- description: "Rule domain tool. ACTIONS: create, list, show, preview, enable, disable, gate. Active rules inject into the agent system prompt.",
302
+ description: "Rule domain tool. ACTIONS: create, list, show, preview, enable, disable, gate, assign_project. 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.",
257
303
  parameters: Type.Object({
258
304
  action: Type.String(), id: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
259
305
  body: Type.Optional(Type.String()), condition: Type.Optional(Type.String()), rule_action: Type.Optional(Type.String()),
260
306
  severity: Type.Optional(Type.String()), labels: Type.Optional(Type.Array(Type.String())),
261
307
  extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
262
308
  text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), task_id: Type.Optional(Type.String()),
309
+ project_root: Type.Optional(Type.String()),
263
310
  }),
311
+ renderCall(args, theme) { return renderPapyrusToolCall("Rules", args, theme); },
312
+ renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
264
313
  async execute(_id, params) {
265
314
  try {
266
315
  const action = params.action;
267
316
  if (action === "create") {
268
317
  const artifact = await callService<Record<string, unknown>, Artifact>("rules.create", params);
269
- return text(`Created rule ${artifactLine(artifact)}`, { artifact });
318
+ return text(`Created rule ${artifactLine(artifact)}`, createArtifactDetails("rules.create", artifact));
270
319
  }
271
320
  if (action === "list") {
272
321
  const rows = await callService<Record<string, unknown>, Artifact[]>("rules.list", params);
273
- return text(rows.length ? rows.map(artifactLine).join("\n") : "No rules found.", { rows });
322
+ return text(rows.length ? rows.map(artifactLine).join("\n") : "No rules found.", createArtifactListDetails("rules.list", rows));
274
323
  }
275
324
  if (action === "preview") {
276
325
  const preview = await callService<Record<string, unknown>, string>("rules.preview", params);
277
- return text(preview, { preview });
326
+ return text(preview, createPreviewDetails("rules.preview", "Rule preview", preview));
278
327
  }
279
- const operations = { show: "rules.show", enable: "rules.enable", disable: "rules.disable", gate: "rules.gate" } as const;
328
+ const operations = { show: "rules.show", enable: "rules.enable", disable: "rules.disable", gate: "rules.gate", assign_project: "rules.assign_project" } as const;
280
329
  const operation = operations[action as keyof typeof operations];
281
- if (!operation) return text(`Unknown rules action: ${action}`);
330
+ if (!operation) throw new Error(`unknown rules action: ${action}`);
282
331
  const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
283
- return text(`${artifactLine(artifact)}${action === "show" ? `\n\n${artifact.body}` : ""}`, { artifact });
332
+ return text(`${artifactLine(artifact)}${action === "show" ? `\n\n${artifact.body}` : ""}`, createArtifactDetails(operation, artifact));
284
333
  } catch (error) {
285
- return text(`rules failed: ${error instanceof Error ? error.message : error}`);
334
+ throw new Error(`rules failed: ${error instanceof Error ? error.message : error}`);
286
335
  }
287
336
  },
288
337
  });
@@ -290,7 +339,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
290
339
  pi.registerTool({
291
340
  name: "skills",
292
341
  label: "Skills",
293
- 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. run validates arguments and atomically creates one scoped workflow run.",
342
+ 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. 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.",
294
343
  parameters: Type.Object({
295
344
  action: Type.String(), id: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
296
345
  body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
@@ -303,6 +352,8 @@ export function registerDomainTools(pi: ExtensionAPI): void {
303
352
  required: Type.Optional(Type.Array(Type.String())), kind: Type.Optional(Type.String()), subtype: Type.Optional(Type.String()),
304
353
  project_root: Type.Optional(Type.String()),
305
354
  }),
355
+ renderCall(args, theme) { return renderPapyrusToolCall("Skills", args, theme); },
356
+ renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
306
357
  async execute(_id, params, _signal, _onUpdate, ctx) {
307
358
  try {
308
359
  const action = params.action;
@@ -310,15 +361,15 @@ export function registerDomainTools(pi: ExtensionAPI): void {
310
361
  if (action === "create" || action === "create_template") {
311
362
  const operation = action === "create" ? "skills.create" : "skills.create_template";
312
363
  const artifact = await callService<Record<string, unknown>, Artifact>(operation, params);
313
- return text(`Created skill ${artifactLine(artifact)}`, { artifact });
364
+ return text(`Created skill ${artifactLine(artifact)}`, createArtifactDetails(operation, artifact));
314
365
  }
315
366
  if (action === "list") {
316
367
  const rows = await callService<Record<string, unknown>, Artifact[]>("skills.list", params);
317
- return text(rows.length ? rows.map(artifactLine).join("\n") : "No skills found.", { rows });
368
+ return text(rows.length ? rows.map(artifactLine).join("\n") : "No skills found.", createArtifactListDetails("skills.list", rows));
318
369
  }
319
370
  if (action === "invoke") {
320
371
  const invocation = await callService<Record<string, unknown>, string>("skills.invoke", params);
321
- return text(invocation, { invocation });
372
+ return text(invocation, createPreviewDetails("skills.invoke", "Skill invocation", invocation));
322
373
  }
323
374
  if (action === "run") {
324
375
  const run = await callService<Record<string, unknown>, SkillWorkflowRunResult>("skills.run", request);
@@ -329,15 +380,20 @@ export function registerDomainTools(pi: ExtensionAPI): void {
329
380
  `Context docs: ${run.created.docs.join(", ") || "none"}.`,
330
381
  `Scoped rules: ${run.created.rules.join(", ") || "none"}.`,
331
382
  ...(execution ? ["Execution:", execution] : []),
332
- ].join("\n"), { run });
383
+ ].join("\n"), createInvocationDetails("skills.run", run.runId, {
384
+ tasks: run.created.tasks,
385
+ docs: run.created.docs,
386
+ rules: run.created.rules,
387
+ roots: run.rootTaskIds,
388
+ }));
333
389
  }
334
- const operations = { show: "skills.show", enable: "skills.enable", disable: "skills.disable", instantiate: "skills.instantiate" } as const;
390
+ const operations = { show: "skills.show", enable: "skills.enable", disable: "skills.disable", instantiate: "skills.instantiate", assign_project: "skills.assign_project" } as const;
335
391
  const operation = operations[action as keyof typeof operations];
336
- if (!operation) return text(`Unknown skills action: ${action}`);
392
+ if (!operation) throw new Error(`unknown skills action: ${action}`);
337
393
  const artifact = await callService<Record<string, unknown>, Artifact>(operation, action === "instantiate" ? request : params);
338
- return text(`${artifactLine(artifact)}${action === "show" ? `\n\n${artifact.body}` : ""}`, { artifact });
394
+ return text(`${artifactLine(artifact)}${action === "show" ? `\n\n${artifact.body}` : ""}`, createArtifactDetails(operation, artifact));
339
395
  } catch (error) {
340
- return text(`skills failed: ${error instanceof Error ? error.message : error}`);
396
+ throw new Error(`skills failed: ${error instanceof Error ? error.message : error}`);
341
397
  }
342
398
  },
343
399
  });