@danypops/papyrus 0.3.0 → 0.5.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.
@@ -1,4 +1,5 @@
1
1
  import type { Artifact, CreateArtifactInput } from "./domain/artifact.ts";
2
+ import { validateSkillDefinition } from "./domain/skill-definition.ts";
2
3
  import type { ArtifactStore } from "./ports/artifact-store.ts";
3
4
 
4
5
  export interface ListFilter {
@@ -98,6 +99,18 @@ export function listRules(artifacts: ArtifactStore, filter: ListFilter): Artifac
98
99
  return artifacts.query({ kind: "rule", ...filter });
99
100
  }
100
101
 
102
+ /** Global rules always apply; scoped workflow rules apply only while their run owns active focus. */
103
+ export function listInjectableRules(artifacts: ArtifactStore, activeTaskId?: string): Artifact[] {
104
+ return artifacts.query({ kind: "rule", status: "active" }).filter((rule) => {
105
+ const scope = rule.extra["scope"];
106
+ if (scope === undefined) return true;
107
+ if (typeof scope !== "object" || scope === null || Array.isArray(scope)) return false;
108
+ const value = scope as Record<string, unknown>;
109
+ if (value["type"] !== "skill-run" || !Array.isArray(value["taskIds"])) return false;
110
+ return activeTaskId !== undefined && value["taskIds"].some((id) => id === activeTaskId);
111
+ });
112
+ }
113
+
101
114
  export function showRule(artifacts: ArtifactStore, id: string): Artifact {
102
115
  requireKind(artifacts, id, "rule");
103
116
  return artifacts.get(id, { tree: true })!;
@@ -131,6 +144,7 @@ export interface CreateSkillInput {
131
144
  trigger?: string;
132
145
  steps?: string[];
133
146
  tools?: string[];
147
+ definition?: unknown;
134
148
  labels?: string[];
135
149
  extra?: Record<string, unknown>;
136
150
  }
@@ -147,13 +161,19 @@ export interface CreateArtifactTemplateInput {
147
161
  export type SkillTransition = "enable" | "disable";
148
162
 
149
163
  export function createSkill(artifacts: ArtifactStore, input: CreateSkillInput): Artifact {
164
+ if (input.definition !== undefined && (input.trigger !== undefined || input.steps !== undefined || input.tools !== undefined)) {
165
+ throw new Error("workflow Skill definition cannot be mixed with legacy trigger, steps, or tools");
166
+ }
167
+ const definition = input.definition === undefined ? undefined : validateSkillDefinition(input.definition);
150
168
  return artifacts.create({
151
169
  kind: "skill",
170
+ subtype: definition ? "workflow" : undefined,
152
171
  title: input.title,
153
172
  body: input.body,
154
173
  labels: input.labels,
155
174
  extra: {
156
175
  ...(input.extra ?? {}),
176
+ ...(definition ? { definition } : {}),
157
177
  ...(input.trigger ? { trigger: input.trigger } : {}),
158
178
  ...(input.steps ? { steps: input.steps } : {}),
159
179
  ...(input.tools ? { tools: input.tools } : {}),
@@ -194,6 +214,17 @@ export function skillInvocation(artifacts: ArtifactStore, id: string): string {
194
214
  if (skill.subtype === "artifact-template") {
195
215
  return `Create an artifact using Papyrus template "${skill.title}".\ntemplate_id: ${skill.id}\nAsk for or infer all required template fields, then call the skills domain tool instantiate action.`;
196
216
  }
217
+ if (skill.subtype === "workflow") {
218
+ const definition = validateSkillDefinition(skill.extra["definition"]);
219
+ const required = Object.entries(definition.inputs)
220
+ .filter(([, input]) => input.required && input.default === undefined)
221
+ .map(([name]) => name);
222
+ return [
223
+ `Run Papyrus workflow Skill "${skill.title}" (${skill.id}).`,
224
+ `Required arguments: ${required.length > 0 ? required.join(", ") : "none"}.`,
225
+ "Call the skills domain tool with action=run and arguments after collecting required values.",
226
+ ].join("\n");
227
+ }
197
228
  const trigger = typeof skill.extra["trigger"] === "string" ? skill.extra["trigger"] : "manual invocation";
198
229
  const steps = Array.isArray(skill.extra["steps"]) ? skill.extra["steps"].filter((step): step is string => typeof step === "string") : [];
199
230
  const tools = Array.isArray(skill.extra["tools"]) ? skill.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
@@ -0,0 +1,13 @@
1
+ import type { ArtifactStore } from "./artifact-store.ts";
2
+
3
+ /** Artifact store boundary for domain operations that must commit as one graph mutation. */
4
+ export interface AtomicArtifactStore extends ArtifactStore {
5
+ atomic<T>(operation: () => T): T;
6
+ }
7
+
8
+ export function requireAtomicArtifactStore(store: ArtifactStore): AtomicArtifactStore {
9
+ if (!("atomic" in store) || typeof store.atomic !== "function") {
10
+ throw new Error("artifact store does not support atomic workflow runs");
11
+ }
12
+ return store as AtomicArtifactStore;
13
+ }
@@ -0,0 +1,43 @@
1
+ import { normalizeTaskHistoryQuery, validateTaskEvent, type AppendTaskEvent, type TaskEvent, type TaskHistoryPage, type TaskHistoryQuery } from "../domain/task-event.ts";
2
+
3
+ export interface TaskEventStore {
4
+ atomic<T>(operation: () => T): T;
5
+ append(event: AppendTaskEvent): TaskEvent;
6
+ history(taskId: string, query?: TaskHistoryQuery): TaskHistoryPage;
7
+ }
8
+
9
+ export class InMemoryTaskEventStore implements TaskEventStore {
10
+ private events: TaskEvent[] = [];
11
+ private nextId = 1;
12
+
13
+ atomic<T>(operation: () => T): T {
14
+ const length = this.events.length;
15
+ const nextId = this.nextId;
16
+ try { return operation(); }
17
+ catch (error) {
18
+ this.events.length = length;
19
+ this.nextId = nextId;
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ append(event: AppendTaskEvent): TaskEvent {
25
+ const stored: TaskEvent = {
26
+ ...validateTaskEvent(event),
27
+ id: this.nextId++,
28
+ occurredAt: new Date().toISOString(),
29
+ schemaVersion: 1,
30
+ };
31
+ this.events.push(stored);
32
+ return stored;
33
+ }
34
+
35
+ history(taskId: string, query: TaskHistoryQuery = {}): TaskHistoryPage {
36
+ const { direction, limit, cursor } = normalizeTaskHistoryQuery(query);
37
+ const ordered = this.events
38
+ .filter((event) => event.taskId === taskId && (cursor === undefined || (direction === "desc" ? event.id < cursor : event.id > cursor)))
39
+ .sort((left, right) => direction === "desc" ? right.id - left.id : left.id - right.id);
40
+ const events = ordered.slice(0, limit);
41
+ return { events, ...(ordered.length > limit ? { nextCursor: events.at(-1)!.id } : {}) };
42
+ }
43
+ }
package/src/service.ts CHANGED
@@ -4,10 +4,13 @@ import { migrateDb, openDb, schemaVersion } from "./db.ts";
4
4
  import { SQLiteArtifactStore } from "./adapters/sqlite-artifact-store.ts";
5
5
  import { SQLiteGateRunner } from "./adapters/sqlite-gate-runner.ts";
6
6
  import { SQLiteTaskFocusStore } from "./adapters/sqlite-task-focus-store.ts";
7
+ import { SQLiteTaskEventStore } from "./adapters/sqlite-task-event-store.ts";
7
8
  import type { CreateArtifactInput } from "./domain/artifact.ts";
8
9
  import type { Checklist } from "./domain/checklist.ts";
10
+ import type { TaskEventContext, TaskEventDirection } from "./domain/task-event.ts";
9
11
  import type { ArtifactStore } from "./ports/artifact-store.ts";
10
12
  import type { GateRunner } from "./ports/gate-runner.ts";
13
+ import type { TaskEventStore } from "./ports/task-event-store.ts";
11
14
  import { projectTaskExecution } from "./task-execution.ts";
12
15
  import { Tasks, type TaskStatus } from "./task-service.ts";
13
16
  import {
@@ -20,6 +23,7 @@ import {
20
23
  instantiateTemplate,
21
24
  listDocuments,
22
25
  listRules,
26
+ listInjectableRules,
23
27
  listSkills,
24
28
  previewRule,
25
29
  showDocument,
@@ -32,6 +36,7 @@ import {
32
36
  type DocumentRelation,
33
37
  } from "./domain-services.ts";
34
38
  import { taskContext } from "./task-context.ts";
39
+ import { instantiateSkillWorkflow } from "./skill-execution.ts";
35
40
 
36
41
  export const EXPECTED_OPERATION_NAMES = [
37
42
  "system.migrate",
@@ -48,6 +53,7 @@ export const EXPECTED_OPERATION_NAMES = [
48
53
  "tasks.graph",
49
54
  "tasks.plan",
50
55
  "tasks.show",
56
+ "tasks.history",
51
57
  "tasks.active",
52
58
  "tasks.focus",
53
59
  "tasks.start",
@@ -80,6 +86,7 @@ export const EXPECTED_OPERATION_NAMES = [
80
86
  "skills.list",
81
87
  "skills.show",
82
88
  "skills.invoke",
89
+ "skills.run",
83
90
  "skills.enable",
84
91
  "skills.disable",
85
92
  "skills.instantiate",
@@ -137,8 +144,19 @@ function handlers(
137
144
  artifacts: ArtifactStore,
138
145
  gates: GateRunner,
139
146
  tasks: Tasks,
147
+ events: TaskEventStore,
140
148
  migrate: () => unknown,
141
149
  ): Record<OperationName, OperationHandler> {
150
+ const eventContext = (input: OperationInput): TaskEventContext => ({
151
+ actor: optionalString(input, "actor"),
152
+ source: optionalString(input, "source"),
153
+ sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
154
+ reason: optionalString(input, "reason"),
155
+ });
156
+ const eventContextFor = (input: OperationInput, source: string): TaskEventContext => {
157
+ const context = eventContext(input);
158
+ return { ...context, source: context.source ?? source };
159
+ };
142
160
  const taskFilter = (input: OperationInput) => ({
143
161
  status: optionalString(input, "status"),
144
162
  text: optionalString(input, "text"),
@@ -146,7 +164,20 @@ function handlers(
146
164
  });
147
165
  return {
148
166
  "system.migrate": () => migrate(),
149
- "artifact.create": (input) => artifacts.create(normalizeCreateInput(input)),
167
+ "artifact.create": (input) => {
168
+ const normalized = normalizeCreateInput(input);
169
+ if (normalized.kind !== "task") return artifacts.create(normalized);
170
+ return tasks.create({
171
+ id: normalized.id,
172
+ title: string(input, "title"),
173
+ body: normalized.body,
174
+ subtype: normalized.subtype,
175
+ status: normalized.status as TaskStatus | undefined,
176
+ labels: normalized.labels,
177
+ extra: normalized.extra,
178
+ templateId: normalized.templateId,
179
+ }, eventContextFor(input, "artifact-api"));
180
+ },
150
181
  "artifact.query": (input) => artifacts.query(input),
151
182
  "artifact.show": (input) => artifacts.get(string(input, "id"), {
152
183
  tree: input["tree"] === true,
@@ -169,9 +200,18 @@ function handlers(
169
200
  depth: optionalNumber(input, "depth"),
170
201
  maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
171
202
  }),
172
- "graph.status": (input) => artifacts.setStatus(string(input, "id"), string(input, "status")),
173
- "gates.run": (input) => gates.runAsync(string(input, "id")),
174
- "rules.injectable": () => artifacts.query({ kind: "rule", status: "active" })
203
+ "graph.status": (input) => {
204
+ const id = string(input, "id");
205
+ if (artifacts.get(id)?.kind === "task") throw new Error("task lifecycle changes require a tasks.* operation so history and review invariants are preserved");
206
+ return artifacts.setStatus(id, string(input, "status"));
207
+ },
208
+ "gates.run": (input) => {
209
+ const id = string(input, "id");
210
+ return artifacts.get(id)?.kind === "task"
211
+ ? tasks.runGates(id, eventContextFor(input, "gates-api"))
212
+ : gates.runAsync(id);
213
+ },
214
+ "rules.injectable": () => listInjectableRules(artifacts, tasks.active()?.id)
175
215
  .map(({ id, title, body, extra }) => ({ id, title, body, extra })),
176
216
  "tasks.create": (input) => tasks.create({
177
217
  title: string(input, "title"),
@@ -184,22 +224,27 @@ function handlers(
184
224
  templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
185
225
  parentId: optionalString(input, "parent_id") ?? optionalString(input, "parentId"),
186
226
  dependsOn: (input["depends_on"] ?? input["dependsOn"]) as string[] | undefined,
187
- }),
227
+ }, eventContext(input)),
188
228
  "tasks.list": (input) => tasks.list(taskFilter(input)),
189
229
  "tasks.graph": (input) => tasks.graph(taskFilter(input)),
190
230
  "tasks.plan": (input) => projectTaskExecution(tasks.graph(taskFilter(input))),
191
231
  "tasks.show": (input) => tasks.show(string(input, "id")),
232
+ "tasks.history": (input) => tasks.history(string(input, "id"), {
233
+ limit: optionalNumber(input, "limit"),
234
+ cursor: optionalNumber(input, "cursor"),
235
+ direction: optionalString(input, "direction") as TaskEventDirection | undefined,
236
+ }),
192
237
  "tasks.active": () => tasks.active(),
193
238
  "tasks.focus": (input) => tasks.focus(string(input, "id")),
194
- "tasks.start": (input) => tasks.transition(string(input, "id"), "start"),
195
- "tasks.submit": (input) => tasks.transition(string(input, "id"), "submit"),
196
- "tasks.complete": (input) => tasks.completeAsync(string(input, "id")),
197
- "tasks.run_gates": (input) => tasks.runGates(string(input, "id")),
239
+ "tasks.start": (input) => tasks.transition(string(input, "id"), "start", eventContext(input)),
240
+ "tasks.submit": (input) => tasks.transition(string(input, "id"), "submit", eventContext(input)),
241
+ "tasks.complete": (input) => tasks.completeAsync(string(input, "id"), eventContext(input)),
242
+ "tasks.run_gates": (input) => tasks.runGates(string(input, "id"), eventContext(input)),
198
243
  "tasks.set_checklist": (input) => tasks.setChecklist(string(input, "id"), input["checklist"] as Checklist),
199
244
  "tasks.context": () => taskContext(artifacts, tasks.active()?.id),
200
- "tasks.reject": (input) => tasks.transition(string(input, "id"), "reject"),
201
- "tasks.retry": (input) => tasks.transition(string(input, "id"), "retry"),
202
- "tasks.cancel": (input) => tasks.transition(string(input, "id"), "cancel"),
245
+ "tasks.reject": (input) => tasks.transition(string(input, "id"), "reject", eventContext(input)),
246
+ "tasks.retry": (input) => tasks.transition(string(input, "id"), "retry", eventContext(input)),
247
+ "tasks.cancel": (input) => tasks.transition(string(input, "id"), "cancel", eventContext(input)),
203
248
  "tasks.depend": (input) => tasks.depend(string(input, "id"), string(input, "dependency_id")),
204
249
  "tasks.contain": (input) => tasks.contain(string(input, "parent_id"), string(input, "child_id")),
205
250
  "docs.create": (input) => createDocument(artifacts, {
@@ -228,6 +273,7 @@ function handlers(
228
273
  "skills.create": (input) => createSkill(artifacts, {
229
274
  title: string(input, "title"), body: optionalString(input, "body"), trigger: optionalString(input, "trigger"),
230
275
  steps: input["steps"] as string[] | undefined, tools: input["tools"] as string[] | undefined,
276
+ definition: input["definition"],
231
277
  labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
232
278
  }),
233
279
  "skills.create_template": (input) => createArtifactTemplate(artifacts, {
@@ -237,6 +283,10 @@ function handlers(
237
283
  "skills.list": (input) => listSkills(artifacts, taskFilter(input)),
238
284
  "skills.show": (input) => showSkill(artifacts, string(input, "id")),
239
285
  "skills.invoke": (input) => skillInvocation(artifacts, string(input, "id")),
286
+ "skills.run": (input) => instantiateSkillWorkflow(artifacts, string(input, "id"), {
287
+ runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
288
+ arguments: input["arguments"] as Record<string, unknown> | undefined,
289
+ }, { events, context: eventContextFor(input, "skill-run") }),
240
290
  "skills.enable": (input) => transitionSkill(artifacts, string(input, "id"), "enable"),
241
291
  "skills.disable": (input) => transitionSkill(artifacts, string(input, "id"), "disable"),
242
292
  "skills.instantiate": (input) => instantiateTemplate(artifacts, string(input, "template_id"), normalizeCreateInput(input)),
@@ -248,8 +298,9 @@ export function createPapyrusService(path: string): PapyrusService {
248
298
  const artifacts = new SQLiteArtifactStore(db);
249
299
  const gates = new SQLiteGateRunner(db);
250
300
  const focus = new SQLiteTaskFocusStore(db);
251
- const tasks = new Tasks(artifacts, gates, focus);
252
- const registry = handlers(artifacts, gates, tasks, () => migrateDb(db));
301
+ const events = new SQLiteTaskEventStore(db);
302
+ const tasks = new Tasks(artifacts, gates, focus, events);
303
+ const registry = handlers(artifacts, gates, tasks, events, () => migrateDb(db));
253
304
  const state = (): SchemaState => {
254
305
  const current = schemaVersion(db);
255
306
  return { current, required: SQLITE_SCHEMA_VERSION, migrationRequired: current !== SQLITE_SCHEMA_VERSION };
@@ -261,7 +312,7 @@ export function createPapyrusService(path: string): PapyrusService {
261
312
  const handler = registry[operation as OperationName];
262
313
  if (!handler) throw new UnknownOperationError(`unknown operation "${operation}"`);
263
314
  if (operation !== "system.migrate" && state().migrationRequired) {
264
- throw new MigrationRequiredError("database migration required; run `papyrus migrate task-lifecycle`");
315
+ throw new MigrationRequiredError("database migration required; run `papyrus migrate task-history`");
265
316
  }
266
317
  return handler(input);
267
318
  },
@@ -0,0 +1,220 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { SKILL_MAX_RENDERED_BYTES, SKILL_RUN_ID_MAX_LENGTH, TASK_EXECUTION_MAX_EDGES } from "./constants.ts";
3
+ import type { Artifact } from "./domain/artifact.ts";
4
+ import { validateChecklist } from "./domain/checklist.ts";
5
+ import {
6
+ resolveSkillArguments,
7
+ validateSkillDefinition,
8
+ type SkillArgumentValue,
9
+ type SkillDefinition,
10
+ } from "./domain/skill-definition.ts";
11
+ import type { ArtifactStore } from "./ports/artifact-store.ts";
12
+ import type { TaskEventContext } from "./domain/task-event.ts";
13
+ import type { TaskEventStore } from "./ports/task-event-store.ts";
14
+ import { requireAtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
15
+ import { projectTaskExecution, type TaskExecutionPlan } from "./task-execution.ts";
16
+ import type { TaskGraph, TaskNode, TaskStatus } from "./task-service.ts";
17
+
18
+ const RUN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
19
+ const EXACT_PLACEHOLDER_PATTERN = /^{{\s*([A-Za-z][A-Za-z0-9_-]{0,63})\s*}}$/;
20
+ const PLACEHOLDER_PATTERN = /{{\s*([A-Za-z][A-Za-z0-9_-]{0,63})\s*}}/g;
21
+ const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
22
+
23
+ export interface InstantiateSkillWorkflowInput {
24
+ runId?: string;
25
+ arguments?: Record<string, unknown>;
26
+ }
27
+
28
+ export interface SkillWorkflowRunResult {
29
+ skillId: string;
30
+ runId: string;
31
+ arguments: Record<string, SkillArgumentValue>;
32
+ created: {
33
+ docs: string[];
34
+ rules: string[];
35
+ tasks: string[];
36
+ };
37
+ rootTaskIds: string[];
38
+ execution: TaskExecutionPlan;
39
+ }
40
+
41
+ function requireWorkflowSkill(artifacts: ArtifactStore, skillId: string): { skill: Artifact; definition: SkillDefinition } {
42
+ const skill = artifacts.get(skillId);
43
+ if (!skill) throw new Error(`skill artifact "${skillId}" not found`);
44
+ if (skill.kind !== "skill" || skill.subtype !== "workflow") {
45
+ throw new Error(`artifact "${skillId}" is not a workflow Skill`);
46
+ }
47
+ if (skill.status !== "active") throw new Error(`cannot run workflow Skill from ${skill.status}`);
48
+ return { skill, definition: validateSkillDefinition(skill.extra["definition"]) };
49
+ }
50
+
51
+ function normalizeRunId(skillId: string, requested: string | undefined): string {
52
+ const runId = requested ?? `${skillId.slice(0, 40)}-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
53
+ if (runId.length > SKILL_RUN_ID_MAX_LENGTH || !RUN_ID_PATTERN.test(runId)) {
54
+ throw new Error(`skill run id must match ${RUN_ID_PATTERN} and contain at most ${SKILL_RUN_ID_MAX_LENGTH} characters`);
55
+ }
56
+ return runId;
57
+ }
58
+
59
+ function renderValue(value: unknown, arguments_: Record<string, SkillArgumentValue>): unknown {
60
+ if (typeof value === "string") {
61
+ const exact = value.match(EXACT_PLACEHOLDER_PATTERN);
62
+ if (exact) {
63
+ const name = exact[1]!;
64
+ if (!(name in arguments_)) throw new Error(`skill input placeholder "${name}" has no argument value`);
65
+ return arguments_[name]!;
66
+ }
67
+ return value.replace(PLACEHOLDER_PATTERN, (_placeholder, name: string) => {
68
+ if (!(name in arguments_)) throw new Error(`skill input placeholder "${name}" has no argument value`);
69
+ return String(arguments_[name]!);
70
+ });
71
+ }
72
+ if (Array.isArray(value)) return value.map((entry) => renderValue(entry, arguments_));
73
+ if (typeof value !== "object" || value === null) return value;
74
+ const rendered: Record<string, unknown> = {};
75
+ for (const [key, entry] of Object.entries(value)) {
76
+ if (UNSAFE_KEYS.has(key)) throw new Error(`unsafe skill blueprint key "${key}"`);
77
+ rendered[key] = renderValue(entry, arguments_);
78
+ }
79
+ return rendered;
80
+ }
81
+
82
+ function renderDefinition(definition: SkillDefinition, arguments_: Record<string, SkillArgumentValue>): SkillDefinition {
83
+ const rendered = renderValue(definition, arguments_) as SkillDefinition;
84
+ const bytes = new TextEncoder().encode(JSON.stringify(rendered)).byteLength;
85
+ if (bytes > SKILL_MAX_RENDERED_BYTES) throw new Error(`rendered skill workflow exceeds ${SKILL_MAX_RENDERED_BYTES} bytes`);
86
+ for (const task of rendered.blueprints.tasks) {
87
+ if (task.extra?.["checklist"] !== undefined) {
88
+ task.extra["checklist"] = validateChecklist(task.extra["checklist"]);
89
+ }
90
+ }
91
+ return validateSkillDefinition(rendered);
92
+ }
93
+
94
+ function withRunLabel(labels: string[] | undefined, runId: string): string[] {
95
+ return [...new Set([...(labels ?? []), `skill-run:${runId}`])];
96
+ }
97
+
98
+ function executionGraph(tasks: Artifact[], definition: SkillDefinition, ids: Map<string, string>): TaskGraph {
99
+ const byRef = new Map(definition.blueprints.tasks.map((task) => [task.ref, task]));
100
+ const nodes: TaskNode[] = tasks.map((task) => {
101
+ const ref = task.extra["skillRun"] && typeof task.extra["skillRun"] === "object"
102
+ ? (task.extra["skillRun"] as Record<string, unknown>)["ref"] as string
103
+ : "";
104
+ const blueprint = byRef.get(ref)!;
105
+ return {
106
+ task,
107
+ active: false,
108
+ parentIds: blueprint.parent ? [ids.get(blueprint.parent)!] : [],
109
+ childIds: definition.blueprints.tasks.filter((candidate) => candidate.parent === ref).map((candidate) => ids.get(candidate.ref)!),
110
+ dependencyIds: (blueprint.dependsOn ?? []).map((dependency) => ids.get(dependency)!),
111
+ };
112
+ });
113
+ return { nodes, rootIds: nodes.filter((node) => node.parentIds.length === 0).map((node) => node.task.id) };
114
+ }
115
+
116
+ export function instantiateSkillWorkflow(
117
+ artifacts: ArtifactStore,
118
+ skillId: string,
119
+ input: InstantiateSkillWorkflowInput = {},
120
+ history?: { events: TaskEventStore; context?: TaskEventContext },
121
+ ): SkillWorkflowRunResult {
122
+ const { definition } = requireWorkflowSkill(artifacts, skillId);
123
+ const arguments_ = resolveSkillArguments(definition, input.arguments);
124
+ const rendered = renderDefinition(definition, arguments_);
125
+ const runId = normalizeRunId(skillId, input.runId);
126
+ const refs = [
127
+ ...rendered.blueprints.docs.map(({ ref }) => ref),
128
+ ...rendered.blueprints.rules.map(({ ref }) => ref),
129
+ ...rendered.blueprints.tasks.map(({ ref }) => ref),
130
+ ];
131
+ const ids = new Map(refs.map((ref) => [ref, `${runId}-${ref}`]));
132
+ const taskIds = rendered.blueprints.tasks.map(({ ref }) => ids.get(ref)!);
133
+ const rootTaskIds = rendered.blueprints.tasks
134
+ .filter((task) => (task.dependsOn?.length ?? 0) === 0)
135
+ .map((task) => ids.get(task.ref)!);
136
+ const relationshipCount = rendered.links.length
137
+ + rendered.blueprints.tasks.reduce((count, task) => count + (task.dependsOn?.length ?? 0) + (task.parent ? 2 : 0), 0)
138
+ + rootTaskIds.length;
139
+ if (relationshipCount > TASK_EXECUTION_MAX_EDGES) {
140
+ throw new Error(`skill workflow run exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
141
+ }
142
+
143
+ const atomic = requireAtomicArtifactStore(artifacts);
144
+ const persist = () => atomic.atomic(() => {
145
+ const docs = rendered.blueprints.docs.map((blueprint) => artifacts.create({
146
+ id: ids.get(blueprint.ref),
147
+ kind: "doc",
148
+ title: blueprint.title,
149
+ body: blueprint.body,
150
+ subtype: blueprint.subtype,
151
+ labels: withRunLabel(blueprint.labels, runId),
152
+ extra: { ...(blueprint.extra ?? {}), skillRun: { id: runId, skillId, ref: blueprint.ref } },
153
+ }));
154
+ const rules = rendered.blueprints.rules.map((blueprint) => artifacts.create({
155
+ id: ids.get(blueprint.ref),
156
+ kind: "rule",
157
+ title: blueprint.title,
158
+ body: blueprint.body,
159
+ labels: withRunLabel(blueprint.labels, runId),
160
+ extra: {
161
+ ...(blueprint.extra ?? {}),
162
+ ...(blueprint.condition ? { condition: blueprint.condition } : {}),
163
+ ...(blueprint.action ? { action: blueprint.action } : {}),
164
+ ...(blueprint.severity ? { severity: blueprint.severity } : {}),
165
+ skillRun: { id: runId, skillId, ref: blueprint.ref },
166
+ scope: { type: "skill-run", runId, taskIds },
167
+ },
168
+ }));
169
+ const tasks = rendered.blueprints.tasks.map((blueprint) => {
170
+ const task = artifacts.create({
171
+ id: ids.get(blueprint.ref),
172
+ kind: "task",
173
+ title: blueprint.title,
174
+ body: blueprint.body,
175
+ labels: withRunLabel(blueprint.labels, runId),
176
+ extra: { ...(blueprint.extra ?? {}), skillRun: { id: runId, skillId, ref: blueprint.ref } },
177
+ });
178
+ if (history) history.events.append({
179
+ taskId: task.id,
180
+ type: "created",
181
+ actor: history.context?.actor ?? "system",
182
+ source: history.context?.source ?? "skill-run",
183
+ toStatus: task.status as TaskStatus,
184
+ ...(history.context?.sessionId === undefined ? {} : { sessionId: history.context.sessionId }),
185
+ ...(history.context?.reason === undefined ? {} : { reason: history.context.reason }),
186
+ });
187
+ return task;
188
+ });
189
+
190
+ for (const blueprint of rendered.blueprints.tasks) {
191
+ const id = ids.get(blueprint.ref)!;
192
+ for (const dependency of blueprint.dependsOn ?? []) {
193
+ artifacts.link({ from: id, relation: "depends_on", to: ids.get(dependency)! });
194
+ }
195
+ if (blueprint.parent) {
196
+ const parentId = ids.get(blueprint.parent)!;
197
+ artifacts.link({ from: parentId, relation: "contains", to: id });
198
+ artifacts.link({ from: id, relation: "part_of", to: parentId });
199
+ }
200
+ }
201
+ for (const link of rendered.links) {
202
+ artifacts.link({ from: ids.get(link.from)!, relation: link.relation, to: ids.get(link.to)! });
203
+ }
204
+ for (const rootTaskId of rootTaskIds) artifacts.link({ from: skillId, relation: "triggers", to: rootTaskId });
205
+
206
+ return {
207
+ skillId,
208
+ runId,
209
+ arguments: arguments_,
210
+ created: {
211
+ docs: docs.map(({ id }) => id),
212
+ rules: rules.map(({ id }) => id),
213
+ tasks: tasks.map(({ id }) => id),
214
+ },
215
+ rootTaskIds,
216
+ execution: projectTaskExecution(executionGraph(tasks, rendered, ids)),
217
+ };
218
+ });
219
+ return history ? history.events.atomic(persist) : persist();
220
+ }