@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 +2 -3
- package/extension/src/domain-tools.ts +6 -1
- package/extension/src/index.ts +2 -39
- package/package.json +1 -1
- package/src/modules/tasks.ts +1 -0
- package/src/task-context.ts +17 -2
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 `
|
|
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
|
|
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));
|
package/extension/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-papyrus — native Pi extension for the Papyrus graph store.
|
|
3
3
|
*
|
|
4
|
-
* Tools:
|
|
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
package/src/modules/tasks.ts
CHANGED
|
@@ -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))),
|
package/src/task-context.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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) {
|