@danypops/pi-papyrus 0.36.2 → 0.37.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
@@ -116,8 +116,11 @@ papyrus tasks complete <id> --json
116
116
  papyrus tasks reject <id> --json
117
117
  papyrus tasks retry <id> --json
118
118
  papyrus tasks cancel <id> --json
119
+ papyrus tasks cancel-subtree <id> --json
119
120
  ```
120
121
 
122
+ `cancel-subtree` cancels a Task and every Task in its containment (`contains`) subtree in one call -- for tearing down a whole materialized Playbook/Skill run at once instead of canceling each Task id by hand. A Task already `done`/`canceled` is skipped, not treated as an error.
123
+
121
124
  Task edits mutate the existing Papyrus-owned Task identity and append an `updated` event; title, body, and labels can be revised without canceling the Task or creating a replacement. Lifecycle, relationships, gates, checklist metadata, scope, and focus remain intact. The same `update` action provides a narrowly guarded recovery for Tasks accidentally created terminal by a legacy default: `status=todo` requires an audit reason, cannot be combined with content edits, only applies when `created` is the sole lifecycle event, and appends `creation_recovered` rather than rewriting history.
122
125
 
123
126
  ### Focus-driven automatic continuation
@@ -244,7 +244,7 @@ export function registerTasksTool(pi: ExtensionAPI): void {
244
244
  pi.registerTool({
245
245
  name: "tasks",
246
246
  label: "Tasks",
247
- 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.",
247
+ 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, cancel_subtree, run_gates, set_checklist, depend, undepend, contain, uncontain, remove, restore, claim, heartbeat_lease, release_lease, lease, event_feed. Lifecycle: todo → in-progress → review → done; review failure → rejected retry → in-progress; canceled is terminal. Focus and lease are independent of lifecycle and of each other -- multiple sessions can focus the same task while only one holds its lease (claim throws if a different owner already holds one; release/heartbeat need the exact token claim returned; owner defaults to this session's id). context returns the full plan (the system prompt itself only carries a one-line pointer) -- call it explicitly after a compaction or before reconciling. complete runs gates + checklist-proof review, then focuses one ready successor. cancel_subtree cancels a task and its whole containment subtree in one call, skipping tasks already done/canceled. remove/restore use a time-gated trash (refuses the live Focus); undepend/uncontain are idempotent no-ops when the edge is already absent. update recovers an accidentally-terminal task via status=todo + reason, without rewriting real history. Prefer `name` (exact title) over `id` -- id is a backend detail, resolved automatically, needed only to disambiguate a shared title; `dependency_name`/`parent_name`/`child_name`/`root_task_name`/`depends_on_names` are the same pattern for their `_id` counterparts.",
248
248
  parameters: Type.Object({
249
249
  action: Type.String(),
250
250
  id: Type.Optional(Type.String()),
@@ -447,6 +447,11 @@ export function registerTasksTool(pi: ExtensionAPI): void {
447
447
  const output = lease ? `Leased by "${lease.owner}" until ${lease.leaseExpiresAt} (token ${lease.token}).` : "No live lease.";
448
448
  return text(output, createPreviewDetails(operation, "Task lease", output));
449
449
  }
450
+ if (action === "cancel_subtree") {
451
+ const outcome = await callService<Record<string, unknown>, { canceled: string[]; skipped: string[] }>("tasks.cancel_subtree", request);
452
+ const output = `Canceled ${outcome.canceled.length} task(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-terminal` : ""}.`;
453
+ return text(output, createPreviewDetails("tasks.cancel_subtree", "Cancel task subtree", JSON.stringify(outcome, null, 2)));
454
+ }
450
455
  const trashResult = await handleArtifactRemoveRestore(action, params);
451
456
  if (trashResult) return trashResult;
452
457
  const operations = {
@@ -675,6 +680,18 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
675
680
  try {
676
681
  const params: Record<string, unknown> = { ...rawParams };
677
682
  const action = params.action;
683
+ // Name resolution must search regardless of project scope -- a Playbook itself is
684
+ // commonly unscoped (e.g. a cross-repo lab-deploy playbook), so resolutionRequest
685
+ // uses the caller's ORIGINAL project_root (undefined unless explicitly given), never
686
+ // the invoke-specific default applied below -- that default is only for where the
687
+ // resulting TASKS land, not for finding the playbook artifact itself.
688
+ const resolutionRequest = { project_root: params.project_root };
689
+ await resolveNameFields(params, [
690
+ { nameKey: "name", idKey: "id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
691
+ { nameKey: "parent_name", idKey: "parent_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
692
+ { nameKey: "child_name", idKey: "child_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
693
+ { nameKey: "dependency_name", idKey: "dependency_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
694
+ ]);
678
695
  // invoke ends by calling tasks.focus server-side -- that focus write must land in the
679
696
  // SAME session scope the tasks tool reads from (ctx.sessionManager.getSessionId()),
680
697
  // the same resolution the tasks tool itself always applies, or the entry task's focus
@@ -684,6 +701,7 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
684
701
  // task is invisible to tasks(action=focused) even with the right session -- confirmed
685
702
  // live (the focus_set event existed with the correct sessionId, but Tasks.focused's own
686
703
  // project-scope filter silently excluded the unscoped task from a cwd-scoped read).
704
+ // Applied AFTER name resolution: it must never affect finding the playbook itself.
687
705
  if (action === "invoke") {
688
706
  const resolvedSessionId = params.session_id ?? ctx.sessionManager.getSessionId();
689
707
  Object.assign(params, {
@@ -692,13 +710,6 @@ export function registerPlaybooksTool(pi: ExtensionAPI): void {
692
710
  ...sessionSecretField(resolvedSessionId as string),
693
711
  });
694
712
  }
695
- const resolutionRequest = { project_root: params.project_root };
696
- await resolveNameFields(params, [
697
- { nameKey: "name", idKey: "id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
698
- { nameKey: "parent_name", idKey: "parent_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
699
- { nameKey: "child_name", idKey: "child_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
700
- { nameKey: "dependency_name", idKey: "dependency_id", listOperation: "playbooks.list", baseRequest: resolutionRequest },
701
- ]);
702
713
  if (action === "create" || action === "invoke" || action === "preview") normalizeJsonEncodedField(params, "arguments");
703
714
  if (action === "create") {
704
715
  const artifact = await callService<Record<string, unknown>, Artifact>("playbooks.create", params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-papyrus",
3
- "version": "0.36.2",
3
+ "version": "0.37.0",
4
4
  "description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@danypops/daemon-kit": "^0.10.0",
21
- "@danypops/papyrus": "^0.36.1",
21
+ "@danypops/papyrus": "^0.37.0",
22
22
  "beautiful-mermaid": "1.1.3"
23
23
  },
24
24
  "devDependencies": {