@danypops/papyrus 0.36.1 → 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/package.json +1 -1
- package/src/cli.ts +9 -1
- package/src/constants.ts +3 -0
- package/src/modules/tasks.ts +2 -1
- package/src/playbook-definition.ts +11 -3
- package/src/service.ts +1 -0
- package/src/task-service.ts +36 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -169,6 +169,7 @@ const USAGE = `Usage:
|
|
|
169
169
|
papyrus tasks reject <id> [--session-id <id>] [--json]
|
|
170
170
|
papyrus tasks retry <id> [--session-id <id>] [--json]
|
|
171
171
|
papyrus tasks cancel <id> [--session-id <id>] [--json]
|
|
172
|
+
papyrus tasks cancel-subtree <id> [--session-id <id>] [--json]
|
|
172
173
|
papyrus tasks depend <id> <prerequisite-id> [--reason <reason>] [--session-id <id>] [--json]
|
|
173
174
|
papyrus tasks undepend <id> <prerequisite-id> [--reason <reason>] [--session-id <id>] [--json]
|
|
174
175
|
papyrus tasks contain <parent-id> <child-id> [--reason <reason>] [--session-id <id>] [--json]
|
|
@@ -1740,6 +1741,13 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
1740
1741
|
human = `${action[0]!.toUpperCase()}${action.slice(1)}: ${artifactLabel(artifact)}`;
|
|
1741
1742
|
break;
|
|
1742
1743
|
}
|
|
1744
|
+
case "cancel-subtree": {
|
|
1745
|
+
if (!id || dependencyId) throw new Error("tasks cancel-subtree requires exactly one task id");
|
|
1746
|
+
const outcome = await client.call<Record<string, unknown>, { canceled: string[]; skipped: string[] }>("tasks.cancel_subtree", { id, actor: "user", source: "cli", ...sessionScope });
|
|
1747
|
+
result = outcome;
|
|
1748
|
+
human = `Canceled ${outcome.canceled.length} task(s)${outcome.skipped.length > 0 ? `, skipped ${outcome.skipped.length} already-terminal` : ""}.`;
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1743
1751
|
case "depend": {
|
|
1744
1752
|
if (!id || !dependencyId || positional.length !== 3) throw new Error("tasks depend requires a task id and prerequisite id");
|
|
1745
1753
|
const artifact = await client.call<Record<string, unknown>, CliArtifact>("tasks.depend", {
|
|
@@ -1759,7 +1767,7 @@ export async function runTaskCli(args: string[], client: TaskCliClient, projectR
|
|
|
1759
1767
|
break;
|
|
1760
1768
|
}
|
|
1761
1769
|
default:
|
|
1762
|
-
throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, context, history, scope, assign-project, complete, start, submit, reject, retry, cancel, depend, undepend, contain, uncontain, run-gates, set-checklist, or set-gates");
|
|
1770
|
+
throw new Error("tasks action must be create, list, show, active, focused, focus, pause, unpause, clear-focus, update, graph, plan, context, history, scope, assign-project, complete, start, submit, reject, retry, cancel, cancel-subtree, depend, undepend, contain, uncontain, run-gates, set-checklist, or set-gates");
|
|
1763
1771
|
}
|
|
1764
1772
|
return json ? JSON.stringify(result) : human;
|
|
1765
1773
|
}
|
package/src/constants.ts
CHANGED
|
@@ -121,6 +121,9 @@ export const PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH = 500;
|
|
|
121
121
|
*/
|
|
122
122
|
export const PLAYBOOK_INVOCATION_MAX_CREATED_TASKS = 200;
|
|
123
123
|
|
|
124
|
+
/** Tasks.cancelSubtree walks `contains` edges transitively (a whole materialized playbook run can be torn down in one call instead of enumerating every task id by hand) -- bounded the same way PLAYBOOK_INVOCATION_MAX_CREATED_TASKS bounds the forward direction. */
|
|
125
|
+
export const TASK_CANCEL_SUBTREE_MAX_NODES = 500;
|
|
126
|
+
|
|
124
127
|
/**
|
|
125
128
|
* At the core, a workflow Skill creates Tasks and begins a pipeline -- an Ansible playbook or
|
|
126
129
|
* a Jenkins job, not just a text prompt. A pipeline step can itself trigger another workflow
|
package/src/modules/tasks.ts
CHANGED
|
@@ -90,7 +90,7 @@ export const TASKS_OPERATION_NAMES = [
|
|
|
90
90
|
"tasks.create", "tasks.update", "tasks.list", "tasks.graph", "tasks.plan", "tasks.show", "tasks.history",
|
|
91
91
|
"tasks.scope", "tasks.set_scope", "tasks.assign_project", "tasks.active", "tasks.focused", "tasks.focus",
|
|
92
92
|
"tasks.pause", "tasks.unpause", "tasks.clear_focus", "tasks.start", "tasks.submit", "tasks.complete",
|
|
93
|
-
"tasks.run_gates", "tasks.set_checklist", "tasks.set_gates", "tasks.context", "tasks.reject", "tasks.retry", "tasks.cancel",
|
|
93
|
+
"tasks.run_gates", "tasks.set_checklist", "tasks.set_gates", "tasks.context", "tasks.reject", "tasks.retry", "tasks.cancel", "tasks.cancel_subtree",
|
|
94
94
|
"tasks.depend", "tasks.undepend", "tasks.contain", "tasks.uncontain", "tasks.reap_stale_focus",
|
|
95
95
|
"tasks.claim", "tasks.heartbeat_lease", "tasks.release_lease", "tasks.lease", "tasks.reap_stale_leases", "tasks.event_feed",
|
|
96
96
|
] as const;
|
|
@@ -169,6 +169,7 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
|
|
|
169
169
|
define("tasks.reject", (input: OperationInput) => tasks.transition(string(input, "id"), "reject", eventContext(input))),
|
|
170
170
|
define("tasks.retry", (input: OperationInput) => tasks.transition(string(input, "id"), "retry", eventContext(input))),
|
|
171
171
|
define("tasks.cancel", (input: OperationInput) => tasks.transition(string(input, "id"), "cancel", eventContext(input))),
|
|
172
|
+
define("tasks.cancel_subtree", (input: OperationInput) => tasks.cancelSubtree(string(input, "id"), eventContext(input))),
|
|
172
173
|
define("tasks.depend", (input: OperationInput) => tasks.depend(string(input, "id"), string(input, "dependency_id"), eventContext(input))),
|
|
173
174
|
define("tasks.undepend", (input: OperationInput) => tasks.undepend(string(input, "id"), string(input, "dependency_id"), eventContext(input))),
|
|
174
175
|
define("tasks.contain", (input: OperationInput) => tasks.contain(string(input, "parent_id"), string(input, "child_id"), eventContext(input))),
|
|
@@ -42,10 +42,17 @@ export interface CompiledPlaybook {
|
|
|
42
42
|
externalLinks: PlaybookExternalLink[];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Trashed but not yet purged: artifacts.get() still returns it (trash is separate, orthogonal metadata -- "still directly showable"), so a stale composition edge left behind by remove/uncontain would otherwise resolve straight through and get compiled in. query() excludes trashed artifacts by default; use that instead of get() for every existence check a compiler makes. */
|
|
46
|
+
function nonTrashedPlaybookIds(artifacts: ArtifactStore, ids: string[]): Set<string> {
|
|
47
|
+
if (ids.length === 0) return new Set();
|
|
48
|
+
return new Set(artifacts.query({ ids, kind: "playbook" }).map((artifact) => artifact.id));
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
function requirePlaybook(artifacts: ArtifactStore, id: string): Artifact {
|
|
46
52
|
const playbook = artifacts.get(id);
|
|
47
53
|
if (!playbook) throw new Error(`playbook artifact "${id}" not found`);
|
|
48
54
|
if (playbook.kind !== "playbook") throw new Error(`artifact "${id}" is not a playbook`);
|
|
55
|
+
if (!nonTrashedPlaybookIds(artifacts, [id]).has(id)) throw new Error(`playbook artifact "${id}" is trashed`);
|
|
49
56
|
return playbook;
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -121,12 +128,13 @@ function compileNode(
|
|
|
121
128
|
if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS) throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} tasks`);
|
|
122
129
|
|
|
123
130
|
const edges = artifacts.relationships({ artifactIds: [playbookId] }).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
|
|
131
|
+
const composablePlaybookIds = nonTrashedPlaybookIds(artifacts, edges.filter((edge) => edge.from === playbookId).map((edge) => edge.to));
|
|
124
132
|
const prerequisiteIds = edges.filter((edge) => edge.from === playbookId && edge.relation === "depends_on").map((edge) => edge.to)
|
|
125
|
-
.filter((id) =>
|
|
133
|
+
.filter((id) => composablePlaybookIds.has(id));
|
|
126
134
|
const nestedIds = edges.filter((edge) => edge.from === playbookId && edge.relation === "contains").map((edge) => edge.to)
|
|
127
|
-
.filter((id) =>
|
|
135
|
+
.filter((id) => composablePlaybookIds.has(id));
|
|
128
136
|
for (const edge of edges) {
|
|
129
|
-
const isComposingFrom = edge.from === playbookId && (edge.relation === "contains" || edge.relation === "depends_on") &&
|
|
137
|
+
const isComposingFrom = edge.from === playbookId && (edge.relation === "contains" || edge.relation === "depends_on") && composablePlaybookIds.has(edge.to);
|
|
130
138
|
if (isComposingFrom) continue;
|
|
131
139
|
if (edge.from === playbookId) ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.to, ownerIsFrom: true });
|
|
132
140
|
else if (edge.to === playbookId) ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.from, ownerIsFrom: false });
|
package/src/service.ts
CHANGED
|
@@ -359,6 +359,7 @@ function handlers(
|
|
|
359
359
|
"tasks.reject": forwardToModule("tasks.reject"),
|
|
360
360
|
"tasks.retry": forwardToModule("tasks.retry"),
|
|
361
361
|
"tasks.cancel": forwardToModule("tasks.cancel"),
|
|
362
|
+
"tasks.cancel_subtree": forwardToModule("tasks.cancel_subtree"),
|
|
362
363
|
"tasks.depend": forwardToModule("tasks.depend"),
|
|
363
364
|
"tasks.undepend": forwardToModule("tasks.undepend"),
|
|
364
365
|
"tasks.contain": forwardToModule("tasks.contain"),
|
package/src/task-service.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TASK_BODY_MAX_LENGTH,
|
|
3
|
+
TASK_CANCEL_SUBTREE_MAX_NODES,
|
|
3
4
|
TASK_EXECUTION_MAX_DEGREE,
|
|
4
5
|
TASK_EXECUTION_MAX_EDGES,
|
|
5
6
|
TASK_EXECUTION_MAX_NODES,
|
|
@@ -459,6 +460,41 @@ export class Tasks {
|
|
|
459
460
|
});
|
|
460
461
|
}
|
|
461
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Cancels a task and every task in its containment subtree (`contains` edges, transitively) --
|
|
465
|
+
* a whole materialized playbook/skill run can be torn down in one call instead of enumerating
|
|
466
|
+
* every task id by hand. A task already in a terminal state (done/canceled) is skipped, not
|
|
467
|
+
* treated as an error, matching how a mixed-status subtree is the normal case (some steps
|
|
468
|
+
* genuinely finished before the rest needed to be abandoned). Does not follow `depends_on` --
|
|
469
|
+
* only containment cascades, a prerequisite is a different unit of work.
|
|
470
|
+
*/
|
|
471
|
+
cancelSubtree(id: string, context: TaskEventContext = {}): { canceled: string[]; skipped: string[] } {
|
|
472
|
+
this.require(id);
|
|
473
|
+
const visited = new Set<string>();
|
|
474
|
+
const queue = [id];
|
|
475
|
+
const canceled: string[] = [];
|
|
476
|
+
const skipped: string[] = [];
|
|
477
|
+
while (queue.length > 0) {
|
|
478
|
+
const current = queue.shift()!;
|
|
479
|
+
if (visited.has(current)) continue;
|
|
480
|
+
visited.add(current);
|
|
481
|
+
if (visited.size > TASK_CANCEL_SUBTREE_MAX_NODES) throw new Error(`cancelSubtree exceeds ${TASK_CANCEL_SUBTREE_MAX_NODES} tasks`);
|
|
482
|
+
const task = this.artifacts.get(current);
|
|
483
|
+
if (!task || task.kind !== "task") continue;
|
|
484
|
+
const childIds = this.artifacts.relationships({ artifactIds: [current] })
|
|
485
|
+
.filter((edge) => edge.from === current && edge.relation === "contains")
|
|
486
|
+
.map((edge) => edge.to);
|
|
487
|
+
queue.push(...childIds);
|
|
488
|
+
if (task.status === "done" || task.status === "canceled") {
|
|
489
|
+
skipped.push(current);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
this.transition(current, "cancel", context);
|
|
493
|
+
canceled.push(current);
|
|
494
|
+
}
|
|
495
|
+
return { canceled, skipped };
|
|
496
|
+
}
|
|
497
|
+
|
|
462
498
|
complete(id: string, context: TaskEventContext = {}, options: TaskCompletionOptions = {}): TaskCompletion {
|
|
463
499
|
const task = this.requireReview(id);
|
|
464
500
|
this.requireNotBlocked(task);
|