@danypops/papyrus 0.35.3 → 0.36.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.35.3",
3
+ "version": "0.36.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"],
package/src/cli.ts CHANGED
@@ -113,10 +113,10 @@ const USAGE = `Usage:
113
113
  papyrus skills assign-project <id> [project-root] [--json]
114
114
  papyrus skills update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
115
115
  papyrus playbooks create --title <title> [--body <body>] [--trigger <text>] [--steps-json <json>] [--tools-json <json>] [--labels-json <json>] [--extra-json <json>] [--arguments-json <json array>] [--project-root <path>] [--json]
116
- papyrus playbooks invoke <id> [--arguments-json <json object>] [--json]
116
+ papyrus playbooks invoke <id> [--arguments-json <json object>] [--project-root <path>] [--json] # materializes real tasks (contains/depends_on-wired) and focuses the entry task
117
+ papyrus playbooks preview <id> [--arguments-json <json object>] [--json] # renders text only, creates nothing
117
118
  papyrus playbooks list [--status <status>] [--text <query>] [--limit <count>] [--project-root <path>] [--json]
118
119
  papyrus playbooks show <id> [--json]
119
- papyrus playbooks invoke <id> [--json]
120
120
  papyrus playbooks enable|disable <id> [--json]
121
121
  papyrus playbooks assign-project <id> [project-root] [--json]
122
122
  papyrus playbooks update <id> [--title <title>] [--body <body>] [--labels-json <json>] [--json]
@@ -863,11 +863,20 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
863
863
  human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
864
864
  break;
865
865
  }
866
+ case "preview": {
867
+ if (!id || second) throw new Error("playbooks preview requires exactly one playbook id");
868
+ const rendered = await client.call<Record<string, unknown>, string>("playbooks.preview", { id, arguments: playbookArguments });
869
+ result = rendered;
870
+ human = rendered;
871
+ break;
872
+ }
866
873
  case "invoke": {
867
874
  if (!id || second) throw new Error("playbooks invoke requires exactly one playbook id");
868
- const invocation = await client.call<Record<string, unknown>, string>("playbooks.invoke", { id, arguments: playbookArguments });
875
+ const invocation = await client.call<Record<string, unknown>, { entryTaskId: string; missingArguments?: string[] }>("playbooks.invoke", { id, arguments: playbookArguments, project_root: playbookProjectRoot });
869
876
  result = invocation;
870
- human = invocation;
877
+ human = invocation.missingArguments
878
+ ? `Missing required argument(s): ${invocation.missingArguments.join(", ")}.`
879
+ : `Invoked: entry task ${invocation.entryTaskId} focused. Drive it forward with \`tasks start/submit/complete\` like any other task.`;
871
880
  break;
872
881
  }
873
882
  case "enable":
@@ -922,7 +931,7 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
922
931
  break;
923
932
  }
924
933
  default:
925
- throw new Error("playbooks action must be create, list, show, invoke, enable, disable, assign-project, update, contain, uncontain, depend, or undepend");
934
+ throw new Error("playbooks action must be create, list, show, invoke, preview, enable, disable, assign-project, update, contain, uncontain, depend, or undepend");
926
935
  }
927
936
  return json ? JSON.stringify(result) : human;
928
937
  }
package/src/constants.ts CHANGED
@@ -113,6 +113,13 @@ export const PLAYBOOK_INVOCATION_MAX_CALL_DEPTH = 4;
113
113
  export const PLAYBOOK_ARGUMENT_MAX_COUNT = 20;
114
114
  export const PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH = 64;
115
115
  export const PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH = 500;
116
+ /**
117
+ * playbooks.invoke materializes a real Task per step (plus one container Task per playbook
118
+ * node in the contains/depends_on composition tree) instead of rendering text -- this bounds
119
+ * the total number of Tasks one invoke call can create, the same blast-radius concern
120
+ * SKILL_MAX_BLUEPRINTS already covers for workflow Skills.
121
+ */
122
+ export const PLAYBOOK_INVOCATION_MAX_CREATED_TASKS = 200;
116
123
 
117
124
  /**
118
125
  * At the core, a workflow Skill creates Tasks and begins a pipeline -- an Ansible playbook or
@@ -49,7 +49,7 @@ export interface SkillTaskBlueprint {
49
49
  /**
50
50
  * A pipeline step that nests another workflow Skill's run inside this one -- the Jenkins
51
51
  * "trigger downstream job and wait" / Ansible "include_tasks" primitive. `skillId` is late-
52
- * bound: existence and workflow-subtype are checked at execution time (skill-execution.ts),
52
+ * bound: existence and workflow-subtype are checked at execution time (workflow-execution.ts),
53
53
  * not here, since this validator has no store access. `dependsOn`/`parent` place this step in
54
54
  * the SAME dependency graph as ordinary task blueprints -- a task can depend on a skill-call
55
55
  * ref (meaning: depend on every task the nested run creates), and a skill-call's own `parent`
@@ -524,18 +524,22 @@ export function transitionSkill(artifacts: ArtifactStore, id: string, action: Sk
524
524
  }
525
525
 
526
526
  /**
527
- * Playbooks: a trigger and an ordered list of steps an agent reads and follows -- a completely
528
- * different beast from Skills, not a subtype of one. A Skill (artifact-template or workflow) is
529
- * mechanically instantiated into other artifacts; a Playbook is never instantiated, it's read
530
- * and followed. Like Tasks, a Playbook can be nested or chained with another Playbook:
531
- * `contains`/`part_of` (containPlaybook/uncontainPlaybook) nests a sub-playbook inside a parent
532
- * -- invoking the parent recursively embeds the nested one's own steps, run as part of it.
533
- * `depends_on` (dependPlaybook/undependPlaybook) chains one playbook before another -- invoking
534
- * the dependent recursively renders the prerequisite's steps FIRST, to be completed before the
535
- * dependent's own. Both are bounded and cycle-safe: a composition cycle degrades to a marker at
536
- * render time (playbookInvocation) rather than being rejected at link time -- unlike Tasks' own
537
- * depends_on, which does reject a real dependency cycle up front because Task dependencies gate
538
- * actual lifecycle execution, not just text rendering.
527
+ * Playbooks: a trigger and an ordered list of steps -- authored as prose, a completely
528
+ * different beast from Skills at that level. But playbooks.invoke (playbook-execution.ts)
529
+ * recycles the exact same materialization engine workflow Skills use: it compiles a Playbook
530
+ * into a SkillDefinition and mechanically instantiates real Tasks from it, same as a Skill's
531
+ * own artifact-template/workflow blueprint. `playbookInvocation` below is the OTHER, older
532
+ * path -- rendered text with no side effects, now exposed as the `preview` action for a human
533
+ * who wants to just read a playbook before invoking it, not the primary way of running one.
534
+ * Like Tasks, a Playbook can be nested or chained with another Playbook: `contains`/`part_of`
535
+ * (containPlaybook/uncontainPlaybook) nests a sub-playbook inside a parent -- both preview and
536
+ * invoke run the nested one's own steps as part of the parent, invoke as real dependsOn-chained
537
+ * Tasks, preview as embedded text. `depends_on` (dependPlaybook/undependPlaybook) chains one
538
+ * playbook before another -- the prerequisite's steps run first, either as real Tasks the
539
+ * dependent's first step depends_on (invoke) or as embedded text rendered first (preview).
540
+ * Composition is bounded in both paths; preview degrades a cycle to a text marker at render
541
+ * time, while invoke's compiler (playbook-definition.ts) treats a cycle as a hard error --
542
+ * real Tasks would otherwise be created in an infinite loop, unlike text rendering.
539
543
  */
540
544
  export interface PlaybookArgument {
541
545
  name: string;
package/src/index.ts CHANGED
@@ -23,7 +23,7 @@ export { connectPapyrusClient, resolvePushChannelTarget, type PapyrusClient, typ
23
23
  export type { DiscussionAndRounds } from "./discussion-service.ts";
24
24
  export { NOTE_DISPOSITIONS } from "./note-service.ts";
25
25
  export type { OperationName, SchemaState } from "./service.ts";
26
- export type { SkillWorkflowRunResult } from "./skill-execution.ts";
26
+ export type { WorkflowRunResult } from "./workflow-execution.ts";
27
27
  export { projectArtifactRelationships } from "./artifact-relationship-view.ts";
28
28
  export { taskContext } from "./task-context.ts";
29
29
  export { projectTaskExecution, type TaskExecutionPlan, type TaskExecutionState } from "./task-execution.ts";
@@ -1,15 +1,34 @@
1
1
  /**
2
2
  * modules/playbooks.ts — Playbooks as a Papyrus-native registered module.
3
3
  *
4
- * A Playbook (trigger + ordered steps an agent reads and follows) is a completely different
5
- * beast from a Skill (a mechanically instantiated artifact-template or workflow blueprint) --
6
- * its own kind, not a subtype squeezed into "skill". See domain-services.ts's Playbook section
7
- * for the full rationale.
4
+ * A Playbook (trigger + ordered steps, contains/depends_on composition, predefined
5
+ * rules/docs) is a completely different beast from a Skill (a mechanically instantiated
6
+ * artifact-template or workflow blueprint) at the AUTHORING level -- but playbooks.invoke
7
+ * recycles the exact same materialization engine workflow Skills use (playbook-execution.ts
8
+ * compiles a Playbook's composition tree into a SkillDefinition, then hands off to
9
+ * workflow-execution.ts's shared core). See domain-services.ts's Playbook section and
10
+ * playbook-definition.ts for the full rationale.
8
11
  */
9
- import { assignPlaybookProject, containPlaybook, createPlaybook, dependPlaybook, listPlaybooks, playbookInvocation, showPlaybook, transitionPlaybook, uncontainPlaybook, undependPlaybook, updatePlaybook } from "../domain-services.ts";
12
+ import {
13
+ assignPlaybookProject,
14
+ containPlaybook,
15
+ createPlaybook,
16
+ dependPlaybook,
17
+ listPlaybooks,
18
+ playbookInvocation,
19
+ showPlaybook,
20
+ transitionPlaybook,
21
+ uncontainPlaybook,
22
+ undependPlaybook,
23
+ updatePlaybook,
24
+ } from "../domain-services.ts";
10
25
  import type { OperationDefinition } from "../module-registry.ts";
11
26
  import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
12
27
  import type { ArtifactStore } from "../ports/artifact-store.ts";
28
+ import type { TaskEventStore } from "../ports/task-event-store.ts";
29
+ import type { TaskScopeStore } from "../ports/task-scope-store.ts";
30
+ import { invokePlaybook } from "../playbook-execution.ts";
31
+ import type { Tasks } from "../task-service.ts";
13
32
 
14
33
  const MODULE_ID = "playbooks";
15
34
 
@@ -41,6 +60,11 @@ const eventContext = (input: OperationInput) => ({
41
60
  sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
42
61
  });
43
62
 
63
+ const eventContextFor = (input: OperationInput, source: string) => {
64
+ const context = eventContext(input);
65
+ return { ...context, source: context.source ?? source };
66
+ };
67
+
44
68
  const artifactFilter = (input: OperationInput) => ({
45
69
  status: optionalString(input, "status"),
46
70
  text: optionalString(input, "text"),
@@ -50,28 +74,47 @@ const artifactFilter = (input: OperationInput) => ({
50
74
 
51
75
  /** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. */
52
76
  export const PLAYBOOKS_OPERATION_NAMES = [
53
- "playbooks.create", "playbooks.list", "playbooks.show", "playbooks.invoke", "playbooks.enable", "playbooks.disable", "playbooks.assign_project", "playbooks.update",
77
+ "playbooks.create", "playbooks.list", "playbooks.show", "playbooks.invoke", "playbooks.preview", "playbooks.enable", "playbooks.disable", "playbooks.assign_project", "playbooks.update",
54
78
  "playbooks.contain", "playbooks.uncontain", "playbooks.depend", "playbooks.undepend",
55
79
  ] as const;
56
80
 
57
- export function playbooksOperations(artifacts: ArtifactStore, scopes: ArtifactScopeStore): OperationDefinition[] {
81
+ export interface PlaybooksModuleDeps {
82
+ artifacts: ArtifactStore;
83
+ events: TaskEventStore;
84
+ scopes: TaskScopeStore;
85
+ /** Docs/Rules/Skills/Playbooks project scoping (distinct from `scopes`, which is Task-run project scoping for playbooks.invoke's materialized tasks). */
86
+ artifactScopes: ArtifactScopeStore;
87
+ /** Used for exactly one thing: focusing the entry task after a successful invoke -- the one safety-checked Tasks operation this module needs, not bulk graph construction (that goes straight through artifacts/events/scopes in playbook-execution.ts, mirroring workflow-execution.ts). */
88
+ tasks: Tasks;
89
+ }
90
+
91
+ export function playbooksOperations({ artifacts, events, scopes, artifactScopes, tasks }: PlaybooksModuleDeps): OperationDefinition[] {
58
92
  const define = <Input, Output>(name: string, execute: (input: Input) => Output): OperationDefinition<Input, Output> => ({
59
93
  name, moduleId: MODULE_ID, execute,
60
94
  });
61
95
  return [
62
- define("playbooks.create", (input: OperationInput) => createPlaybook(artifacts, scopes, {
96
+ define("playbooks.create", (input: OperationInput) => createPlaybook(artifacts, artifactScopes, {
63
97
  title: string(input, "title"), body: optionalString(input, "body"), trigger: optionalString(input, "trigger"),
64
98
  steps: input["steps"] as string[] | undefined, tools: input["tools"] as string[] | undefined,
65
99
  arguments: input["arguments"],
66
100
  labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
67
101
  projectRoot: optionalString(input, "project_root"),
68
102
  }, eventContext(input))),
69
- define("playbooks.list", (input: OperationInput) => listPlaybooks(artifacts, scopes, artifactFilter(input))),
103
+ define("playbooks.list", (input: OperationInput) => listPlaybooks(artifacts, artifactScopes, artifactFilter(input))),
70
104
  define("playbooks.show", (input: OperationInput) => showPlaybook(artifacts, string(input, "id"))),
71
- define("playbooks.invoke", (input: OperationInput) => playbookInvocation(artifacts, string(input, "id"), input["arguments"] as Record<string, string> | undefined)),
105
+ define("playbooks.preview", (input: OperationInput) => playbookInvocation(artifacts, string(input, "id"), input["arguments"] as Record<string, string> | undefined)),
106
+ define("playbooks.invoke", (input: OperationInput) => {
107
+ const result = invokePlaybook(artifacts, string(input, "id"), {
108
+ runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
109
+ arguments: input["arguments"] as Record<string, unknown> | undefined,
110
+ }, { events, scopes, projectRoot: optionalString(input, "project_root"), context: eventContextFor(input, "playbook-run") });
111
+ if ("missingArguments" in result) return result;
112
+ tasks.focus(result.entryTaskId, eventContextFor(input, "playbook-run"));
113
+ return result;
114
+ }),
72
115
  define("playbooks.enable", (input: OperationInput) => transitionPlaybook(artifacts, string(input, "id"), "enable", eventContext(input))),
73
116
  define("playbooks.disable", (input: OperationInput) => transitionPlaybook(artifacts, string(input, "id"), "disable", eventContext(input))),
74
- define("playbooks.assign_project", (input: OperationInput) => assignPlaybookProject(artifacts, scopes, string(input, "id"), optionalString(input, "project_root"))),
117
+ define("playbooks.assign_project", (input: OperationInput) => assignPlaybookProject(artifacts, artifactScopes, string(input, "id"), optionalString(input, "project_root"))),
75
118
  define("playbooks.update", (input: OperationInput) => updatePlaybook(artifacts, string(input, "id"), {
76
119
  title: optionalString(input, "title"), body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
77
120
  }, eventContext(input))),
@@ -12,7 +12,7 @@
12
12
  * skills.run depends on the Task-domain ports (TaskEventStore, TaskScopeStore) as
13
13
  * constructor parameters. These are shared port contracts every module may depend on,
14
14
  * the same way every module already depends on ArtifactStore — not "another module's
15
- * infrastructure" in the sense of a concrete class. skill-execution.ts already has this
15
+ * infrastructure" in the sense of a concrete class. workflow-execution.ts already has this
16
16
  * port dependency pre-existing; untangling it is a separate, larger concern than this
17
17
  * extraction.
18
18
  */
@@ -23,7 +23,7 @@ import type { ArtifactScopeStore } from "../ports/artifact-scope-store.ts";
23
23
  import type { ArtifactStore } from "../ports/artifact-store.ts";
24
24
  import type { TaskEventStore } from "../ports/task-event-store.ts";
25
25
  import type { TaskScopeStore } from "../ports/task-scope-store.ts";
26
- import { instantiateSkillWorkflow } from "../skill-execution.ts";
26
+ import { instantiateSkillWorkflow } from "../workflow-execution.ts";
27
27
 
28
28
  const MODULE_ID = "skills";
29
29
 
@@ -0,0 +1,176 @@
1
+ /**
2
+ * playbook-definition.ts — compiles a Playbook's own steps/trigger/tools/arguments, plus its
3
+ * `contains` (nested) and `depends_on` (prerequisite) composition tree, into an in-memory
4
+ * SkillDefinition. This is the "recycle Papyrus Skills -> Playbooks" half of the redesign:
5
+ * rather than a second graph-materialization engine, a Playbook becomes prose that compiles
6
+ * down to the exact blueprint shape workflow Skills already use, then hands off to
7
+ * workflow-execution.ts's shared materializeWorkflowDefinition for the actual Task creation.
8
+ *
9
+ * One task blueprint per playbook-node root (a container, never itself gated) plus one per
10
+ * step (chained by sequential dependsOn); `contains`-linked playbooks nest their own root
11
+ * under the parent root and continue the parent's own step chain ("run as part of this one",
12
+ * after the parent's own steps); `depends_on`-linked playbooks compile as independent
13
+ * subtrees whose tails gate this node's first step ("complete this FIRST"). No SkillCallBlueprint
14
+ * indirection is used -- everything is inlined into one flat definition, since a Playbook's
15
+ * composition tree is fully known and owned at compile time, unlike a workflow Skill's nested
16
+ * pipeline step (which references another SKILL by id, resolved and executed independently).
17
+ */
18
+ import {
19
+ PLAYBOOK_INVOCATION_MAX_CALL_DEPTH,
20
+ PLAYBOOK_INVOCATION_MAX_CREATED_TASKS,
21
+ PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS,
22
+ } from "./constants.ts";
23
+ import type { Artifact } from "./domain/artifact.ts";
24
+ import type { SkillDefinition, SkillInputDefinition, SkillTaskBlueprint } from "./domain/skill-definition.ts";
25
+ import { validateSkillDefinition } from "./domain/skill-definition.ts";
26
+ import type { PlaybookArgument } from "./domain-services.ts";
27
+ import type { ArtifactStore } from "./ports/artifact-store.ts";
28
+
29
+ /** A non-composing edge touching a playbook node, to be mirrored onto that node's generated root task once real task ids exist -- e.g. a Rule `gates` this playbook, or this playbook `references`/`documents` a Doc. Direction is preserved exactly: `from`/`to` name whichever side is NOT the playbook, and `ownerIsFrom` says which side the playbook (now the generated root task) occupies. */
30
+ export interface PlaybookExternalLink {
31
+ rootRef: string;
32
+ relation: string;
33
+ otherArtifactId: string;
34
+ /** true: playbook (root task) is the edge's `from`; false: playbook (root task) is the edge's `to`. */
35
+ ownerIsFrom: boolean;
36
+ }
37
+
38
+ export interface CompiledPlaybook {
39
+ definition: SkillDefinition;
40
+ /** The very first real leaf task in the whole tree's reading order -- what a caller should focus once materialized. */
41
+ entryRef: string;
42
+ externalLinks: PlaybookExternalLink[];
43
+ }
44
+
45
+ function requirePlaybook(artifacts: ArtifactStore, id: string): Artifact {
46
+ const playbook = artifacts.get(id);
47
+ if (!playbook) throw new Error(`playbook artifact "${id}" not found`);
48
+ if (playbook.kind !== "playbook") throw new Error(`artifact "${id}" is not a playbook`);
49
+ return playbook;
50
+ }
51
+
52
+ function stepsOf(playbook: Artifact): string[] {
53
+ return Array.isArray(playbook.extra["steps"]) ? playbook.extra["steps"].filter((step): step is string => typeof step === "string") : [];
54
+ }
55
+
56
+ function toolsOf(playbook: Artifact): string[] {
57
+ return Array.isArray(playbook.extra["tools"]) ? playbook.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
58
+ }
59
+
60
+ function argumentsOf(playbook: Artifact): PlaybookArgument[] {
61
+ return Array.isArray(playbook.extra["arguments"]) ? (playbook.extra["arguments"] as PlaybookArgument[]) : [];
62
+ }
63
+
64
+ /** The generated container task's own body -- purpose and context only. Steps are separate child tasks, so they are not re-listed here (that was the old text-dump shape). */
65
+ function rootTaskBody(playbook: Artifact): string {
66
+ const trigger = typeof playbook.extra["trigger"] === "string" ? playbook.extra["trigger"] : "manual invocation";
67
+ const tools = toolsOf(playbook);
68
+ return [
69
+ `Playbook "${playbook.title}".`,
70
+ `Trigger: ${trigger}`,
71
+ ...(playbook.body ? [`Context: ${playbook.body}`] : []),
72
+ ...(tools.length > 0 ? [`Tools: ${tools.join(", ")}`] : []),
73
+ "This task contains its steps as child tasks -- work through them in order as each becomes focused.",
74
+ ].join("\n");
75
+ }
76
+
77
+ function stepTitle(step: string): string {
78
+ const firstLine = step.split("\n")[0]!.trim();
79
+ return firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
80
+ }
81
+
82
+ interface CompileContext {
83
+ tasks: SkillTaskBlueprint[];
84
+ inputs: Record<string, SkillInputDefinition>;
85
+ externalLinks: PlaybookExternalLink[];
86
+ refCounter: { n: number };
87
+ }
88
+
89
+ interface CompileNodeResult {
90
+ rootRef: string;
91
+ /** First real leaf in this subtree's own reading order -- rootRef itself when the node has neither steps nor nested children. */
92
+ headRef: string;
93
+ /** Last real leaf in this subtree's own reading order -- what an enclosing "nested-after" continuation or successor prerequisite gate should depend on. */
94
+ tailRef: string;
95
+ }
96
+
97
+ function mergeArgument(inputs: Record<string, SkillInputDefinition>, argument: PlaybookArgument): void {
98
+ const existing = inputs[argument.name];
99
+ inputs[argument.name] = { type: "string", required: (existing?.required ?? false) || argument.required };
100
+ }
101
+
102
+ function compileNode(
103
+ artifacts: ArtifactStore,
104
+ playbookId: string,
105
+ ctx: CompileContext,
106
+ ancestorIds: ReadonlySet<string>,
107
+ depth: number,
108
+ parentRef: string | undefined,
109
+ incomingPrecedingRefs: string[],
110
+ ): CompileNodeResult {
111
+ if (ancestorIds.has(playbookId)) throw new Error(`playbook composition cycle includes "${playbookId}"`);
112
+ if (depth > PLAYBOOK_INVOCATION_MAX_CALL_DEPTH) throw new Error(`playbook composition exceeds ${PLAYBOOK_INVOCATION_MAX_CALL_DEPTH} levels`);
113
+ const nextAncestors = new Set([...ancestorIds, playbookId]);
114
+
115
+ const playbook = requirePlaybook(artifacts, playbookId);
116
+ for (const argument of argumentsOf(playbook)) mergeArgument(ctx.inputs, argument);
117
+
118
+ const rootRef = `pb${ctx.refCounter.n++}`;
119
+ const rootBlueprint: SkillTaskBlueprint = { ref: rootRef, title: playbook.title, body: rootTaskBody(playbook), ...(parentRef ? { parent: parentRef } : {}) };
120
+ ctx.tasks.push(rootBlueprint);
121
+ if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS) throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} tasks`);
122
+
123
+ const edges = artifacts.relationships({ artifactIds: [playbookId] }).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
124
+ const prerequisiteIds = edges.filter((edge) => edge.from === playbookId && edge.relation === "depends_on").map((edge) => edge.to)
125
+ .filter((id) => artifacts.get(id)?.kind === "playbook");
126
+ const nestedIds = edges.filter((edge) => edge.from === playbookId && edge.relation === "contains").map((edge) => edge.to)
127
+ .filter((id) => artifacts.get(id)?.kind === "playbook");
128
+ for (const edge of edges) {
129
+ const isComposingFrom = edge.from === playbookId && (edge.relation === "contains" || edge.relation === "depends_on") && artifacts.get(edge.to)?.kind === "playbook";
130
+ if (isComposingFrom) continue;
131
+ if (edge.from === playbookId) ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.to, ownerIsFrom: true });
132
+ else if (edge.to === playbookId) ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.from, ownerIsFrom: false });
133
+ }
134
+
135
+ const prerequisiteTailRefs: string[] = [];
136
+ let headRef: string | undefined;
137
+ for (const prerequisiteId of prerequisiteIds) {
138
+ const result = compileNode(artifacts, prerequisiteId, ctx, nextAncestors, depth + 1, undefined, []);
139
+ prerequisiteTailRefs.push(result.tailRef);
140
+ if (headRef === undefined) headRef = result.headRef;
141
+ }
142
+
143
+ let cursorPrecedingRefs = [...incomingPrecedingRefs, ...prerequisiteTailRefs];
144
+ let tailRef = rootRef;
145
+ for (const [index, step] of stepsOf(playbook).entries()) {
146
+ const stepRef = `${rootRef}-s${index}`;
147
+ ctx.tasks.push({ ref: stepRef, title: stepTitle(step), body: step, parent: rootRef, dependsOn: cursorPrecedingRefs });
148
+ if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS) throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} tasks`);
149
+ if (headRef === undefined) headRef = stepRef;
150
+ cursorPrecedingRefs = [stepRef];
151
+ tailRef = stepRef;
152
+ }
153
+
154
+ for (const nestedId of nestedIds) {
155
+ const result = compileNode(artifacts, nestedId, ctx, nextAncestors, depth + 1, rootRef, cursorPrecedingRefs);
156
+ if (headRef === undefined) headRef = result.headRef;
157
+ cursorPrecedingRefs = [result.tailRef];
158
+ tailRef = result.tailRef;
159
+ }
160
+
161
+ if (tailRef === rootRef && cursorPrecedingRefs.length > 0) rootBlueprint.dependsOn = cursorPrecedingRefs;
162
+ return { rootRef, headRef: headRef ?? rootRef, tailRef };
163
+ }
164
+
165
+ /** Pure and read-only: creates no artifacts. Cycle/depth-bounded exactly like playbookInvocation's own traversal, but a composition cycle here is a hard error (real Tasks would be created, unlike a text render degrading to a marker). */
166
+ export function compilePlaybookDefinition(artifacts: ArtifactStore, playbookId: string): CompiledPlaybook {
167
+ const ctx: CompileContext = { tasks: [], inputs: {}, externalLinks: [], refCounter: { n: 0 } };
168
+ const { headRef } = compileNode(artifacts, playbookId, ctx, new Set(), 0, undefined, []);
169
+ const definition = validateSkillDefinition({
170
+ version: 1,
171
+ inputs: ctx.inputs,
172
+ blueprints: { docs: [], rules: [], tasks: ctx.tasks, skills: [] },
173
+ links: [],
174
+ });
175
+ return { definition, entryRef: headRef, externalLinks: ctx.externalLinks };
176
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * playbook-execution.ts — playbooks.invoke's real implementation: compile the Playbook's
3
+ * composition tree (playbook-definition.ts) into a SkillDefinition, materialize it through
4
+ * workflow-execution.ts's shared engine, mirror any pre-existing Rule/Doc links onto the
5
+ * generated root task, and report which task to focus. No text is rendered here -- every
6
+ * step is its own Task, and only the currently-focused one is ever surfaced to an agent
7
+ * (via the existing Task Focus system-prompt pointer), which is what actually avoids the
8
+ * old text-dump problem: one page at a time, not the whole book at once.
9
+ */
10
+ import type { SkillArgumentValue } from "./domain/skill-definition.ts";
11
+ import { compilePlaybookDefinition } from "./playbook-definition.ts";
12
+ import type { ArtifactStore } from "./ports/artifact-store.ts";
13
+ import { requireAtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
14
+ import { materializeWorkflowDefinition, type WorkflowRunHistory } from "./workflow-execution.ts";
15
+ import type { TaskExecutionPlan } from "./task-execution.ts";
16
+
17
+ export interface InvokePlaybookInput {
18
+ runId?: string;
19
+ arguments?: Record<string, unknown>;
20
+ }
21
+
22
+ export interface PlaybookInvocationResult {
23
+ playbookId: string;
24
+ runId: string;
25
+ arguments: Record<string, SkillArgumentValue>;
26
+ created: { docs: string[]; rules: string[]; tasks: string[] };
27
+ rootTaskIds: string[];
28
+ /** The one task to focus -- the first real leaf in the whole composition tree's reading order (deepest prerequisite's own first step, or this playbook's own first step, or its first nested child's, or the container task itself when there is nothing else). */
29
+ entryTaskId: string;
30
+ execution: TaskExecutionPlan;
31
+ }
32
+
33
+ export interface PlaybookMissingArguments {
34
+ playbookId: string;
35
+ /** Nothing was created: ask the human for these (discuss tool, live:true) before invoking again with them supplied. */
36
+ missingArguments: string[];
37
+ }
38
+
39
+ function missingRequiredInputs(inputs: Record<string, { required?: boolean; default?: SkillArgumentValue }>, provided: Record<string, unknown>): string[] {
40
+ return Object.entries(inputs)
41
+ .filter(([name, input]) => input.required && provided[name] === undefined && input.default === undefined)
42
+ .map(([name]) => name);
43
+ }
44
+
45
+ /** Reads each newly-created task's own playbookRun.ref tag back off the store -- avoids threading an extra ref-to-id map out of materializeWorkflowDefinition's existing, already-stable return shape. */
46
+ function resolveRefToTaskId(artifacts: ArtifactStore, taskIds: string[]): Map<string, string> {
47
+ const map = new Map<string, string>();
48
+ for (const taskId of taskIds) {
49
+ const lineage = artifacts.get(taskId)?.extra["playbookRun"];
50
+ if (typeof lineage !== "object" || lineage === null || Array.isArray(lineage)) continue;
51
+ const ref = (lineage as Record<string, unknown>)["ref"];
52
+ if (typeof ref === "string") map.set(ref, taskId);
53
+ }
54
+ return map;
55
+ }
56
+
57
+ export function invokePlaybook(
58
+ artifacts: ArtifactStore,
59
+ playbookId: string,
60
+ input: InvokePlaybookInput,
61
+ history?: WorkflowRunHistory,
62
+ ): PlaybookInvocationResult | PlaybookMissingArguments {
63
+ const compiled = compilePlaybookDefinition(artifacts, playbookId);
64
+ const missingArguments = missingRequiredInputs(compiled.definition.inputs, input.arguments ?? {});
65
+ if (missingArguments.length > 0) return { playbookId, missingArguments };
66
+
67
+ const atomic = history ? history.events.atomic.bind(history.events) : requireAtomicArtifactStore(artifacts).atomic.bind(requireAtomicArtifactStore(artifacts));
68
+ return atomic(() => {
69
+ const result = materializeWorkflowDefinition(
70
+ artifacts,
71
+ { ownerId: playbookId, extraKey: "playbookRun", labelPrefix: "playbook-run" },
72
+ compiled.definition,
73
+ { runId: input.runId, arguments: input.arguments, focusRef: compiled.entryRef },
74
+ history,
75
+ new Set(),
76
+ 0,
77
+ );
78
+ const refToTaskId = resolveRefToTaskId(artifacts, result.created.tasks);
79
+ for (const link of compiled.externalLinks) {
80
+ const taskId = refToTaskId.get(link.rootRef);
81
+ if (!taskId) continue; // defensive -- every rootRef this compiler emits is always materialized
82
+ if (link.ownerIsFrom) artifacts.link({ from: taskId, relation: link.relation, to: link.otherArtifactId });
83
+ else artifacts.link({ from: link.otherArtifactId, relation: link.relation, to: taskId });
84
+ }
85
+ return {
86
+ playbookId,
87
+ runId: result.runId,
88
+ arguments: result.arguments,
89
+ created: { docs: result.created.docs, rules: result.created.rules, tasks: result.created.tasks },
90
+ rootTaskIds: result.rootTaskIds,
91
+ entryTaskId: result.entryTaskId!,
92
+ execution: result.execution,
93
+ };
94
+ });
95
+ }
package/src/service.ts CHANGED
@@ -409,6 +409,7 @@ function handlers(
409
409
  "playbooks.list": forwardToModule("playbooks.list"),
410
410
  "playbooks.show": forwardToModule("playbooks.show"),
411
411
  "playbooks.invoke": forwardToModule("playbooks.invoke"),
412
+ "playbooks.preview": forwardToModule("playbooks.preview"),
412
413
  "playbooks.enable": forwardToModule("playbooks.enable"),
413
414
  "playbooks.disable": forwardToModule("playbooks.disable"),
414
415
  "playbooks.assign_project": forwardToModule("playbooks.assign_project"),
@@ -480,7 +481,7 @@ export function createPapyrusService(path: string): PapyrusService {
480
481
  moduleRegistry.registerAll(docsOperations(artifacts, artifactScopes, authority));
481
482
  moduleRegistry.registerAll(rulesOperations(artifacts, artifactScopes));
482
483
  moduleRegistry.registerAll(skillsOperations({ artifacts, events, scopes, artifactScopes, authority }));
483
- moduleRegistry.registerAll(playbooksOperations(artifacts, artifactScopes));
484
+ moduleRegistry.registerAll(playbooksOperations({ artifacts, events, scopes, artifactScopes, tasks }));
484
485
  moduleRegistry.registerAll(graphProjectionOperations(artifacts, projections, authority));
485
486
  const registry = handlers(artifacts, gates, tasks, notes, events, scopes, () => migrateDb(db), moduleRegistry, authority);
486
487
  const state = (): SchemaState => {
@@ -26,9 +26,25 @@ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
26
26
  export interface InstantiateSkillWorkflowInput {
27
27
  runId?: string;
28
28
  arguments?: Record<string, unknown>;
29
+ /** When set, names one blueprint task ref whose resolved real task id is returned as `entryTaskId` -- e.g. the first-in-reading-order step of a compiled Playbook, so the caller can focus it without recomputing the ref-to-id mapping externally. */
30
+ focusRef?: string;
29
31
  }
30
32
 
31
- export interface SkillWorkflowRunResult {
33
+ /**
34
+ * Identifies who owns a materialized run for tagging purposes: which artifact gets the
35
+ * `triggers` edges to its root tasks, which extra-bag key records run lineage on each created
36
+ * artifact, and which label prefix scopes them. Defaults used by instantiateSkillWorkflow
37
+ * (ownerId: the skill's own id, extraKey: "skillRun", labelPrefix: "skill-run") are unchanged
38
+ * from before this was made pluggable -- a Playbook-compiled run supplies its own (playbook id,
39
+ * "playbookRun", "playbook-run") instead, the only thing that actually differs between the two.
40
+ */
41
+ export interface WorkflowLineage {
42
+ ownerId: string;
43
+ extraKey: string;
44
+ labelPrefix: string;
45
+ }
46
+
47
+ export interface WorkflowRunResult {
32
48
  skillId: string;
33
49
  runId: string;
34
50
  arguments: Record<string, SkillArgumentValue>;
@@ -41,6 +57,8 @@ export interface SkillWorkflowRunResult {
41
57
  };
42
58
  /** Real starting points: for a nested skill-call root step, that nested run's own root tasks (recursively), not just "all its tasks". */
43
59
  rootTaskIds: string[];
60
+ /** Resolved from input.focusRef when supplied -- the one real task id a caller (e.g. Playbook invocation) should focus, undefined when focusRef was not requested or names an unknown ref. */
61
+ entryTaskId?: string;
44
62
  /** Scoped to this definition's own directly-created tasks only -- nested runs' tasks are real, graph-linked, and visible via /tasks graph, but not folded into this projection. */
45
63
  execution: TaskExecutionPlan;
46
64
  }
@@ -98,15 +116,15 @@ function renderDefinition(definition: SkillDefinition, arguments_: Record<string
98
116
  return validateSkillDefinition(rendered);
99
117
  }
100
118
 
101
- function withRunLabel(labels: string[] | undefined, runId: string): string[] {
102
- return [...new Set([...(labels ?? []), `skill-run:${runId}`])];
119
+ function withRunLabel(labels: string[] | undefined, labelPrefix: string, runId: string): string[] {
120
+ return [...new Set([...(labels ?? []), `${labelPrefix}:${runId}`])];
103
121
  }
104
122
 
105
- function executionGraph(tasks: Artifact[], definition: SkillDefinition, ids: Map<string, string>): TaskGraph {
123
+ function executionGraph(tasks: Artifact[], definition: SkillDefinition, ids: Map<string, string>, extraKey: string): TaskGraph {
106
124
  const byRef = new Map(definition.blueprints.tasks.map((task) => [task.ref, task]));
107
125
  const nodes: TaskNode[] = tasks.map((task) => {
108
- const ref = task.extra["skillRun"] && typeof task.extra["skillRun"] === "object"
109
- ? (task.extra["skillRun"] as Record<string, unknown>)["ref"] as string
126
+ const ref = task.extra[extraKey] && typeof task.extra[extraKey] === "object"
127
+ ? (task.extra[extraKey] as Record<string, unknown>)["ref"] as string
110
128
  : "";
111
129
  const blueprint = byRef.get(ref)!;
112
130
  return {
@@ -120,7 +138,8 @@ function executionGraph(tasks: Artifact[], definition: SkillDefinition, ids: Map
120
138
  return { nodes, rootIds: nodes.filter((node) => node.parentIds.length === 0).map((node) => node.task.id) };
121
139
  }
122
140
 
123
- type SkillWorkflowHistory = { events: TaskEventStore; scopes: TaskScopeStore; projectRoot: string; context?: TaskEventContext };
141
+ /** projectRoot is optional -- skills.run always supplies one (workflow Skill runs are always project-scoped today), while a Playbook invocation may legitimately be ad hoc/cross-project (e.g. a lab-deploy playbook not tied to any one repo), landing its tasks in the same "unscoped" bucket Tasks.create already supports for a caller that omits projectRoot entirely. */
142
+ export type WorkflowRunHistory = { events: TaskEventStore; scopes: TaskScopeStore; projectRoot?: string; context?: TaskEventContext };
124
143
 
125
144
  /**
126
145
  * Public entry point: wraps one complete pipeline run (including every nested sub-pipeline
@@ -133,8 +152,8 @@ export function instantiateSkillWorkflow(
133
152
  artifacts: ArtifactStore,
134
153
  skillId: string,
135
154
  input: InstantiateSkillWorkflowInput = {},
136
- history?: SkillWorkflowHistory,
137
- ): SkillWorkflowRunResult {
155
+ history?: WorkflowRunHistory,
156
+ ): WorkflowRunResult {
138
157
  const run = () => runWorkflowSteps(artifacts, skillId, input, history, new Set(), 0);
139
158
  if (history) return history.events.atomic(run);
140
159
  return requireAtomicArtifactStore(artifacts).atomic(run);
@@ -154,19 +173,53 @@ function runWorkflowSteps(
154
173
  artifacts: ArtifactStore,
155
174
  skillId: string,
156
175
  input: InstantiateSkillWorkflowInput,
157
- history: SkillWorkflowHistory | undefined,
176
+ history: WorkflowRunHistory | undefined,
158
177
  ancestorSkillIds: ReadonlySet<string>,
159
178
  depth: number,
160
- ): SkillWorkflowRunResult {
179
+ ): WorkflowRunResult {
161
180
  if (ancestorSkillIds.has(skillId)) throw new Error(`skill workflow nesting cycle includes "${skillId}"`);
162
181
  if (depth > SKILL_WORKFLOW_MAX_NESTING_DEPTH) throw new Error(`skill workflow nesting exceeds ${SKILL_WORKFLOW_MAX_NESTING_DEPTH} levels`);
163
182
  const nextAncestors = new Set([...ancestorSkillIds, skillId]);
164
-
165
183
  const { definition } = requireWorkflowSkill(artifacts, skillId);
166
- const projectRoot = history ? normalizeProjectRoot(history.projectRoot) : undefined;
184
+ return materializeWorkflowDefinition(
185
+ artifacts,
186
+ { ownerId: skillId, extraKey: "skillRun", labelPrefix: "skill-run" },
187
+ definition,
188
+ input,
189
+ history,
190
+ nextAncestors,
191
+ depth,
192
+ );
193
+ }
194
+
195
+ /**
196
+ * The definition-materialization core, shared by workflow Skills (instantiateSkillWorkflow,
197
+ * via runWorkflowSteps above) and Playbook invocation (playbook-execution.ts): given an
198
+ * ALREADY-RESOLVED SkillDefinition -- fetched from a persisted Skill artifact for the Skill
199
+ * path, compiled in-memory from a Playbook's steps/trigger/arguments and its contains/
200
+ * depends_on composition tree for the Playbook path -- creates every blueprint artifact,
201
+ * wires dependsOn/parent/links, recurses into nested skill-call pipeline steps (a no-op for a
202
+ * Playbook-compiled definition, which never populates blueprints.skills), and tags every
203
+ * created artifact and the run's containing labels via `lineage` rather than a hardcoded
204
+ * "skillRun"/"skill-run" shape -- the only thing that differs between the two callers.
205
+ * `ancestorSkillIds`/`depth` are the same cycle/nesting-depth tracking runWorkflowSteps already
206
+ * enforced before this was extracted; a Playbook caller with no nested skill-calls to recurse
207
+ * into passes an empty set and depth 0 and never revisits this function itself.
208
+ */
209
+ export function materializeWorkflowDefinition(
210
+ artifacts: ArtifactStore,
211
+ lineage: WorkflowLineage,
212
+ definition: SkillDefinition,
213
+ input: InstantiateSkillWorkflowInput,
214
+ history: WorkflowRunHistory | undefined,
215
+ ancestorSkillIds: ReadonlySet<string>,
216
+ depth: number,
217
+ ): WorkflowRunResult {
218
+ const { ownerId, extraKey, labelPrefix } = lineage;
219
+ const projectRoot = history?.projectRoot !== undefined ? normalizeProjectRoot(history.projectRoot) : undefined;
167
220
  const arguments_ = resolveSkillArguments(definition, input.arguments);
168
221
  const rendered = renderDefinition(definition, arguments_);
169
- const runId = normalizeRunId(skillId, input.runId);
222
+ const runId = normalizeRunId(ownerId, input.runId);
170
223
  const refs = [
171
224
  ...rendered.blueprints.docs.map(({ ref }) => ref),
172
225
  ...rendered.blueprints.rules.map(({ ref }) => ref),
@@ -194,22 +247,22 @@ function runWorkflowSteps(
194
247
  title: blueprint.title,
195
248
  body: blueprint.body,
196
249
  subtype: blueprint.subtype,
197
- labels: withRunLabel(blueprint.labels, runId),
198
- extra: { ...(blueprint.extra ?? {}), skillRun: { id: runId, skillId, ref: blueprint.ref } },
250
+ labels: withRunLabel(blueprint.labels, labelPrefix, runId),
251
+ extra: { ...(blueprint.extra ?? {}), [extraKey]: { id: runId, ownerId, ref: blueprint.ref } },
199
252
  }));
200
253
  const rules = rendered.blueprints.rules.map((blueprint) => artifacts.create({
201
254
  id: ids.get(blueprint.ref),
202
255
  kind: "rule",
203
256
  title: blueprint.title,
204
257
  body: blueprint.body,
205
- labels: withRunLabel(blueprint.labels, runId),
258
+ labels: withRunLabel(blueprint.labels, labelPrefix, runId),
206
259
  extra: {
207
260
  ...(blueprint.extra ?? {}),
208
261
  ...(blueprint.condition ? { condition: blueprint.condition } : {}),
209
262
  ...(blueprint.action ? { action: blueprint.action } : {}),
210
263
  ...(blueprint.severity ? { severity: blueprint.severity } : {}),
211
- skillRun: { id: runId, skillId, ref: blueprint.ref },
212
- scope: { type: "skill-run", runId, taskIds },
264
+ [extraKey]: { id: runId, ownerId, ref: blueprint.ref },
265
+ scope: { type: labelPrefix, runId, taskIds },
213
266
  },
214
267
  }));
215
268
  const tasks = rendered.blueprints.tasks.map((blueprint) => {
@@ -218,16 +271,16 @@ function runWorkflowSteps(
218
271
  kind: "task",
219
272
  title: blueprint.title,
220
273
  body: blueprint.body,
221
- labels: withRunLabel(blueprint.labels, runId),
222
- extra: { ...(blueprint.extra ?? {}), skillRun: { id: runId, skillId, ref: blueprint.ref } },
274
+ labels: withRunLabel(blueprint.labels, labelPrefix, runId),
275
+ extra: { ...(blueprint.extra ?? {}), [extraKey]: { id: runId, ownerId, ref: blueprint.ref } },
223
276
  });
224
277
  if (history) {
225
- history.scopes.assign(task.id, projectRoot, "cwd");
278
+ history.scopes.assign(task.id, projectRoot, projectRoot ? "cwd" : "unscoped");
226
279
  history.events.append({
227
280
  taskId: task.id,
228
281
  type: "created",
229
282
  actor: history.context?.actor ?? "system",
230
- source: history.context?.source ?? "skill-run",
283
+ source: history.context?.source ?? labelPrefix,
231
284
  toStatus: task.status as TaskStatus,
232
285
  ...(history.context?.sessionId === undefined ? {} : { sessionId: history.context.sessionId }),
233
286
  ...(history.context?.reason === undefined ? {} : { reason: history.context.reason }),
@@ -240,7 +293,7 @@ function runWorkflowSteps(
240
293
  // nested run actually produced. stepTaskIds/stepRootTaskIds map EVERY step ref (task or
241
294
  // skill-call) to the task id(s) it resolves to, so dependsOn/parent wiring below treats
242
295
  // both kinds of step uniformly.
243
- const nestedRuns: SkillWorkflowRunResult[] = [];
296
+ const nestedRuns: WorkflowRunResult[] = [];
244
297
  const stepTaskIds = new Map<string, string[]>(tasks.map((task, index) => [rendered.blueprints.tasks[index]!.ref, [task.id]]));
245
298
  const stepRootTaskIds = new Map<string, string[]>(
246
299
  tasks.map((task, index) => [rendered.blueprints.tasks[index]!.ref, (rendered.blueprints.tasks[index]!.dependsOn?.length ?? 0) === 0 ? [task.id] : []]),
@@ -251,7 +304,7 @@ function runWorkflowSteps(
251
304
  call.skillId,
252
305
  { runId: `${runId}-${call.ref}`, arguments: call.arguments },
253
306
  history,
254
- nextAncestors,
307
+ ancestorSkillIds,
255
308
  depth + 1,
256
309
  );
257
310
  nestedRuns.push(nested);
@@ -298,14 +351,14 @@ function runWorkflowSteps(
298
351
  .flatMap((call) => stepRootTaskIds.get(call.ref) ?? []),
299
352
  ];
300
353
  for (const task of rendered.blueprints.tasks) {
301
- if ((task.dependsOn?.length ?? 0) === 0) artifacts.link({ from: skillId, relation: "triggers", to: ids.get(task.ref)! });
354
+ if ((task.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: ids.get(task.ref)! });
302
355
  }
303
356
  for (const call of rendered.blueprints.skills as SkillCallBlueprint[]) {
304
- if ((call.dependsOn?.length ?? 0) === 0) artifacts.link({ from: skillId, relation: "triggers", to: call.skillId });
357
+ if ((call.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: call.skillId });
305
358
  }
306
359
 
307
360
  return {
308
- skillId,
361
+ skillId: ownerId,
309
362
  runId,
310
363
  arguments: arguments_,
311
364
  created: {
@@ -315,6 +368,7 @@ function runWorkflowSteps(
315
368
  skillRuns: [...nestedRuns.map((run) => run.runId), ...nestedRuns.flatMap((run) => run.created.skillRuns)],
316
369
  },
317
370
  rootTaskIds,
318
- execution: projectTaskExecution(executionGraph(tasks, rendered, ids)),
371
+ ...(input.focusRef !== undefined && ids.has(input.focusRef) ? { entryTaskId: ids.get(input.focusRef)! } : {}),
372
+ execution: projectTaskExecution(executionGraph(tasks, rendered, ids, extraKey)),
319
373
  };
320
374
  }