@danypops/papyrus 0.35.2 → 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 +1 -1
- package/src/cli.ts +46 -5
- package/src/constants.ts +9 -0
- package/src/domain/skill-definition.ts +1 -1
- package/src/domain-services.ts +106 -19
- package/src/index.ts +1 -1
- package/src/modules/playbooks.ts +59 -11
- package/src/modules/skills.ts +2 -2
- package/src/playbook-definition.ts +176 -0
- package/src/playbook-execution.ts +95 -0
- package/src/service.ts +6 -1
- package/src/{skill-execution.ts → workflow-execution.ts} +83 -29
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -113,13 +113,17 @@ 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]
|
|
123
|
+
papyrus playbooks contain <parent-id> <child-id> [--json]
|
|
124
|
+
papyrus playbooks uncontain <parent-id> <child-id> [--json]
|
|
125
|
+
papyrus playbooks depend <id> <dependency-id> [--json]
|
|
126
|
+
papyrus playbooks undepend <id> <dependency-id> [--json]
|
|
123
127
|
papyrus notes capture <request> [--title <title>] [--json]
|
|
124
128
|
papyrus notes list [--status <draft|active|archived>] [--text <query>] [--limit <count>] [--json]
|
|
125
129
|
papyrus notes show <id> [--json]
|
|
@@ -859,11 +863,20 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
|
|
|
859
863
|
human = `${artifactLabel(artifact)}\n\n${artifact.body ?? ""}`;
|
|
860
864
|
break;
|
|
861
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
|
+
}
|
|
862
873
|
case "invoke": {
|
|
863
874
|
if (!id || second) throw new Error("playbooks invoke requires exactly one playbook id");
|
|
864
|
-
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 });
|
|
865
876
|
result = invocation;
|
|
866
|
-
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.`;
|
|
867
880
|
break;
|
|
868
881
|
}
|
|
869
882
|
case "enable":
|
|
@@ -889,8 +902,36 @@ export async function runPlaybooksCli(args: string[], client: TaskCliClient): Pr
|
|
|
889
902
|
human = `${artifactLabel(artifact)}`;
|
|
890
903
|
break;
|
|
891
904
|
}
|
|
905
|
+
case "contain": {
|
|
906
|
+
if (!id || !second || positional.length !== 3) throw new Error("playbooks contain requires a parent id and child id");
|
|
907
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.contain", { parent_id: id, child_id: second });
|
|
908
|
+
result = artifact;
|
|
909
|
+
human = `Nested: ${second} → ${artifactLabel(artifact)}`;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
case "uncontain": {
|
|
913
|
+
if (!id || !second || positional.length !== 3) throw new Error("playbooks uncontain requires a parent id and child id");
|
|
914
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.uncontain", { parent_id: id, child_id: second });
|
|
915
|
+
result = artifact;
|
|
916
|
+
human = `Removed ${second} from ${artifactLabel(artifact)}`;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
case "depend": {
|
|
920
|
+
if (!id || !second || positional.length !== 3) throw new Error("playbooks depend requires a playbook id and prerequisite id");
|
|
921
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.depend", { id, dependency_id: second });
|
|
922
|
+
result = artifact;
|
|
923
|
+
human = `Dependency added: ${artifactLabel(artifact)} waits for ${second}`;
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
case "undepend": {
|
|
927
|
+
if (!id || !second || positional.length !== 3) throw new Error("playbooks undepend requires a playbook id and prerequisite id");
|
|
928
|
+
const artifact = await client.call<Record<string, unknown>, CliArtifact>("playbooks.undepend", { id, dependency_id: second });
|
|
929
|
+
result = artifact;
|
|
930
|
+
human = `Dependency removed: ${artifactLabel(artifact)} no longer waits for ${second}`;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
892
933
|
default:
|
|
893
|
-
throw new Error("playbooks action must be create, list, show, invoke, enable, disable, assign-project, or
|
|
934
|
+
throw new Error("playbooks action must be create, list, show, invoke, preview, enable, disable, assign-project, update, contain, uncontain, depend, or undepend");
|
|
894
935
|
}
|
|
895
936
|
return json ? JSON.stringify(result) : human;
|
|
896
937
|
}
|
package/src/constants.ts
CHANGED
|
@@ -108,9 +108,18 @@ export const SKILL_MAX_RENDERED_BYTES = 1_048_576;
|
|
|
108
108
|
export const SKILL_INVOCATION_MAX_LINKED_ARTIFACTS = 20;
|
|
109
109
|
export const SKILL_INVOCATION_MAX_CALL_DEPTH = 4;
|
|
110
110
|
export const PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS = 20;
|
|
111
|
+
/** Mirrors SKILL_INVOCATION_MAX_CALL_DEPTH: a playbook-calls-playbook edge chain is bounded the same way a skill-calls-skill chain is. */
|
|
112
|
+
export const PLAYBOOK_INVOCATION_MAX_CALL_DEPTH = 4;
|
|
111
113
|
export const PLAYBOOK_ARGUMENT_MAX_COUNT = 20;
|
|
112
114
|
export const PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH = 64;
|
|
113
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;
|
|
114
123
|
|
|
115
124
|
/**
|
|
116
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 (
|
|
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`
|
package/src/domain-services.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH,
|
|
8
8
|
PLAYBOOK_ARGUMENT_MAX_COUNT,
|
|
9
9
|
PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH,
|
|
10
|
+
PLAYBOOK_INVOCATION_MAX_CALL_DEPTH,
|
|
10
11
|
PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS,
|
|
11
12
|
RULE_TEXT_HARD_LIMIT_CHARACTERS,
|
|
12
13
|
SKILL_INVOCATION_MAX_CALL_DEPTH,
|
|
@@ -523,10 +524,22 @@ export function transitionSkill(artifacts: ArtifactStore, id: string, action: Sk
|
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
/**
|
|
526
|
-
* Playbooks: a trigger and an ordered list of steps
|
|
527
|
-
* different beast from Skills
|
|
528
|
-
*
|
|
529
|
-
*
|
|
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.
|
|
530
543
|
*/
|
|
531
544
|
export interface PlaybookArgument {
|
|
532
545
|
name: string;
|
|
@@ -629,15 +642,44 @@ export function transitionPlaybook(artifacts: ArtifactStore, id: string, action:
|
|
|
629
642
|
return artifacts.setStatus(id, target, context)!;
|
|
630
643
|
}
|
|
631
644
|
|
|
632
|
-
/**
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
645
|
+
/** Idempotent (INSERT OR IGNORE at the storage layer): containing an already-nested child is a no-op, not an error. Both contains/part_of edges are written atomically -- matches tasks.contain's own shape. */
|
|
646
|
+
export function containPlaybook(artifacts: ArtifactStore, parentId: string, childId: string, context?: ArtifactEventContext): Artifact {
|
|
647
|
+
requireLocallyOwnedContent(requireKind(artifacts, parentId, "playbook"));
|
|
648
|
+
requireLocallyOwnedContent(requireKind(artifacts, childId, "playbook"));
|
|
649
|
+
if (parentId === childId) throw new Error(`playbook "${parentId}" cannot contain itself`);
|
|
650
|
+
artifacts.link({ from: parentId, relation: "contains", to: childId }, context);
|
|
651
|
+
artifacts.link({ from: childId, relation: "part_of", to: parentId }, context);
|
|
652
|
+
return showPlaybook(artifacts, parentId);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Idempotent: uncontaining an already-absent nesting is a no-op. Both contains/part_of edges are removed atomically. */
|
|
656
|
+
export function uncontainPlaybook(artifacts: ArtifactStore, parentId: string, childId: string, context?: ArtifactEventContext): Artifact {
|
|
657
|
+
requireLocallyOwnedContent(requireKind(artifacts, parentId, "playbook"));
|
|
658
|
+
requireLocallyOwnedContent(requireKind(artifacts, childId, "playbook"));
|
|
659
|
+
artifacts.unlink({ from: parentId, relation: "contains", to: childId }, context);
|
|
660
|
+
artifacts.unlink({ from: childId, relation: "part_of", to: parentId }, context);
|
|
661
|
+
return showPlaybook(artifacts, parentId);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Idempotent: depending on an already-declared prerequisite is a no-op, not an error. Unlike tasks.depend, this never rejects a cycle at write time -- playbookInvocation degrades a composition cycle to a marker at render time instead, the same posture already established for Skill-calls-Skill and Playbook-calls-Playbook via `contains`. */
|
|
665
|
+
export function dependPlaybook(artifacts: ArtifactStore, id: string, dependencyId: string, context?: ArtifactEventContext): Artifact {
|
|
666
|
+
requireLocallyOwnedContent(requireKind(artifacts, id, "playbook"));
|
|
667
|
+
requireLocallyOwnedContent(requireKind(artifacts, dependencyId, "playbook"));
|
|
668
|
+
if (id === dependencyId) throw new Error(`playbook "${id}" cannot depend on itself`);
|
|
669
|
+
artifacts.link({ from: id, relation: "depends_on", to: dependencyId }, context);
|
|
670
|
+
return showPlaybook(artifacts, id);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Idempotent: undepending an already-absent prerequisite is a no-op. */
|
|
674
|
+
export function undependPlaybook(artifacts: ArtifactStore, id: string, dependencyId: string, context?: ArtifactEventContext): Artifact {
|
|
675
|
+
requireLocallyOwnedContent(requireKind(artifacts, id, "playbook"));
|
|
676
|
+
requireLocallyOwnedContent(requireKind(artifacts, dependencyId, "playbook"));
|
|
677
|
+
artifacts.unlink({ from: id, relation: "depends_on", to: dependencyId }, context);
|
|
678
|
+
return showPlaybook(artifacts, id);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Renders trigger/body/arguments/steps/tools into readable guidance -- the flat, non-recursive part of a Playbook's own invocation, shared by the top-level render and by a nested composed call. */
|
|
682
|
+
function playbookInvocationBody(playbook: Artifact, provided: Record<string, string>): string {
|
|
641
683
|
const trigger = typeof playbook.extra["trigger"] === "string" ? playbook.extra["trigger"] : "manual invocation";
|
|
642
684
|
const steps = Array.isArray(playbook.extra["steps"]) ? playbook.extra["steps"].filter((step): step is string => typeof step === "string") : [];
|
|
643
685
|
const tools = Array.isArray(playbook.extra["tools"]) ? playbook.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
|
|
@@ -649,7 +691,7 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
|
|
|
649
691
|
return `- ${argument.name} (${qualifier}${argument.description ? `: ${argument.description}` : ""}) -- not yet provided`;
|
|
650
692
|
});
|
|
651
693
|
const missingRequired = declaredArguments.filter((argument) => argument.required && provided[argument.name] === undefined);
|
|
652
|
-
|
|
694
|
+
return [
|
|
653
695
|
`Apply Papyrus playbook "${playbook.title}".`,
|
|
654
696
|
`Trigger: ${trigger}`,
|
|
655
697
|
...(playbook.body ? [`Context: ${playbook.body}`] : []),
|
|
@@ -659,11 +701,56 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
|
|
|
659
701
|
: []),
|
|
660
702
|
...(steps.length ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
|
|
661
703
|
...(tools.length ? [`Tools: ${tools.join(", ")}`] : []),
|
|
662
|
-
].join("\n")
|
|
704
|
+
].join("\n");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Renders trigger/steps/tools/arguments into readable guidance, plus any real linked artifacts.
|
|
709
|
+
* Two relations compose recursively, each with distinct wording matching Tasks' own semantics:
|
|
710
|
+
* `contains` nests a child playbook -- its full steps render AFTER this playbook's own, as
|
|
711
|
+
* "run as part of this one". `depends_on` chains a prerequisite -- its full steps render BEFORE
|
|
712
|
+
* this playbook's own, as "complete this first". Every other relation (references, relates_to,
|
|
713
|
+
* etc.) still gets the flat one-line "Linked context" pointer, unchanged. Bounded and
|
|
714
|
+
* cycle-safe -- a composition cycle degrades to a marker instead of infinite-looping, matching
|
|
715
|
+
* skillInvocation's own cycle-safety discipline.
|
|
716
|
+
* `provided` is the caller's already-known argument values (e.g. from the conversation so far);
|
|
717
|
+
* any declared *required* argument missing from it is called out explicitly, directing the agent
|
|
718
|
+
* to discuss (live:true) rather than guess or silently proceed. `visited` and `depth` are
|
|
719
|
+
* recursion-internal; callers should not pass them.
|
|
720
|
+
*/
|
|
721
|
+
export function playbookInvocation(artifacts: ArtifactStore, id: string, provided: Record<string, string> = {}, visited: Set<string> = new Set(), depth = 0): string {
|
|
722
|
+
const playbook = requireKind(artifacts, id, "playbook");
|
|
723
|
+
visited.add(id);
|
|
724
|
+
|
|
663
725
|
const edges = artifacts.relationships({ artifactIds: [id] }).filter((edge) => edge.from === id).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
726
|
+
const linkedArtifactLines: string[] = [];
|
|
727
|
+
const nestedSections: string[] = []; // contains -- rendered after this playbook's own body
|
|
728
|
+
const prerequisiteSections: string[] = []; // depends_on -- rendered before this playbook's own body
|
|
729
|
+
for (const edge of edges) {
|
|
730
|
+
const target = artifacts.get(edge.to);
|
|
731
|
+
if (!target) continue; // dangling edge -- defensive, should not happen
|
|
732
|
+
const isComposing = target.kind === "playbook" && (edge.relation === "contains" || edge.relation === "depends_on");
|
|
733
|
+
if (!isComposing) {
|
|
734
|
+
linkedArtifactLines.push(`- ${edge.relation} ${target.kind} "${target.title}"`);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const bucket = edge.relation === "contains" ? nestedSections : prerequisiteSections;
|
|
738
|
+
const role = edge.relation === "contains" ? "nested" : "prerequisite";
|
|
739
|
+
if (visited.has(target.id)) {
|
|
740
|
+
bucket.push(`Also linked via ${edge.relation} to ${role} playbook "${target.title}" -- already invoked above in this chain, not repeated.`);
|
|
741
|
+
} else if (depth + 1 > PLAYBOOK_INVOCATION_MAX_CALL_DEPTH) {
|
|
742
|
+
bucket.push(`Also linked via ${edge.relation} to ${role} playbook "${target.title}" -- call depth limit reached, invoke it separately.`);
|
|
743
|
+
} else {
|
|
744
|
+
const nested = playbookInvocation(artifacts, target.id, provided, visited, depth + 1);
|
|
745
|
+
bucket.push(edge.relation === "contains"
|
|
746
|
+
? `Nested playbook (contains) "${target.title}" -- run as part of this one:\n${nested}`
|
|
747
|
+
: `Prerequisite playbook (depends_on) "${target.title}" -- complete this FIRST, before the steps below:\n${nested}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const sections = [...prerequisiteSections, playbookInvocationBody(playbook, provided), ...nestedSections];
|
|
752
|
+
if (linkedArtifactLines.length > 0) {
|
|
753
|
+
sections.push(["Linked context (query Papyrus for full detail before proceeding):", ...linkedArtifactLines].join("\n"));
|
|
754
|
+
}
|
|
668
755
|
return sections.join("\n\n");
|
|
669
756
|
}
|
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 {
|
|
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";
|
package/src/modules/playbooks.ts
CHANGED
|
@@ -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
|
|
5
|
-
* beast from a Skill (a mechanically instantiated
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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,29 +74,53 @@ 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",
|
|
78
|
+
"playbooks.contain", "playbooks.uncontain", "playbooks.depend", "playbooks.undepend",
|
|
54
79
|
] as const;
|
|
55
80
|
|
|
56
|
-
export
|
|
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[] {
|
|
57
92
|
const define = <Input, Output>(name: string, execute: (input: Input) => Output): OperationDefinition<Input, Output> => ({
|
|
58
93
|
name, moduleId: MODULE_ID, execute,
|
|
59
94
|
});
|
|
60
95
|
return [
|
|
61
|
-
define("playbooks.create", (input: OperationInput) => createPlaybook(artifacts,
|
|
96
|
+
define("playbooks.create", (input: OperationInput) => createPlaybook(artifacts, artifactScopes, {
|
|
62
97
|
title: string(input, "title"), body: optionalString(input, "body"), trigger: optionalString(input, "trigger"),
|
|
63
98
|
steps: input["steps"] as string[] | undefined, tools: input["tools"] as string[] | undefined,
|
|
64
99
|
arguments: input["arguments"],
|
|
65
100
|
labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
|
|
66
101
|
projectRoot: optionalString(input, "project_root"),
|
|
67
102
|
}, eventContext(input))),
|
|
68
|
-
define("playbooks.list", (input: OperationInput) => listPlaybooks(artifacts,
|
|
103
|
+
define("playbooks.list", (input: OperationInput) => listPlaybooks(artifacts, artifactScopes, artifactFilter(input))),
|
|
69
104
|
define("playbooks.show", (input: OperationInput) => showPlaybook(artifacts, string(input, "id"))),
|
|
70
|
-
define("playbooks.
|
|
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
|
+
}),
|
|
71
115
|
define("playbooks.enable", (input: OperationInput) => transitionPlaybook(artifacts, string(input, "id"), "enable", eventContext(input))),
|
|
72
116
|
define("playbooks.disable", (input: OperationInput) => transitionPlaybook(artifacts, string(input, "id"), "disable", eventContext(input))),
|
|
73
|
-
define("playbooks.assign_project", (input: OperationInput) => assignPlaybookProject(artifacts,
|
|
117
|
+
define("playbooks.assign_project", (input: OperationInput) => assignPlaybookProject(artifacts, artifactScopes, string(input, "id"), optionalString(input, "project_root"))),
|
|
74
118
|
define("playbooks.update", (input: OperationInput) => updatePlaybook(artifacts, string(input, "id"), {
|
|
75
119
|
title: optionalString(input, "title"), body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
76
120
|
}, eventContext(input))),
|
|
121
|
+
define("playbooks.contain", (input: OperationInput) => containPlaybook(artifacts, string(input, "parent_id"), string(input, "child_id"), eventContext(input))),
|
|
122
|
+
define("playbooks.uncontain", (input: OperationInput) => uncontainPlaybook(artifacts, string(input, "parent_id"), string(input, "child_id"), eventContext(input))),
|
|
123
|
+
define("playbooks.depend", (input: OperationInput) => dependPlaybook(artifacts, string(input, "id"), string(input, "dependency_id"), eventContext(input))),
|
|
124
|
+
define("playbooks.undepend", (input: OperationInput) => undependPlaybook(artifacts, string(input, "id"), string(input, "dependency_id"), eventContext(input))),
|
|
77
125
|
];
|
|
78
126
|
}
|
package/src/modules/skills.ts
CHANGED
|
@@ -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.
|
|
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 "../
|
|
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,10 +409,15 @@ 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"),
|
|
415
416
|
"playbooks.update": forwardToModule("playbooks.update"),
|
|
417
|
+
"playbooks.contain": forwardToModule("playbooks.contain"),
|
|
418
|
+
"playbooks.uncontain": forwardToModule("playbooks.uncontain"),
|
|
419
|
+
"playbooks.depend": forwardToModule("playbooks.depend"),
|
|
420
|
+
"playbooks.undepend": forwardToModule("playbooks.undepend"),
|
|
416
421
|
"skills.instantiate": (input) => {
|
|
417
422
|
const templateId = string(input, "template_id");
|
|
418
423
|
const template = artifacts.get(templateId);
|
|
@@ -476,7 +481,7 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
476
481
|
moduleRegistry.registerAll(docsOperations(artifacts, artifactScopes, authority));
|
|
477
482
|
moduleRegistry.registerAll(rulesOperations(artifacts, artifactScopes));
|
|
478
483
|
moduleRegistry.registerAll(skillsOperations({ artifacts, events, scopes, artifactScopes, authority }));
|
|
479
|
-
moduleRegistry.registerAll(playbooksOperations(artifacts, artifactScopes));
|
|
484
|
+
moduleRegistry.registerAll(playbooksOperations({ artifacts, events, scopes, artifactScopes, tasks }));
|
|
480
485
|
moduleRegistry.registerAll(graphProjectionOperations(artifacts, projections, authority));
|
|
481
486
|
const registry = handlers(artifacts, gates, tasks, notes, events, scopes, () => migrateDb(db), moduleRegistry, authority);
|
|
482
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
|
-
|
|
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 ?? []),
|
|
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
|
|
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[
|
|
109
|
-
? (task.extra[
|
|
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
|
-
|
|
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?:
|
|
137
|
-
):
|
|
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:
|
|
176
|
+
history: WorkflowRunHistory | undefined,
|
|
158
177
|
ancestorSkillIds: ReadonlySet<string>,
|
|
159
178
|
depth: number,
|
|
160
|
-
):
|
|
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
|
-
|
|
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(
|
|
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 ?? {}),
|
|
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
|
-
|
|
212
|
-
scope: { type:
|
|
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 ?? {}),
|
|
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 ??
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
}
|