@danypops/papyrus 0.32.1 → 0.33.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.
package/README.md CHANGED
@@ -98,7 +98,7 @@ papyrus skills run <skill-id> \
98
98
  --json
99
99
  ```
100
100
 
101
- The existing `artifact-template` skill subtype remains a compatibility mechanism for one-artifact templates with metadata `{targetKind, defaults, required}`. Instantiate it through `papyrus_create` with `template_id`; defaults merge recursively, explicit arrays replace defaults, required paths such as `extra.owner` are validated, and target-kind mismatches are rejected.
101
+ The existing `artifact-template` skill subtype remains a compatibility mechanism for one-artifact templates with metadata `{targetKind, defaults, required}`. Instantiate it through the `skills` tool's `instantiate` action with `template_id`; defaults merge recursively, explicit arrays replace defaults, required paths such as `extra.owner` are validated, and target-kind mismatches are rejected.
102
102
 
103
103
  ### Removing an artifact
104
104
 
@@ -110,9 +110,8 @@ Once the deadline passes, the daemon's periodic sweep performs a real, cascading
110
110
 
111
111
  The `papyrus_*` tools are the low-level graph-store API:
112
112
 
113
- - **`papyrus_create`** — create directly or instantiate via `template_id`
114
113
  - **`papyrus_query`** — filter by kind/status or search title and body
115
- - **`papyrus_graph`** — link artifacts, perform bounded traversal, or update status
114
+ - **`papyrus_graph`** — link artifacts, perform bounded traversal, or read the mutation event log
116
115
  - **`papyrus_show`** — read nested metadata and bounded edges, optionally running gates
117
116
 
118
117
  Agent-facing domain tools own lifecycle invariants and sit above this store API:
@@ -238,7 +238,7 @@ export function registerTasksTool(pi: ExtensionAPI): void {
238
238
  pi.registerTool({
239
239
  name: "tasks",
240
240
  label: "Tasks",
241
- 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, remove, restore, claim, heartbeat_lease, release_lease, lease, event_feed. 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. remove moves a Task to a time-gated trash (restorable via restore until the purge deadline; refuses if it is the live Task Focus). claim/heartbeat_lease/release_lease/lease manage a bounded work-reservation lease -- independent of both lifecycle status and Focus, so multiple sessions can Focus the same task while only one owner holds its lease at a time; claim throws if a DIFFERENT owner already holds a live lease, release/heartbeat require the exact token claim returned. `owner` defaults to this session's own id when omitted. PREFER addressing a task by `name` (its exact title) over `id` for every action -- id is a backend implementation detail, resolved from name automatically, and only needs to appear explicitly when a name is genuinely ambiguous (two tasks share a title; the error will say so and list the real ids to disambiguate with). Task results likewise show name and status, not id, unless two shown tasks share a title. `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`. Prefer this over low-level papyrus_* tools for task work.",
241
+ description: "Task domain tool. ACTIONS: create, update, list, show, history, context, scope, set_scope, assign_project, graph, plan, active, focused, focus, pause, unpause, clear_focus, start, submit, complete, reject, retry, cancel, run_gates, set_checklist, depend, undepend, contain, uncontain, remove, restore, claim, heartbeat_lease, release_lease, lease, event_feed. context returns the full current/desired/verify reconciliation plan for the active task(s) -- the system prompt is injected with only a one-line pointer to save tokens on turns that don't need it; call this explicitly when you actually need the full plan (e.g. after a compaction, or before reconciling). 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. remove moves a Task to a time-gated trash (restorable via restore until the purge deadline; refuses if it is the live Task Focus). claim/heartbeat_lease/release_lease/lease manage a bounded work-reservation lease -- independent of both lifecycle status and Focus, so multiple sessions can Focus the same task while only one owner holds its lease at a time; claim throws if a DIFFERENT owner already holds a live lease, release/heartbeat require the exact token claim returned. `owner` defaults to this session's own id when omitted. PREFER addressing a task by `name` (its exact title) over `id` for every action -- id is a backend implementation detail, resolved from name automatically, and only needs to appear explicitly when a name is genuinely ambiguous (two tasks share a title; the error will say so and list the real ids to disambiguate with). Task results likewise show name and status, not id, unless two shown tasks share a title. `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`. Prefer this over low-level papyrus_* tools for task work.",
242
242
  parameters: Type.Object({
243
243
  action: Type.String(),
244
244
  id: Type.Optional(Type.String()),
@@ -390,6 +390,11 @@ export function registerTasksTool(pi: ExtensionAPI): void {
390
390
  const output = lines.join("\n") || "No tasks in execution plan.";
391
391
  return text(output, createPreviewDetails("tasks.plan", "Task execution plan", output));
392
392
  }
393
+ if (action === "context") {
394
+ const summary = await callService<Record<string, unknown>, string | null>("tasks.context", { ...request, verbosity: "full" });
395
+ const output = summary ?? "No open tasks.";
396
+ return text(output, createPreviewDetails("tasks.context", "Task reconciliation context", output));
397
+ }
393
398
  if (action === "set_checklist") {
394
399
  const artifact = await callService<Record<string, unknown>, Artifact>("tasks.set_checklist", params);
395
400
  return text(`Updated checklist: ${artifactLine(artifact)}`, createArtifactDetails("tasks.set_checklist", artifact));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pi-papyrus — native Pi extension for the Papyrus graph store.
3
3
  *
4
- * Tools: papyrus_create/query/graph/show.
4
+ * Tools: papyrus_query/graph/show (low-level), plus one native tool per domain (docs/rules/skills/playbooks/tasks/discuss/notes).
5
5
  * Command: /tasks (interactive task panel).
6
6
  * Widget: persistent task status above editor (rpiv-todo pattern).
7
7
  * Injection: active rules + open tasks appended to system prompt every turn.
@@ -279,43 +279,6 @@ export default async function (pi: ExtensionAPI) {
279
279
 
280
280
  // ── Low-level graph-store tools ────────────────────────────────────
281
281
 
282
- pi.registerTool({
283
- name: "papyrus_create",
284
- label: "Papyrus Create",
285
- description:
286
- "Create a graph artifact. KINDS: doc (knowledge — specs, decisions, research), " +
287
- "task (work — with gates/checklists in extra), rule (governance — when doing X, follow Y; " +
288
- "active rules inject into the system prompt), skill (parameterized workflow bundle — validated inputs render connected Tasks, Rules, and Docs). " +
289
- "RULE extra: {condition, action, severity: 'block'|'warn'|'info'}. " +
290
- "TASK extra: {gates: [{type:'file-exists'|'contains'|'command'|'test', target, expect}], checklist: {'criterion': {proof: [{type:'file'|'symbol'|'code'|'test'|'command'|'artifact'|'url', target, expect}]}}}. " +
291
- "Legacy SKILL extra: {trigger, steps: [...], tools: [...]}. Workflow Skill schemas are versioned separately. " +
292
- "Templates are skills with subtype='artifact-template' and extra {targetKind, defaults, required}; pass template_id to instantiate.",
293
- parameters: Type.Object({
294
- kind: Type.Optional(Type.String({ description: "doc | task | rule | skill; optional when template_id supplies targetKind" })),
295
- title: Type.Optional(Type.String({ description: "required unless supplied by template defaults" })),
296
- status: Type.Optional(Type.String({ description: "default: first registered for kind" })),
297
- subtype: Type.Optional(Type.String()),
298
- body: Type.Optional(Type.String()),
299
- labels: Type.Optional(Type.Array(Type.String())),
300
- extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
301
- template_id: Type.Optional(Type.String({ description: "skill/artifact-template id whose defaults and requirements apply" })),
302
- project_root: Type.Optional(Type.String({ description: "required for Tasks; defaults to Pi cwd" })),
303
- }),
304
- renderCall(args, theme) { return renderPapyrusToolCall("Create artifact", args, theme); },
305
- renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
306
- async execute(_id, params, _signal, _onUpdate, ctx) {
307
- try {
308
- const a = await callService<Record<string, unknown>, Artifact>("artifact.create", {
309
- ...params,
310
- ...(params.kind === "task" ? { project_root: params.project_root ?? ctx.cwd } : {}),
311
- });
312
- return text(`Created ${artifactTextLabel(a)}`, createArtifactDetails("artifact.create", a));
313
- } catch (e) {
314
- throw new Error(`papyrus_create failed: ${e instanceof Error ? e.message : e}`);
315
- }
316
- },
317
- });
318
-
319
282
  pi.registerTool({
320
283
  name: "papyrus_query",
321
284
  label: "Papyrus Query",
@@ -644,7 +607,7 @@ export default async function (pi: ExtensionAPI) {
644
607
  const [rules, playbooks, summary] = await Promise.all([
645
608
  callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "body" | "extra">>>("rules.injectable", { project_root: ctx.cwd, session_id: sessionId }),
646
609
  callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "extra">>>("playbooks.list", { status: "active", limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS }),
647
- callService<Record<string, unknown>, string | null>("tasks.context", { project_root: ctx.cwd, session_id: sessionId }),
610
+ callService<Record<string, unknown>, string | null>("tasks.context", { project_root: ctx.cwd, session_id: sessionId, verbosity: "summary" }),
648
611
  ]);
649
612
  const injection = buildContextInjection({
650
613
  basePrompt: event.systemPrompt ?? "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.32.1",
3
+ "version": "0.33.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"],
@@ -164,6 +164,7 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
164
164
  artifacts,
165
165
  tasks.active(taskFilter(input))?.id,
166
166
  new Set(tasks.list(taskFilter(input)).map((task) => task.id)),
167
+ optionalString(input, "verbosity") === "summary" ? "summary" : "full",
167
168
  )),
168
169
  define("tasks.reject", (input: OperationInput) => tasks.transition(string(input, "id"), "reject", eventContext(input))),
169
170
  define("tasks.retry", (input: OperationInput) => tasks.transition(string(input, "id"), "retry", eventContext(input))),
@@ -35,6 +35,11 @@ function renderCurrent(task: Artifact): string[] {
35
35
  ];
36
36
  }
37
37
 
38
+ /** The unconditional-injection form: enough to know the current task and that fuller detail exists, without repeating its full Desired/Verify prose every turn. */
39
+ function renderCurrentSummary(task: Artifact): string[] {
40
+ return [`Current: ${task.title} [${task.status}] -- call tasks(action="context") for the full current/desired/verify plan`];
41
+ }
42
+
38
43
  /** Same scoping rule taskContext already applies to ordinary tasks, plus the focused task even if scope excludes it. */
39
44
  function inScope(taskId: string, activeTaskId: string | undefined, taskIds: Set<string> | undefined): boolean {
40
45
  return taskIds === undefined || taskIds.has(taskId) || taskId === activeTaskId;
@@ -62,7 +67,16 @@ function deferredBlockingDiscussions(artifacts: ArtifactStore, activeTaskId: str
62
67
  return lines;
63
68
  }
64
69
 
65
- export function taskContext(artifacts: ArtifactStore, activeTaskId?: string, taskIds?: Set<string>): string | null {
70
+ export type TaskContextVerbosity = "summary" | "full";
71
+
72
+ /**
73
+ * verbosity="full" (the default, matching tasks(action="context") called on demand) renders the
74
+ * current task's complete Desired/Verify plan. verbosity="summary" (used for the unconditional
75
+ * system-prompt injection every turn) renders only enough for the agent to know a current task
76
+ * exists and that the full plan is one explicit call away -- avoiding repeating the same
77
+ * unchanged prose every single turn for a task that can persist across dozens of turns.
78
+ */
79
+ export function taskContext(artifacts: ArtifactStore, activeTaskId?: string, taskIds?: Set<string>, verbosity: TaskContextVerbosity = "full"): string | null {
66
80
  const tasks = artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE })
67
81
  .filter((task) => taskIds === undefined || taskIds.has(task.id))
68
82
  .sort((left, right) => left.updated_at.localeCompare(right.updated_at));
@@ -76,7 +90,8 @@ export function taskContext(artifacts: ArtifactStore, activeTaskId?: string, tas
76
90
  const next = open.find((task) => task.status === "todo");
77
91
  const rejected = open.filter((task) => task.status === "rejected").slice(0, TASK_CONTEXT_REJECTED_LIMIT);
78
92
  const lines = tasks.length > 0 ? [`Progress: ${done}/${tasks.length} done`] : [];
79
- for (const task of current) lines.push(...renderCurrent(task));
93
+ const renderTask = verbosity === "summary" ? renderCurrentSummary : renderCurrent;
94
+ for (const task of current) lines.push(...renderTask(task));
80
95
  if (next) lines.push(`Next: ${next.title}`);
81
96
  if (rejected.length > 0) lines.push(`Rejected: ${rejected.map((task) => task.title).join(", ")}`);
82
97
  if (deferredDiscussions.length > 0) {