@danypops/papyrus 0.32.2 → 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.
|
@@ -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
|
@@ -607,7 +607,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
607
607
|
const [rules, playbooks, summary] = await Promise.all([
|
|
608
608
|
callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "body" | "extra">>>("rules.injectable", { project_root: ctx.cwd, session_id: sessionId }),
|
|
609
609
|
callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "extra">>>("playbooks.list", { status: "active", limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS }),
|
|
610
|
-
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" }),
|
|
611
611
|
]);
|
|
612
612
|
const injection = buildContextInjection({
|
|
613
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) {
|