@danypops/papyrus 0.41.0 → 0.42.1

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.
Files changed (78) hide show
  1. package/README.md +8 -11
  2. package/package.json +2 -2
  3. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  4. package/src/adapters/sqlite-artifact-store.ts +7 -5
  5. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  6. package/src/adapters/sqlite-gate-runner.ts +1 -1
  7. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  8. package/src/adapters/sqlite-log-store.ts +36 -17
  9. package/src/adapters/sqlite-note-event-store.ts +20 -16
  10. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  11. package/src/adapters/sqlite-task-event-store.ts +29 -21
  12. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  13. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  14. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  15. package/src/artifact-relationship-view.ts +4 -4
  16. package/src/artifact-subtree.ts +4 -2
  17. package/src/authority-registry.ts +2 -1
  18. package/src/cli.ts +794 -354
  19. package/src/client.ts +6 -3
  20. package/src/constants.ts +34 -56
  21. package/src/daemon-state.ts +4 -12
  22. package/src/daemon.ts +31 -9
  23. package/src/db.ts +153 -105
  24. package/src/discussion-service.ts +109 -44
  25. package/src/domain/artifact-event.ts +18 -5
  26. package/src/domain/artifact.ts +3 -1
  27. package/src/domain/blueprint-definition.ts +268 -0
  28. package/src/domain/checklist.ts +20 -17
  29. package/src/domain/discussion.ts +37 -18
  30. package/src/domain/gate.ts +7 -7
  31. package/src/domain/log-entry.ts +1 -1
  32. package/src/domain/note-event.ts +20 -7
  33. package/src/domain/task-event.ts +17 -7
  34. package/src/domain-services.ts +362 -288
  35. package/src/graph-projection-service.ts +34 -8
  36. package/src/id-migration.ts +17 -4
  37. package/src/index.ts +16 -11
  38. package/src/log-service.ts +6 -5
  39. package/src/log.ts +19 -0
  40. package/src/modules/discuss.ts +63 -28
  41. package/src/modules/docs.ts +74 -17
  42. package/src/modules/graph-projection.ts +20 -9
  43. package/src/modules/logs.ts +34 -22
  44. package/src/modules/notes.ts +66 -28
  45. package/src/modules/playbooks.ts +93 -32
  46. package/src/modules/rules.ts +57 -15
  47. package/src/modules/session-identity.ts +6 -2
  48. package/src/modules/tasks.ts +142 -67
  49. package/src/note-service.ts +11 -7
  50. package/src/ops.ts +134 -69
  51. package/src/playbook-definition.ts +124 -39
  52. package/src/playbook-execution.ts +18 -25
  53. package/src/ports/artifact-scope-store.ts +1 -1
  54. package/src/ports/note-event-store.ts +6 -4
  55. package/src/ports/task-event-store.ts +9 -6
  56. package/src/ports/task-focus-store.ts +17 -4
  57. package/src/ports/task-lease-store.ts +9 -4
  58. package/src/ports/task-scope-store.ts +3 -1
  59. package/src/service.ts +148 -110
  60. package/src/session-identity-service.ts +10 -2
  61. package/src/task-context.ts +28 -16
  62. package/src/task-execution.ts +4 -12
  63. package/src/task-graph-view.ts +12 -12
  64. package/src/task-relationship-view.ts +1 -3
  65. package/src/task-service.ts +168 -73
  66. package/src/vehicle/artifact-trash-vehicle.ts +26 -14
  67. package/src/vehicle/artifact-vehicle-shared.ts +32 -13
  68. package/src/vehicle/docs-vehicle.ts +50 -18
  69. package/src/vehicle/notes-vehicle.ts +26 -8
  70. package/src/vehicle/papyrus-vehicle.ts +16 -8
  71. package/src/vehicle/playbooks-vehicle.ts +88 -19
  72. package/src/vehicle/rules-vehicle.ts +58 -21
  73. package/src/vehicle/tasks-vehicle.ts +366 -54
  74. package/src/version.ts +1 -1
  75. package/src/workflow-execution.ts +198 -109
  76. package/src/domain/skill-definition.ts +0 -270
  77. package/src/modules/skills.ts +0 -158
  78. package/src/vehicle/skills-vehicle.ts +0 -194
@@ -1,20 +1,26 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { SKILL_MAX_RENDERED_BYTES, SKILL_RUN_ID_MAX_LENGTH, SKILL_WORKFLOW_MAX_NESTING_DEPTH, TASK_EXECUTION_MAX_EDGES } from "./constants.ts";
2
+ import {
3
+ SKILL_MAX_RENDERED_BYTES,
4
+ SKILL_RUN_ID_MAX_LENGTH,
5
+ SKILL_WORKFLOW_MAX_NESTING_DEPTH,
6
+ TASK_EXECUTION_MAX_EDGES,
7
+ } from "./constants.ts";
3
8
  import type { Artifact } from "./domain/artifact.ts";
4
- import { validateChecklist } from "./domain/checklist.ts";
5
9
  import {
6
- resolveSkillArguments,
7
- validateSkillDefinition,
8
- type SkillArgumentValue,
9
- type SkillCallBlueprint,
10
- type SkillDefinition,
11
- } from "./domain/skill-definition.ts";
12
- import type { ArtifactStore } from "./ports/artifact-store.ts";
10
+ type BlueprintArgumentValue,
11
+ type BlueprintDefinition,
12
+ type CallBlueprint,
13
+ resolveBlueprintArguments,
14
+ validateBlueprintDefinition,
15
+ } from "./domain/blueprint-definition.ts";
16
+ import { validateChecklist } from "./domain/checklist.ts";
13
17
  import type { TaskEventContext } from "./domain/task-event.ts";
14
- import type { TaskEventStore } from "./ports/task-event-store.ts";
15
- import type { TaskScopeStore } from "./ports/task-scope-store.ts";
16
18
  import { normalizeProjectRoot } from "./domain/task-scope.ts";
19
+ import { compilePlaybookDefinition, type PlaybookExternalLink } from "./playbook-definition.ts";
20
+ import type { ArtifactStore } from "./ports/artifact-store.ts";
17
21
  import { requireAtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
22
+ import type { TaskEventStore } from "./ports/task-event-store.ts";
23
+ import type { TaskScopeStore } from "./ports/task-scope-store.ts";
18
24
  import { projectTaskExecution, type TaskExecutionPlan } from "./task-execution.ts";
19
25
  import type { TaskGraph, TaskNode, TaskStatus } from "./task-service.ts";
20
26
 
@@ -34,7 +40,7 @@ export interface InstantiateSkillWorkflowInput {
34
40
  * Identifies who owns a materialized run for tagging purposes: which artifact gets the
35
41
  * `triggers` edges to its root tasks, which extra-bag key records run lineage on each created
36
42
  * 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
43
+ * (ownerId: the target's own id, extraKey: "skillRun", labelPrefix: "skill-run") are unchanged
38
44
  * from before this was made pluggable -- a Playbook-compiled run supplies its own (playbook id,
39
45
  * "playbookRun", "playbook-run") instead, the only thing that actually differs between the two.
40
46
  */
@@ -47,15 +53,15 @@ export interface WorkflowLineage {
47
53
  export interface WorkflowRunResult {
48
54
  skillId: string;
49
55
  runId: string;
50
- arguments: Record<string, SkillArgumentValue>;
56
+ arguments: Record<string, BlueprintArgumentValue>;
51
57
  created: {
52
58
  docs: string[];
53
59
  rules: string[];
54
60
  tasks: string[];
55
- /** Nested workflow Skill runs this pipeline triggered as pipeline steps, in execution order. */
61
+ /** Nested workflow-definition runs this pipeline triggered as pipeline steps, in execution order. */
56
62
  skillRuns: string[];
57
63
  };
58
- /** Real starting points: for a nested skill-call root step, that nested run's own root tasks (recursively), not just "all its tasks". */
64
+ /** Real starting points: for a nested call root step, that nested run's own root tasks (recursively), not just "all its tasks". */
59
65
  rootTaskIds: string[];
60
66
  /** 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
67
  entryTaskId?: string;
@@ -63,34 +69,39 @@ export interface WorkflowRunResult {
63
69
  execution: TaskExecutionPlan;
64
70
  }
65
71
 
66
- function requireWorkflowSkill(artifacts: ArtifactStore, skillId: string): { skill: Artifact; definition: SkillDefinition } {
72
+ /**
73
+ * A definition-shaped target: kind=playbook (Skill-the-kind is retired; every remaining
74
+ * definition-holding row -- migrated legacy or freshly constructed -- lives under kind=playbook
75
+ * now) with subtype=workflow, distinguishing it from an ordinary steps/trigger-shaped Playbook.
76
+ */
77
+ function requireWorkflowSkill(artifacts: ArtifactStore, skillId: string): { skill: Artifact; definition: BlueprintDefinition } {
67
78
  const skill = artifacts.get(skillId);
68
- if (!skill) throw new Error(`skill artifact "${skillId}" not found`);
69
- if (skill.kind !== "skill" || skill.subtype !== "workflow") {
70
- throw new Error(`artifact "${skillId}" is not a workflow Skill`);
79
+ if (!skill) throw new Error(`playbook artifact "${skillId}" not found`);
80
+ if (skill.kind !== "playbook" || skill.subtype !== "workflow") {
81
+ throw new Error(`artifact "${skillId}" is not a workflow-definition playbook`);
71
82
  }
72
- if (skill.status !== "active") throw new Error(`cannot run workflow Skill from ${skill.status}`);
73
- return { skill, definition: validateSkillDefinition(skill.extra["definition"]) };
83
+ if (skill.status !== "active") throw new Error(`cannot run workflow playbook from ${skill.status}`);
84
+ return { skill, definition: validateBlueprintDefinition(skill.extra.definition) };
74
85
  }
75
86
 
76
87
  function normalizeRunId(skillId: string, requested: string | undefined): string {
77
88
  const runId = requested ?? `${skillId.slice(0, 40)}-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
78
89
  if (runId.length > SKILL_RUN_ID_MAX_LENGTH || !RUN_ID_PATTERN.test(runId)) {
79
- throw new Error(`skill run id must match ${RUN_ID_PATTERN} and contain at most ${SKILL_RUN_ID_MAX_LENGTH} characters`);
90
+ throw new Error(`run id must match ${RUN_ID_PATTERN} and contain at most ${SKILL_RUN_ID_MAX_LENGTH} characters`);
80
91
  }
81
92
  return runId;
82
93
  }
83
94
 
84
- function renderValue(value: unknown, arguments_: Record<string, SkillArgumentValue>): unknown {
95
+ function renderValue(value: unknown, arguments_: Record<string, BlueprintArgumentValue>): unknown {
85
96
  if (typeof value === "string") {
86
97
  const exact = value.match(EXACT_PLACEHOLDER_PATTERN);
87
98
  if (exact) {
88
99
  const name = exact[1]!;
89
- if (!(name in arguments_)) throw new Error(`skill input placeholder "${name}" has no argument value`);
100
+ if (!(name in arguments_)) throw new Error(`input placeholder "${name}" has no argument value`);
90
101
  return arguments_[name]!;
91
102
  }
92
103
  return value.replace(PLACEHOLDER_PATTERN, (_placeholder, name: string) => {
93
- if (!(name in arguments_)) throw new Error(`skill input placeholder "${name}" has no argument value`);
104
+ if (!(name in arguments_)) throw new Error(`input placeholder "${name}" has no argument value`);
94
105
  return String(arguments_[name]!);
95
106
  });
96
107
  }
@@ -98,34 +109,35 @@ function renderValue(value: unknown, arguments_: Record<string, SkillArgumentVal
98
109
  if (typeof value !== "object" || value === null) return value;
99
110
  const rendered: Record<string, unknown> = {};
100
111
  for (const [key, entry] of Object.entries(value)) {
101
- if (UNSAFE_KEYS.has(key)) throw new Error(`unsafe skill blueprint key "${key}"`);
112
+ if (UNSAFE_KEYS.has(key)) throw new Error(`unsafe blueprint key "${key}"`);
102
113
  rendered[key] = renderValue(entry, arguments_);
103
114
  }
104
115
  return rendered;
105
116
  }
106
117
 
107
- function renderDefinition(definition: SkillDefinition, arguments_: Record<string, SkillArgumentValue>): SkillDefinition {
108
- const rendered = renderValue(definition, arguments_) as SkillDefinition;
118
+ function renderDefinition(definition: BlueprintDefinition, arguments_: Record<string, BlueprintArgumentValue>): BlueprintDefinition {
119
+ const rendered = renderValue(definition, arguments_) as BlueprintDefinition;
109
120
  const bytes = new TextEncoder().encode(JSON.stringify(rendered)).byteLength;
110
- if (bytes > SKILL_MAX_RENDERED_BYTES) throw new Error(`rendered skill workflow exceeds ${SKILL_MAX_RENDERED_BYTES} bytes`);
121
+ if (bytes > SKILL_MAX_RENDERED_BYTES) throw new Error(`rendered workflow exceeds ${SKILL_MAX_RENDERED_BYTES} bytes`);
111
122
  for (const task of rendered.blueprints.tasks) {
112
- if (task.extra?.["checklist"] !== undefined) {
113
- task.extra["checklist"] = validateChecklist(task.extra["checklist"]);
123
+ if (task.extra?.checklist !== undefined) {
124
+ task.extra.checklist = validateChecklist(task.extra.checklist);
114
125
  }
115
126
  }
116
- return validateSkillDefinition(rendered);
127
+ return validateBlueprintDefinition(rendered);
117
128
  }
118
129
 
119
130
  function withRunLabel(labels: string[] | undefined, labelPrefix: string, runId: string): string[] {
120
131
  return [...new Set([...(labels ?? []), `${labelPrefix}:${runId}`])];
121
132
  }
122
133
 
123
- function executionGraph(tasks: Artifact[], definition: SkillDefinition, ids: Map<string, string>, extraKey: string): TaskGraph {
134
+ function executionGraph(tasks: Artifact[], definition: BlueprintDefinition, ids: Map<string, string>, extraKey: string): TaskGraph {
124
135
  const byRef = new Map(definition.blueprints.tasks.map((task) => [task.ref, task]));
125
136
  const nodes: TaskNode[] = tasks.map((task) => {
126
- const ref = task.extra[extraKey] && typeof task.extra[extraKey] === "object"
127
- ? (task.extra[extraKey] as Record<string, unknown>)["ref"] as string
128
- : "";
137
+ const ref =
138
+ task.extra[extraKey] && typeof task.extra[extraKey] === "object"
139
+ ? ((task.extra[extraKey] as Record<string, unknown>).ref as string)
140
+ : "";
129
141
  const blueprint = byRef.get(ref)!;
130
142
  return {
131
143
  task,
@@ -138,15 +150,15 @@ function executionGraph(tasks: Artifact[], definition: SkillDefinition, ids: Map
138
150
  return { nodes, rootIds: nodes.filter((node) => node.parentIds.length === 0).map((node) => node.task.id) };
139
151
  }
140
152
 
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. */
153
+ /** projectRoot is optional -- skills.run always supplies one (workflow-definition 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
154
  export type WorkflowRunHistory = { events: TaskEventStore; scopes: TaskScopeStore; projectRoot?: string; context?: TaskEventContext };
143
155
 
144
156
  /**
145
157
  * Public entry point: wraps one complete pipeline run (including every nested sub-pipeline
146
158
  * it triggers) in exactly one atomic transaction. The recursive core (runWorkflowSteps) never
147
159
  * opens its own atomic wrapper -- SQLite savepoint nesting (inTransaction in db.ts) would
148
- * tolerate it, but wrapping once here keeps the atomicity story unambiguous: one skills.run
149
- * call is one all-or-nothing graph mutation, however many nested skills it triggers.
160
+ * tolerate it, but wrapping once here keeps the atomicity story unambiguous: one run call is
161
+ * one all-or-nothing graph mutation, however many nested targets it triggers.
150
162
  */
151
163
  export function instantiateSkillWorkflow(
152
164
  artifacts: ArtifactStore,
@@ -159,31 +171,84 @@ export function instantiateSkillWorkflow(
159
171
  return requireAtomicArtifactStore(artifacts).atomic(run);
160
172
  }
161
173
 
174
+ /** Reads each created task's own lineage.ref tag back off the store -- resolves a compiled Playbook's blueprint refs back to real task ids after materialization, without threading an extra ref-to-id map out of materializeWorkflowDefinition's own return shape. Shared by top-level Playbook invocation (playbook-execution.ts) and a nested Playbook pipeline-call step alike. */
175
+ export function resolveRefToTaskId(artifacts: ArtifactStore, taskIds: string[], extraKey: string): Map<string, string> {
176
+ const map = new Map<string, string>();
177
+ for (const taskId of taskIds) {
178
+ const lineage = artifacts.get(taskId)?.extra[extraKey];
179
+ if (typeof lineage !== "object" || lineage === null || Array.isArray(lineage)) continue;
180
+ const ref = (lineage as Record<string, unknown>).ref;
181
+ if (typeof ref === "string") map.set(ref, taskId);
182
+ }
183
+ return map;
184
+ }
185
+
186
+ /** Applies a compiled Playbook's external links (a Rule that `gates` it, a Doc it `references`, etc.) once its blueprint refs have resolved to real task ids -- shared by top-level Playbook invocation and a nested Playbook pipeline-call step alike. */
187
+ export function applyPlaybookExternalLinks(
188
+ artifacts: ArtifactStore,
189
+ externalLinks: PlaybookExternalLink[],
190
+ refToTaskId: Map<string, string>,
191
+ ): void {
192
+ for (const link of externalLinks) {
193
+ const taskId = refToTaskId.get(link.rootRef);
194
+ if (!taskId) continue; // defensive -- every rootRef the compiler emits is always materialized
195
+ if (link.ownerIsFrom) artifacts.link({ from: taskId, relation: link.relation, to: link.otherArtifactId });
196
+ else artifacts.link({ from: link.otherArtifactId, relation: link.relation, to: taskId });
197
+ }
198
+ }
199
+
162
200
  /**
163
- * The recursive pipeline core. A workflow Skill's `skills` blueprint entries are pipeline
164
- * steps that trigger another workflow Skill's own run -- the Jenkins "downstream job" /
165
- * Ansible "include_tasks" primitive. Nested runs execute BEFORE this level's dependsOn/parent
166
- * edges are wired, since a step depending on a skill-call ref needs to know every task id
167
- * that nested run actually produced (not knowable ahead of time -- it depends on the nested
168
- * skill's own definition). `ancestorSkillIds` tracks the current call CHAIN (not a global
169
- * ever-visited set): sibling skill-calls under the same parent are independent and may
170
- * legitimately share a called skill; only a real cycle back to an ancestor is rejected.
201
+ * The recursive pipeline core. A `skills` blueprint entry's target can be either a
202
+ * workflow-definition Playbook (an already-persisted, versioned JSON blueprint) or an ordinary
203
+ * steps/trigger-shaped Playbook (its steps/composition tree compiled fresh, right here, the
204
+ * same way a top-level Playbook invocation does) -- the Jenkins "downstream job" / Ansible
205
+ * "include_tasks" primitive either way, dispatched purely by the target artifact's own subtype.
206
+ * Nested runs execute BEFORE this level's dependsOn/parent edges are wired, since a step
207
+ * depending on a call ref needs to know every task id that nested run actually produced (not
208
+ * knowable ahead of time -- it depends on the nested target's own definition). `ancestorIds`
209
+ * tracks the current call CHAIN (not a global ever-visited set): sibling calls under the same
210
+ * parent are independent and may legitimately share a called target; only a real cycle back to
211
+ * an ancestor is rejected. This is a SEPARATE cycle/depth check from a Playbook's own
212
+ * contains/depends_on composition tree (playbook-definition.ts's compileNode) -- a cycle
213
+ * threading through BOTH graphs at once (Playbook composition -> a call step -> back into that
214
+ * same Playbook's composition) is not cross-checked between the two, but each graph's own
215
+ * independent depth cap still bounds it; it fails with a nesting-depth error rather than a
216
+ * precise cycle message, not an infinite loop.
171
217
  */
172
218
  function runWorkflowSteps(
173
219
  artifacts: ArtifactStore,
174
- skillId: string,
220
+ targetId: string,
175
221
  input: InstantiateSkillWorkflowInput,
176
222
  history: WorkflowRunHistory | undefined,
177
- ancestorSkillIds: ReadonlySet<string>,
223
+ ancestorIds: ReadonlySet<string>,
178
224
  depth: number,
179
225
  ): WorkflowRunResult {
180
- if (ancestorSkillIds.has(skillId)) throw new Error(`skill workflow nesting cycle includes "${skillId}"`);
181
- if (depth > SKILL_WORKFLOW_MAX_NESTING_DEPTH) throw new Error(`skill workflow nesting exceeds ${SKILL_WORKFLOW_MAX_NESTING_DEPTH} levels`);
182
- const nextAncestors = new Set([...ancestorSkillIds, skillId]);
183
- const { definition } = requireWorkflowSkill(artifacts, skillId);
226
+ if (ancestorIds.has(targetId)) throw new Error(`workflow nesting cycle includes "${targetId}"`);
227
+ if (depth > SKILL_WORKFLOW_MAX_NESTING_DEPTH) throw new Error(`workflow nesting exceeds ${SKILL_WORKFLOW_MAX_NESTING_DEPTH} levels`);
228
+ const nextAncestors = new Set([...ancestorIds, targetId]);
229
+ const target = artifacts.get(targetId);
230
+ // subtype=workflow is the legacy definition-shaped case (raw extra.definition, no
231
+ // extra.steps) -- compilePlaybookDefinition assumes an ordinary steps/trigger-shaped
232
+ // Playbook and must not see it; it falls through to requireWorkflowSkill below instead.
233
+ if (target?.kind === "playbook" && target.subtype !== "workflow") {
234
+ const compiled = compilePlaybookDefinition(artifacts, targetId);
235
+ const result = materializeWorkflowDefinition(
236
+ artifacts,
237
+ { ownerId: targetId, extraKey: "playbookRun", labelPrefix: "playbook-run" },
238
+ compiled.definition,
239
+ { ...input, focusRef: compiled.entryRef },
240
+ history,
241
+ nextAncestors,
242
+ depth,
243
+ );
244
+ const refToTaskId = resolveRefToTaskId(artifacts, result.created.tasks, "playbookRun");
245
+ applyPlaybookExternalLinks(artifacts, compiled.externalLinks, refToTaskId);
246
+ return result;
247
+ }
248
+ const { definition } = requireWorkflowSkill(artifacts, targetId);
184
249
  return materializeWorkflowDefinition(
185
250
  artifacts,
186
- { ownerId: skillId, extraKey: "skillRun", labelPrefix: "skill-run" },
251
+ { ownerId: targetId, extraKey: "skillRun", labelPrefix: "skill-run" },
187
252
  definition,
188
253
  input,
189
254
  history,
@@ -193,23 +258,23 @@ function runWorkflowSteps(
193
258
  }
194
259
 
195
260
  /**
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.
261
+ * The definition-materialization core, shared by workflow-definition targets
262
+ * (instantiateSkillWorkflow, via runWorkflowSteps above) and Playbook invocation
263
+ * (playbook-execution.ts): given an ALREADY-RESOLVED BlueprintDefinition -- fetched from a
264
+ * persisted workflow-definition artifact for that path, compiled in-memory from a Playbook's
265
+ * steps/trigger/arguments and its contains/depends_on composition tree for the Playbook path --
266
+ * creates every blueprint artifact, wires dependsOn/parent/links, recurses into nested call
267
+ * pipeline steps (a no-op for a Playbook-compiled definition, which never populates
268
+ * blueprints.skills), and tags every created artifact and the run's containing labels via
269
+ * `lineage` rather than a hardcoded "skillRun"/"skill-run" shape -- the only thing that differs
270
+ * between the two callers. `ancestorSkillIds`/`depth` are the same cycle/nesting-depth tracking
271
+ * runWorkflowSteps already enforced before this was extracted; a Playbook caller with no nested
272
+ * calls to recurse into passes an empty set and depth 0 and never revisits this function itself.
208
273
  */
209
274
  export function materializeWorkflowDefinition(
210
275
  artifacts: ArtifactStore,
211
276
  lineage: WorkflowLineage,
212
- definition: SkillDefinition,
277
+ definition: BlueprintDefinition,
213
278
  input: InstantiateSkillWorkflowInput,
214
279
  history: WorkflowRunHistory | undefined,
215
280
  ancestorSkillIds: ReadonlySet<string>,
@@ -217,7 +282,7 @@ export function materializeWorkflowDefinition(
217
282
  ): WorkflowRunResult {
218
283
  const { ownerId, extraKey, labelPrefix } = lineage;
219
284
  const projectRoot = history?.projectRoot !== undefined ? normalizeProjectRoot(history.projectRoot) : undefined;
220
- const arguments_ = resolveSkillArguments(definition, input.arguments);
285
+ const arguments_ = resolveBlueprintArguments(definition, input.arguments);
221
286
  const rendered = renderDefinition(definition, arguments_);
222
287
  const runId = normalizeRunId(ownerId, input.runId);
223
288
  const refs = [
@@ -231,40 +296,45 @@ export function materializeWorkflowDefinition(
231
296
  // A bound at THIS level's own blueprint size; nested runs are independently bounded the same
232
297
  // way at their own level, and nesting depth is separately capped -- so total blast radius
233
298
  // across a whole pipeline stays bounded on both dimensions even though a step's dependency
234
- // on a skill-call ref can fan out to more edges than this per-level count captures exactly.
235
- const relationshipCount = rendered.links.length
236
- + rendered.blueprints.tasks.reduce((count, task) => count + (task.dependsOn?.length ?? 0) + (task.parent ? 2 : 0), 0)
237
- + rendered.blueprints.skills.reduce((count, call) => count + (call.dependsOn?.length ?? 0) + (call.parent ? 2 : 0), 0)
238
- + rendered.blueprints.tasks.filter((task) => (task.dependsOn?.length ?? 0) === 0).length
239
- + rendered.blueprints.skills.filter((call) => (call.dependsOn?.length ?? 0) === 0).length;
299
+ // on a call ref can fan out to more edges than this per-level count captures exactly.
300
+ const relationshipCount =
301
+ rendered.links.length +
302
+ rendered.blueprints.tasks.reduce((count, task) => count + (task.dependsOn?.length ?? 0) + (task.parent ? 2 : 0), 0) +
303
+ rendered.blueprints.skills.reduce((count, call) => count + (call.dependsOn?.length ?? 0) + (call.parent ? 2 : 0), 0) +
304
+ rendered.blueprints.tasks.filter((task) => (task.dependsOn?.length ?? 0) === 0).length +
305
+ rendered.blueprints.skills.filter((call) => (call.dependsOn?.length ?? 0) === 0).length;
240
306
  if (relationshipCount > TASK_EXECUTION_MAX_EDGES) {
241
- throw new Error(`skill workflow run exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
307
+ throw new Error(`workflow run exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
242
308
  }
243
309
 
244
- const docs = rendered.blueprints.docs.map((blueprint) => artifacts.create({
245
- id: ids.get(blueprint.ref),
246
- kind: "doc",
247
- title: blueprint.title,
248
- body: blueprint.body,
249
- subtype: blueprint.subtype,
250
- labels: withRunLabel(blueprint.labels, labelPrefix, runId),
251
- extra: { ...(blueprint.extra ?? {}), [extraKey]: { id: runId, ownerId, ref: blueprint.ref } },
252
- }));
253
- const rules = rendered.blueprints.rules.map((blueprint) => artifacts.create({
254
- id: ids.get(blueprint.ref),
255
- kind: "rule",
256
- title: blueprint.title,
257
- body: blueprint.body,
258
- labels: withRunLabel(blueprint.labels, labelPrefix, runId),
259
- extra: {
260
- ...(blueprint.extra ?? {}),
261
- ...(blueprint.condition ? { condition: blueprint.condition } : {}),
262
- ...(blueprint.action ? { action: blueprint.action } : {}),
263
- ...(blueprint.severity ? { severity: blueprint.severity } : {}),
264
- [extraKey]: { id: runId, ownerId, ref: blueprint.ref },
265
- scope: { type: labelPrefix, runId, taskIds },
266
- },
267
- }));
310
+ const docs = rendered.blueprints.docs.map((blueprint) =>
311
+ artifacts.create({
312
+ id: ids.get(blueprint.ref),
313
+ kind: "doc",
314
+ title: blueprint.title,
315
+ body: blueprint.body,
316
+ subtype: blueprint.subtype,
317
+ labels: withRunLabel(blueprint.labels, labelPrefix, runId),
318
+ extra: { ...(blueprint.extra ?? {}), [extraKey]: { id: runId, ownerId, ref: blueprint.ref } },
319
+ }),
320
+ );
321
+ const rules = rendered.blueprints.rules.map((blueprint) =>
322
+ artifacts.create({
323
+ id: ids.get(blueprint.ref),
324
+ kind: "rule",
325
+ title: blueprint.title,
326
+ body: blueprint.body,
327
+ labels: withRunLabel(blueprint.labels, labelPrefix, runId),
328
+ extra: {
329
+ ...(blueprint.extra ?? {}),
330
+ ...(blueprint.condition ? { condition: blueprint.condition } : {}),
331
+ ...(blueprint.action ? { action: blueprint.action } : {}),
332
+ ...(blueprint.severity ? { severity: blueprint.severity } : {}),
333
+ [extraKey]: { id: runId, ownerId, ref: blueprint.ref },
334
+ scope: { type: labelPrefix, runId, taskIds },
335
+ },
336
+ }),
337
+ );
268
338
  const tasks = rendered.blueprints.tasks.map((blueprint) => {
269
339
  const task = artifacts.create({
270
340
  id: ids.get(blueprint.ref),
@@ -291,17 +361,24 @@ export function materializeWorkflowDefinition(
291
361
 
292
362
  // Nested pipeline steps run before edge-wiring: dependents need to know what tasks each
293
363
  // nested run actually produced. stepTaskIds/stepRootTaskIds map EVERY step ref (task or
294
- // skill-call) to the task id(s) it resolves to, so dependsOn/parent wiring below treats
295
- // both kinds of step uniformly.
364
+ // call) to the task id(s) it resolves to, so dependsOn/parent wiring below treats both
365
+ // kinds of step uniformly.
296
366
  const nestedRuns: WorkflowRunResult[] = [];
297
367
  const stepTaskIds = new Map<string, string[]>(tasks.map((task, index) => [rendered.blueprints.tasks[index]!.ref, [task.id]]));
298
368
  const stepRootTaskIds = new Map<string, string[]>(
299
- tasks.map((task, index) => [rendered.blueprints.tasks[index]!.ref, (rendered.blueprints.tasks[index]!.dependsOn?.length ?? 0) === 0 ? [task.id] : []]),
369
+ tasks.map((task, index) => [
370
+ rendered.blueprints.tasks[index]!.ref,
371
+ (rendered.blueprints.tasks[index]!.dependsOn?.length ?? 0) === 0 ? [task.id] : [],
372
+ ]),
300
373
  );
301
- for (const call of rendered.blueprints.skills as SkillCallBlueprint[]) {
374
+ // A call ref never gets its own real task -- it resolves to whatever the nested run's entry
375
+ // (or, absent a resolvable one, its first root task) actually is. Lets a focusRef chain
376
+ // straight through an arbitrary number of nested calls down to a real task, recursively.
377
+ const stepEntryTaskIds = new Map<string, string>();
378
+ for (const call of rendered.blueprints.skills as CallBlueprint[]) {
302
379
  const nested = runWorkflowSteps(
303
380
  artifacts,
304
- call.skillId,
381
+ call.targetId,
305
382
  { runId: `${runId}-${call.ref}`, arguments: call.arguments },
306
383
  history,
307
384
  ancestorSkillIds,
@@ -310,6 +387,8 @@ export function materializeWorkflowDefinition(
310
387
  nestedRuns.push(nested);
311
388
  stepTaskIds.set(call.ref, nested.created.tasks);
312
389
  stepRootTaskIds.set(call.ref, nested.rootTaskIds);
390
+ const entry = nested.entryTaskId ?? nested.rootTaskIds[0];
391
+ if (entry !== undefined) stepEntryTaskIds.set(call.ref, entry);
313
392
  }
314
393
 
315
394
  for (const blueprint of rendered.blueprints.tasks) {
@@ -323,7 +402,7 @@ export function materializeWorkflowDefinition(
323
402
  artifacts.link({ from: id, relation: "part_of", to: parentId });
324
403
  }
325
404
  }
326
- for (const call of rendered.blueprints.skills as SkillCallBlueprint[]) {
405
+ for (const call of rendered.blueprints.skills as CallBlueprint[]) {
327
406
  const stepTaskIdsForCall = stepTaskIds.get(call.ref) ?? [];
328
407
  for (const dependency of call.dependsOn ?? []) {
329
408
  for (const dependencyId of stepTaskIds.get(dependency) ?? []) {
@@ -346,17 +425,27 @@ export function materializeWorkflowDefinition(
346
425
 
347
426
  const rootTaskIds = [
348
427
  ...rendered.blueprints.tasks.filter((task) => (task.dependsOn?.length ?? 0) === 0).map((task) => ids.get(task.ref)!),
349
- ...(rendered.blueprints.skills as SkillCallBlueprint[])
428
+ ...(rendered.blueprints.skills as CallBlueprint[])
350
429
  .filter((call) => (call.dependsOn?.length ?? 0) === 0)
351
430
  .flatMap((call) => stepRootTaskIds.get(call.ref) ?? []),
352
431
  ];
353
432
  for (const task of rendered.blueprints.tasks) {
354
433
  if ((task.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: ids.get(task.ref)! });
355
434
  }
356
- for (const call of rendered.blueprints.skills as SkillCallBlueprint[]) {
357
- if ((call.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: call.skillId });
435
+ for (const call of rendered.blueprints.skills as CallBlueprint[]) {
436
+ if ((call.dependsOn?.length ?? 0) === 0) artifacts.link({ from: ownerId, relation: "triggers", to: call.targetId });
358
437
  }
359
438
 
439
+ // A focusRef naming an ordinary task ref resolves directly through ids; one naming a call
440
+ // ref resolves through the nested run it triggered instead (never through ids, which only
441
+ // maps a call ref to a synthetic placeholder that was never actually created).
442
+ const focusTaskId =
443
+ input.focusRef === undefined
444
+ ? undefined
445
+ : rendered.blueprints.tasks.some((task) => task.ref === input.focusRef)
446
+ ? ids.get(input.focusRef)
447
+ : stepEntryTaskIds.get(input.focusRef);
448
+
360
449
  return {
361
450
  skillId: ownerId,
362
451
  runId,
@@ -368,7 +457,7 @@ export function materializeWorkflowDefinition(
368
457
  skillRuns: [...nestedRuns.map((run) => run.runId), ...nestedRuns.flatMap((run) => run.created.skillRuns)],
369
458
  },
370
459
  rootTaskIds,
371
- ...(input.focusRef !== undefined && ids.has(input.focusRef) ? { entryTaskId: ids.get(input.focusRef)! } : {}),
460
+ ...(focusTaskId !== undefined ? { entryTaskId: focusTaskId } : {}),
372
461
  execution: projectTaskExecution(executionGraph(tasks, rendered, ids, extraKey)),
373
462
  };
374
463
  }