@danypops/papyrus 0.41.0 → 0.42.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.
@@ -9,14 +9,19 @@ import {
9
9
  PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH,
10
10
  PLAYBOOK_INVOCATION_MAX_CALL_DEPTH,
11
11
  PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS,
12
+ PLAYBOOK_MAX_STEPS,
12
13
  RULE_TEXT_HARD_LIMIT_CHARACTERS,
13
- SKILL_INVOCATION_MAX_CALL_DEPTH,
14
- SKILL_INVOCATION_MAX_LINKED_ARTIFACTS,
14
+ SKILL_MAX_ENUM_VALUES,
15
15
  } from "./constants.ts";
16
16
  import { requireLocallyOwnedContent, type Artifact, type CreateArtifactInput } from "./domain/artifact.ts";
17
17
  import type { ArtifactEventContext } from "./domain/artifact-event.ts";
18
18
  import { normalizeProjectRoot } from "./domain/task-scope.ts";
19
- import { validateSkillDefinition } from "./domain/skill-definition.ts";
19
+ import {
20
+ BLUEPRINT_INPUT_TYPES,
21
+ validateArgumentValue,
22
+ type BlueprintArgumentValue,
23
+ type BlueprintInputType,
24
+ } from "./domain/blueprint-definition.ts";
20
25
  import type { ArtifactStore } from "./ports/artifact-store.ts";
21
26
  import type { ArtifactScopeStore } from "./ports/artifact-scope-store.ts";
22
27
  import { NOTE_SUBTYPE } from "./note-service.ts";
@@ -61,7 +66,7 @@ export interface ListFilter {
61
66
  }
62
67
 
63
68
  /**
64
- * Shared by listDocuments/listRules/listSkills: when filter.projectRoot is given, resolve
69
+ * Shared by listDocuments/listRules/listPlaybooks: when filter.projectRoot is given, resolve
65
70
  * via ArtifactScopeStore first and post-filter by kind/status/text (mirrors Tasks.list's
66
71
  * established scoped-listing shape); otherwise fall back to the existing unscoped query
67
72
  * path unchanged, so every caller that predates project scoping keeps working exactly as
@@ -85,7 +90,7 @@ function listScoped(artifacts: ArtifactStore, scopes: ArtifactScopeStore, kind:
85
90
  .slice(0, limit);
86
91
  }
87
92
 
88
- /** Shared by assignDocumentProject/assignRuleProject/assignSkillProject. */
93
+ /** Shared by assignDocumentProject/assignRuleProject/assignPlaybookProject. */
89
94
  function assignArtifactProject(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string, kind: string, projectRoot: string | undefined): Artifact {
90
95
  requireKind(artifacts, id, kind);
91
96
  scopes.assign(id, projectRoot === undefined ? undefined : normalizeProjectRoot(projectRoot), projectRoot === undefined ? "unscoped" : "explicit");
@@ -287,14 +292,21 @@ export function assignRuleProject(artifacts: ArtifactStore, scopes: ArtifactScop
287
292
  return assignArtifactProject(artifacts, scopes, id, "rule", projectRoot);
288
293
  }
289
294
 
290
- /** Global rules always apply; scoped workflow rules apply only while their run owns active focus. */
295
+ /**
296
+ * Global rules always apply; scoped workflow-run rules apply only while their run owns active
297
+ * focus. Both a workflow-definition target's own run scope ("skill-run", written by
298
+ * workflow-execution.ts's runWorkflowSteps for that target kind) and a Playbook's own run scope
299
+ * ("playbook-run", same call for a Playbook target) are recognized -- confirmed live that only
300
+ * "skill-run" was ever checked here, silently breaking Playbook-run-scoped rule injection since
301
+ * Playbook gained its own doc/rule structured steps.
302
+ */
291
303
  export function listInjectableRules(artifacts: ArtifactStore, activeTaskId?: string): Artifact[] {
292
304
  return artifacts.query({ kind: "rule", status: "active" }).filter((rule) => {
293
305
  const scope = rule.extra["scope"];
294
306
  if (scope === undefined) return true;
295
307
  if (typeof scope !== "object" || scope === null || Array.isArray(scope)) return false;
296
308
  const value = scope as Record<string, unknown>;
297
- if (value["type"] !== "skill-run" || !Array.isArray(value["taskIds"])) return false;
309
+ if ((value["type"] !== "skill-run" && value["type"] !== "playbook-run") || !Array.isArray(value["taskIds"])) return false;
298
310
  return activeTaskId !== undefined && value["taskIds"].some((id) => id === activeTaskId);
299
311
  });
300
312
  }
@@ -344,191 +356,11 @@ export function gateTaskWithRule(artifacts: ArtifactStore, ruleId: string, taskI
344
356
  return showRule(artifacts, ruleId);
345
357
  }
346
358
 
347
- export interface CreateSkillInput {
348
- title: string;
349
- body?: string;
350
- trigger?: string;
351
- steps?: string[];
352
- tools?: string[];
353
- definition?: unknown;
354
- labels?: string[];
355
- extra?: Record<string, unknown>;
356
- projectRoot?: string;
357
- }
358
-
359
- export interface CreateArtifactTemplateInput {
360
- title: string;
361
- targetKind: string;
362
- defaults?: Record<string, unknown>;
363
- required?: string[];
364
- body?: string;
365
- labels?: string[];
366
- projectRoot?: string;
367
- }
368
-
369
- export type SkillTransition = "enable" | "disable";
370
-
371
- export function createSkill(artifacts: ArtifactStore, scopes: ArtifactScopeStore, input: CreateSkillInput, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
372
- if (input.definition !== undefined && (input.trigger !== undefined || input.steps !== undefined || input.tools !== undefined)) {
373
- throw new Error("workflow Skill definition cannot be mixed with legacy trigger, steps, or tools");
374
- }
375
- const definition = input.definition === undefined ? undefined : validateSkillDefinition(input.definition);
376
- if (definition?.blueprints.docs.some((document) => document.subtype === NOTE_SUBTYPE)) requireNotesFacade(authority, "skills");
377
- const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
378
- const skill = artifacts.create({
379
- kind: "skill",
380
- status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
381
- subtype: definition ? "workflow" : undefined,
382
- title: input.title,
383
- body: input.body,
384
- labels: input.labels,
385
- extra: {
386
- ...(input.extra ?? {}),
387
- ...(definition ? { definition } : {}),
388
- ...(input.trigger ? { trigger: input.trigger } : {}),
389
- ...(input.steps ? { steps: input.steps } : {}),
390
- ...(input.tools ? { tools: input.tools } : {}),
391
- },
392
- }, context);
393
- scopes.assign(skill.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
394
- return skill;
395
- }
396
-
397
- export function createArtifactTemplate(artifacts: ArtifactStore, scopes: ArtifactScopeStore, input: CreateArtifactTemplateInput, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
398
- if (input.targetKind === "doc" && input.defaults?.["subtype"] === NOTE_SUBTYPE) requireNotesFacade(authority, "skills");
399
- const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
400
- const template = artifacts.create({
401
- kind: "skill",
402
- status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
403
- subtype: "artifact-template",
404
- title: input.title,
405
- body: input.body,
406
- labels: input.labels,
407
- extra: {
408
- targetKind: input.targetKind,
409
- defaults: input.defaults ?? {},
410
- required: input.required ?? ["title"],
411
- },
412
- }, context);
413
- scopes.assign(template.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
414
- return template;
415
- }
416
-
417
- export function instantiateTemplate(artifacts: ArtifactStore, templateId: string, input: CreateArtifactInput, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
418
- if (rejectsNoteTemplate(artifacts, templateId, input.subtype)) requireNotesFacade(authority, "skills");
419
- return artifacts.create({ ...input, templateId }, context);
420
- }
421
-
422
- export function listSkills(artifacts: ArtifactStore, scopes: ArtifactScopeStore, filter: ListFilter): Artifact[] {
423
- return listScoped(artifacts, scopes, "skill", filter);
424
- }
425
-
426
- export function assignSkillProject(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string, projectRoot: string | undefined): Artifact {
427
- return assignArtifactProject(artifacts, scopes, id, "skill", projectRoot);
428
- }
429
-
430
- export function showSkill(artifacts: ArtifactStore, id: string): Artifact {
431
- requireKind(artifacts, id, "skill");
432
- return artifacts.get(id, { tree: true })!;
433
- }
434
-
435
- export type UpdateSkillInput = UpdateContentInput;
436
-
437
- export function updateSkill(artifacts: ArtifactStore, id: string, input: UpdateSkillInput, context?: ArtifactEventContext): Artifact {
438
- requireContentUpdateFields(input);
439
- assertTitleBounds(input.title);
440
- assertBodyBounds(input.body);
441
- assertLabelsBounds(input.labels);
442
- const skill = requireLocallyOwnedContent(requireKind(artifacts, id, "skill"));
443
- const updated = artifacts.updateContent(skill.id, input, context);
444
- if (!updated) throw new Error(`skill "${id}" not found`);
445
- return updated;
446
- }
447
-
448
- function skillInvocationBody(skill: Artifact): string {
449
- if (skill.subtype === "artifact-template") {
450
- return `Create an artifact using Papyrus template "${skill.title}".\ntemplate_name: ${skill.title}\nAsk for or infer all required template fields, then call the skills domain tool instantiate action.`;
451
- }
452
- if (skill.subtype === "workflow") {
453
- const definition = validateSkillDefinition(skill.extra["definition"]);
454
- const required = Object.entries(definition.inputs)
455
- .filter(([, input]) => input.required && input.default === undefined)
456
- .map(([name]) => name);
457
- return [
458
- `Run Papyrus workflow Skill "${skill.title}".`,
459
- `Required arguments: ${required.length > 0 ? required.join(", ") : "none"}.`,
460
- "Call the skills domain tool with action=run and arguments after collecting required values.",
461
- ].join("\n");
462
- }
463
- const trigger = typeof skill.extra["trigger"] === "string" ? skill.extra["trigger"] : "manual invocation";
464
- const steps = Array.isArray(skill.extra["steps"]) ? skill.extra["steps"].filter((step): step is string => typeof step === "string") : [];
465
- const tools = Array.isArray(skill.extra["tools"]) ? skill.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
466
- return [
467
- `Apply Papyrus skill "${skill.title}".`,
468
- `Trigger: ${trigger}`,
469
- ...(skill.body ? [`Context: ${skill.body}`] : []),
470
- ...(steps.length ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
471
- ...(tools.length ? [`Tools: ${tools.join(", ")}`] : []),
472
- ].join("\n");
473
- }
474
-
475
359
  /**
476
- * Skills are special: invoking one queries Papyrus for the skill's real outgoing graph edges
477
- * -- not just its own static body/extra fields -- so a Skill linked to existing Tasks, Rules,
478
- * or Docs surfaces that linked context on invocation. A Skill can also link to and invoke
479
- * OTHER Skills (any relation whose target is itself a Skill, e.g. the same "triggers" relation
480
- * workflow execution already uses for skill-to-task edges): invoking the parent recursively
481
- * composes the linked skill's own invocation. Bounded and cycle-safe -- a skill-calls-skill
482
- * edge cycle degrades to a marker instead of infinite-looping, matching the cycle-safety
483
- * discipline established by task dependency graphs and the (since-removed; see Doc
484
- * "ConversationJournal design record") ConversationJournal domain's own reply chains.
485
- * `visited` and `depth` are recursion-internal; callers should not pass them.
486
- */
487
- export function skillInvocation(artifacts: ArtifactStore, id: string, visited: Set<string> = new Set(), depth = 0): string {
488
- const skill = requireKind(artifacts, id, "skill");
489
- visited.add(id);
490
- const sections = [skillInvocationBody(skill)];
491
-
492
- const edges = artifacts.relationships({ artifactIds: [id] }).filter((edge) => edge.from === id).slice(0, SKILL_INVOCATION_MAX_LINKED_ARTIFACTS);
493
- const linkedArtifactLines: string[] = [];
494
- const linkedSkillSections: string[] = [];
495
- for (const edge of edges) {
496
- const target = artifacts.get(edge.to);
497
- if (!target) continue; // dangling edge -- defensive, should not happen
498
- if (target.kind !== "skill") {
499
- linkedArtifactLines.push(`- ${edge.relation} ${target.kind} "${target.title}"`);
500
- continue;
501
- }
502
- if (visited.has(target.id)) {
503
- linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" -- already invoked above in this chain, not repeated.`);
504
- } else if (depth + 1 > SKILL_INVOCATION_MAX_CALL_DEPTH) {
505
- linkedSkillSections.push(`Also linked via ${edge.relation} to skill "${target.title}" -- call depth limit reached, invoke it separately.`);
506
- } else {
507
- const nested = skillInvocation(artifacts, target.id, visited, depth + 1);
508
- linkedSkillSections.push(`Also invoke linked skill (${edge.relation}) "${target.title}":\n${nested}`);
509
- }
510
- }
511
- if (linkedArtifactLines.length > 0) {
512
- sections.push(["Linked context (query Papyrus for full detail before proceeding):", ...linkedArtifactLines].join("\n"));
513
- }
514
- for (const section of linkedSkillSections) sections.push(section);
515
- return sections.join("\n\n");
516
- }
517
-
518
- export function transitionSkill(artifacts: ArtifactStore, id: string, action: SkillTransition, context?: ArtifactEventContext): Artifact {
519
- const skill = requireLocallyOwnedContent(requireKind(artifacts, id, "skill"));
520
- const expected = action === "enable" ? "deprecated" : "active";
521
- const target = action === "enable" ? "active" : "deprecated";
522
- if (skill.status !== expected) throw new Error(`cannot ${action} skill from ${skill.status}`);
523
- return artifacts.setStatus(id, target, context)!;
524
- }
525
-
526
- /**
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
360
+ * Playbooks: a trigger and an ordered list of steps -- authored as prose. But playbooks.invoke
361
+ * (playbook-execution.ts) recycles the shared blueprint materialization engine: it compiles a
362
+ * Playbook into a BlueprintDefinition and mechanically instantiates real Tasks from it.
363
+ * `playbookInvocation` below is the OTHER, older
532
364
  * path -- rendered text with no side effects, now exposed as the `preview` action for a human
533
365
  * who wants to just read a playbook before invoking it, not the primary way of running one.
534
366
  * Like Tasks, a Playbook can be nested or chained with another Playbook: `contains`/`part_of`
@@ -546,6 +378,10 @@ export interface PlaybookArgument {
546
378
  description?: string;
547
379
  /** Defaults true: naming an argument at all is a signal it matters, so an author must opt out explicitly to make one optional. */
548
380
  required: boolean;
381
+ /** Defaults "string" (unchanged behavior for every argument declared before typed arguments existed). Validated the exact same way a workflow-definition target's own BlueprintInputDefinition is (domain/blueprint-definition.ts), not re-derived here. */
382
+ type: BlueprintInputType;
383
+ enum?: BlueprintArgumentValue[];
384
+ default?: BlueprintArgumentValue;
549
385
  }
550
386
 
551
387
  /** Rejects malformed input rather than silently dropping a bad entry -- the same posture creation validation already takes everywhere else. */
@@ -569,7 +405,98 @@ function validatePlaybookArguments(value: unknown): PlaybookArgument[] | undefin
569
405
  }
570
406
  const required = record["required"];
571
407
  if (required !== undefined && typeof required !== "boolean") throw new Error(`argument "${name}" required must be a boolean`);
572
- return { name, ...(description !== undefined ? { description: description as string } : {}), required: required !== false };
408
+ const type = record["type"] === undefined ? "string" : record["type"];
409
+ if (!BLUEPRINT_INPUT_TYPES.has(type as BlueprintInputType)) throw new Error(`argument "${name}" has unsupported type`);
410
+ const result: PlaybookArgument = { name, required: required !== false, type: type as BlueprintInputType };
411
+ if (description !== undefined) result.description = description as string;
412
+ if (record["default"] !== undefined) result.default = validateArgumentValue(name, result.type, record["default"]);
413
+ if (record["enum"] !== undefined) {
414
+ const values = record["enum"];
415
+ if (!Array.isArray(values) || values.length === 0 || values.length > SKILL_MAX_ENUM_VALUES) {
416
+ throw new Error(`argument "${name}" enum must contain 1-${SKILL_MAX_ENUM_VALUES} values`);
417
+ }
418
+ result.enum = values.map((entry_) => validateArgumentValue(name, result.type, entry_));
419
+ if (result.default !== undefined && !result.enum.includes(result.default)) {
420
+ throw new Error(`argument "${name}" default must be one of its enum values`);
421
+ }
422
+ }
423
+ return result;
424
+ });
425
+ }
426
+
427
+ /** A plain string step is an ordinary prose task -- unchanged since Playbooks first existed. A structured step declares one of the other three Blueprint kinds (domain/blueprint-definition.ts): a Doc, a Rule, or a nested pipeline call into another Playbook (or a workflow-definition target). No `ref` field -- refs are compiler-assigned (playbook-definition.ts); a Playbook author never sees them, keeping the common case exactly as prose-simple as a plain string. */
428
+ export type PlaybookStep =
429
+ | string
430
+ | { kind: "task"; title?: string; body: string }
431
+ | { kind: "doc"; title: string; body?: string; subtype?: string; labels?: string[] }
432
+ | { kind: "rule"; title: string; body?: string; condition?: string; action?: string; severity?: "block" | "warn" | "info"; labels?: string[] }
433
+ | { kind: "call"; title: string; playbookId: string; arguments?: Record<string, unknown> };
434
+
435
+ function validateStructuredStep(value: Record<string, unknown>, index: number): PlaybookStep {
436
+ const kind = value["kind"];
437
+ const title = value["title"];
438
+ if (kind === "task") {
439
+ const body = value["body"];
440
+ if (typeof body !== "string" || body.trim().length === 0) throw new Error(`step ${index} (task) requires a non-empty body`);
441
+ return { kind: "task", body, ...(typeof title === "string" && title.length > 0 ? { title } : {}) };
442
+ }
443
+ if (typeof title !== "string" || title.trim().length === 0 || title.length > ARTIFACT_TITLE_MAX_LENGTH) {
444
+ throw new Error(`step ${index} (${String(kind)}) requires a title between 1 and ${ARTIFACT_TITLE_MAX_LENGTH} characters`);
445
+ }
446
+ if (kind === "doc" || kind === "rule") {
447
+ const body = value["body"];
448
+ if (body !== undefined && typeof body !== "string") throw new Error(`step ${index} (${kind}) body must be a string`);
449
+ const labels = value["labels"];
450
+ if (labels !== undefined && (!Array.isArray(labels) || labels.some((label) => typeof label !== "string"))) {
451
+ throw new Error(`step ${index} (${kind}) labels must be a string array`);
452
+ }
453
+ if (kind === "doc") {
454
+ const subtype = value["subtype"];
455
+ if (subtype !== undefined && typeof subtype !== "string") throw new Error(`step ${index} (doc) subtype must be a string`);
456
+ return { kind: "doc", title, ...(body !== undefined ? { body: body as string } : {}), ...(subtype !== undefined ? { subtype: subtype as string } : {}), ...(labels !== undefined ? { labels: labels as string[] } : {}) };
457
+ }
458
+ const condition = value["condition"];
459
+ const action = value["action"];
460
+ const severity = value["severity"];
461
+ if (condition !== undefined && typeof condition !== "string") throw new Error(`step ${index} (rule) condition must be a string`);
462
+ if (action !== undefined && typeof action !== "string") throw new Error(`step ${index} (rule) action must be a string`);
463
+ if (severity !== undefined && severity !== "block" && severity !== "warn" && severity !== "info") {
464
+ throw new Error(`step ${index} (rule) severity must be block, warn, or info`);
465
+ }
466
+ return {
467
+ kind: "rule",
468
+ title,
469
+ ...(body !== undefined ? { body: body as string } : {}),
470
+ ...(condition !== undefined ? { condition: condition as string } : {}),
471
+ ...(action !== undefined ? { action: action as string } : {}),
472
+ ...(severity !== undefined ? { severity: severity as "block" | "warn" | "info" } : {}),
473
+ ...(labels !== undefined ? { labels: labels as string[] } : {}),
474
+ };
475
+ }
476
+ if (kind === "call") {
477
+ const playbookId = value["playbookId"];
478
+ if (typeof playbookId !== "string" || playbookId.trim().length === 0) throw new Error(`step ${index} (call) requires a playbookId`);
479
+ const callArguments = value["arguments"];
480
+ if (callArguments !== undefined && (typeof callArguments !== "object" || callArguments === null || Array.isArray(callArguments))) {
481
+ throw new Error(`step ${index} (call) arguments must be an object`);
482
+ }
483
+ return { kind: "call", title, playbookId, ...(callArguments !== undefined ? { arguments: callArguments as Record<string, unknown> } : {}) };
484
+ }
485
+ throw new Error(`step ${index} has unknown kind "${String(kind)}"`);
486
+ }
487
+
488
+ /** A plain string passes through unchanged (the entire authoring surface before this extension); an object is validated against one of the three structured step kinds. Rejects malformed input rather than silently dropping it, matching validatePlaybookArguments' own posture. */
489
+ function validatePlaybookSteps(value: unknown): PlaybookStep[] | undefined {
490
+ if (value === undefined) return undefined;
491
+ if (!Array.isArray(value)) throw new Error("playbook steps must be an array");
492
+ if (value.length > PLAYBOOK_MAX_STEPS) throw new Error(`playbook steps cannot exceed ${PLAYBOOK_MAX_STEPS} entries`);
493
+ return value.map((entry, index) => {
494
+ if (typeof entry === "string") {
495
+ if (entry.trim().length === 0) throw new Error(`step ${index} must not be empty`);
496
+ return entry;
497
+ }
498
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`step ${index} must be a string or a structured step object`);
499
+ return validateStructuredStep(entry as Record<string, unknown>, index);
573
500
  });
574
501
  }
575
502
 
@@ -577,7 +504,7 @@ export interface CreatePlaybookInput {
577
504
  title: string;
578
505
  body?: string;
579
506
  trigger?: string;
580
- steps?: string[];
507
+ steps?: unknown;
581
508
  tools?: string[];
582
509
  /** Declares named arguments this Playbook needs -- see playbookInvocation for how a missing required one surfaces. */
583
510
  arguments?: unknown;
@@ -592,6 +519,7 @@ export type UpdatePlaybookInput = UpdateContentInput;
592
519
  export function createPlaybook(artifacts: ArtifactStore, scopes: ArtifactScopeStore, input: CreatePlaybookInput, context?: ArtifactEventContext): Artifact {
593
520
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
594
521
  const declaredArguments = validatePlaybookArguments(input.arguments);
522
+ const declaredSteps = validatePlaybookSteps(input.steps);
595
523
  const playbook = artifacts.create({
596
524
  kind: "playbook",
597
525
  status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
@@ -601,7 +529,7 @@ export function createPlaybook(artifacts: ArtifactStore, scopes: ArtifactScopeSt
601
529
  extra: {
602
530
  ...(input.extra ?? {}),
603
531
  ...(input.trigger ? { trigger: input.trigger } : {}),
604
- ...(input.steps ? { steps: input.steps } : {}),
532
+ ...(declaredSteps ? { steps: declaredSteps } : {}),
605
533
  ...(input.tools ? { tools: input.tools } : {}),
606
534
  ...(declaredArguments ? { arguments: declaredArguments } : {}),
607
535
  },
@@ -678,19 +606,34 @@ export function undependPlaybook(artifacts: ArtifactStore, id: string, dependenc
678
606
  return showPlaybook(artifacts, id);
679
607
  }
680
608
 
609
+ /** Text rendering for one preview step, covering all four Blueprint kinds -- distinct from playbook-definition.ts's stepTitle (a compiled Task's title), since a preview is read by a human/agent deciding whether to invoke, not turned into a real artifact. */
610
+ function stepText(step: PlaybookStep, index: number): string {
611
+ if (typeof step === "string") return `${index + 1}. ${step}`;
612
+ if (step.kind === "task") return `${index + 1}. ${step.title ? `${step.title} -- ` : ""}${step.body}`;
613
+ if (step.kind === "doc") return `${index + 1}. [creates Doc] "${step.title}"${step.subtype ? ` (${step.subtype})` : ""}`;
614
+ if (step.kind === "rule") return `${index + 1}. [creates Rule] "${step.title}"${step.condition ? ` -- when: ${step.condition}` : ""}`;
615
+ return `${index + 1}. [calls playbook] "${step.title}" -> ${step.playbookId}`;
616
+ }
617
+
618
+ function argumentQualifier(argument: PlaybookArgument): string {
619
+ const qualifier = argument.required ? "required" : "optional";
620
+ const type = argument.type === "string" ? "" : `, ${argument.type}`;
621
+ const options = argument.enum ? `, one of: ${argument.enum.join(", ")}` : "";
622
+ return `${qualifier}${type}${options}`;
623
+ }
624
+
681
625
  /** 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 {
626
+ function playbookInvocationBody(playbook: Artifact, provided: Record<string, unknown>): string {
683
627
  const trigger = typeof playbook.extra["trigger"] === "string" ? playbook.extra["trigger"] : "manual invocation";
684
- const steps = Array.isArray(playbook.extra["steps"]) ? playbook.extra["steps"].filter((step): step is string => typeof step === "string") : [];
628
+ const steps = Array.isArray(playbook.extra["steps"]) ? (playbook.extra["steps"] as PlaybookStep[]) : [];
685
629
  const tools = Array.isArray(playbook.extra["tools"]) ? playbook.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
686
630
  const declaredArguments = Array.isArray(playbook.extra["arguments"]) ? (playbook.extra["arguments"] as PlaybookArgument[]) : [];
687
631
  const argumentLines = declaredArguments.map((argument) => {
688
632
  const value = provided[argument.name];
689
- if (value !== undefined) return `- ${argument.name}: ${value}`;
690
- const qualifier = argument.required ? "required" : "optional";
691
- return `- ${argument.name} (${qualifier}${argument.description ? `: ${argument.description}` : ""}) -- not yet provided`;
633
+ if (value !== undefined) return `- ${argument.name}: ${String(value)}`;
634
+ return `- ${argument.name} (${argumentQualifier(argument)}${argument.description ? `: ${argument.description}` : ""}) -- not yet provided`;
692
635
  });
693
- const missingRequired = declaredArguments.filter((argument) => argument.required && provided[argument.name] === undefined);
636
+ const missingRequired = declaredArguments.filter((argument) => argument.required && provided[argument.name] === undefined && argument.default === undefined);
694
637
  return [
695
638
  `Apply Papyrus playbook "${playbook.title}".`,
696
639
  `Trigger: ${trigger}`,
@@ -699,7 +642,7 @@ function playbookInvocationBody(playbook: Artifact, provided: Record<string, str
699
642
  ...(missingRequired.length > 0
700
643
  ? [`Missing required argument(s): ${missingRequired.map((argument) => argument.name).join(", ")}. Ask the human for these directly -- the discuss tool with live:true asks synchronously and gets a real answer in this same turn -- before proceeding with the steps below. Do not guess or invent a value.`]
701
644
  : []),
702
- ...(steps.length ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
645
+ ...(steps.length ? ["Steps:", ...steps.map((step, index) => stepText(step, index))] : []),
703
646
  ...(tools.length ? [`Tools: ${tools.join(", ")}`] : []),
704
647
  ].join("\n");
705
648
  }
@@ -711,14 +654,14 @@ function playbookInvocationBody(playbook: Artifact, provided: Record<string, str
711
654
  * "run as part of this one". `depends_on` chains a prerequisite -- its full steps render BEFORE
712
655
  * this playbook's own, as "complete this first". Every other relation (references, relates_to,
713
656
  * 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.
657
+ * cycle-safe -- a composition cycle degrades to a marker instead of infinite-looping, the same
658
+ * cycle-safety discipline task dependency graphs already established.
716
659
  * `provided` is the caller's already-known argument values (e.g. from the conversation so far);
717
660
  * any declared *required* argument missing from it is called out explicitly, directing the agent
718
661
  * to discuss (live:true) rather than guess or silently proceed. `visited` and `depth` are
719
662
  * recursion-internal; callers should not pass them.
720
663
  */
721
- export function playbookInvocation(artifacts: ArtifactStore, id: string, provided: Record<string, string> = {}, visited: Set<string> = new Set(), depth = 0): string {
664
+ export function playbookInvocation(artifacts: ArtifactStore, id: string, provided: Record<string, unknown> = {}, visited: Set<string> = new Set(), depth = 0): string {
722
665
  const playbook = requireKind(artifacts, id, "playbook");
723
666
  visited.add(id);
724
667
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * modules/logs.ts — the `log` domain as a registered Papyrus-native module.
3
3
  *
4
- * Deliberately self-contained: does not import artifact/task/rule/skill infrastructure --
4
+ * Deliberately self-contained: does not import artifact/task/rule/playbook infrastructure --
5
5
  * logs never touch the Artifact graph directly (see src/domain/log-entry.ts's own module
6
6
  * comment on why `log` is not an Artifact kind).
7
7
  */
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * modules/playbooks.ts — Playbooks as a Papyrus-native registered module.
3
3
  *
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
4
+ * A Playbook is prose-first, whole-artifact composition (contains/depends_on between real
5
+ * Playbook artifacts) rather than a raw JSON blueprint -- but its own step list can also
6
+ * declare Doc/Rule blueprints and typed arguments and nested pipeline calls. playbooks.invoke
7
+ * recycles the shared blueprint materialization engine (playbook-execution.ts compiles a
8
+ * Playbook's steps and composition tree into a BlueprintDefinition, then hands off to
9
9
  * workflow-execution.ts's shared core). See domain-services.ts's Playbook section and
10
10
  * playbook-definition.ts for the full rationale.
11
11
  */
@@ -98,14 +98,14 @@ export function playbooksOperations({ artifacts, events, scopes, artifactScopes,
98
98
  return [
99
99
  define("playbooks.create", (input: OperationInput) => createPlaybook(artifacts, artifactScopes, {
100
100
  title: string(input, "title"), body: optionalString(input, "body"), trigger: optionalString(input, "trigger"),
101
- steps: input["steps"] as string[] | undefined, tools: input["tools"] as string[] | undefined,
101
+ steps: input["steps"], tools: input["tools"] as string[] | undefined,
102
102
  arguments: input["arguments"],
103
103
  labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
104
104
  projectRoot: optionalString(input, "project_root"),
105
105
  }, eventContext(input))),
106
106
  define("playbooks.list", (input: OperationInput) => listPlaybooks(artifacts, artifactScopes, artifactFilter(input))),
107
107
  define("playbooks.show", (input: OperationInput) => showPlaybook(artifacts, string(input, "id"))),
108
- define("playbooks.preview", (input: OperationInput) => playbookInvocation(artifacts, string(input, "id"), input["arguments"] as Record<string, string> | undefined)),
108
+ define("playbooks.preview", (input: OperationInput) => playbookInvocation(artifacts, string(input, "id"), input["arguments"] as Record<string, unknown> | undefined)),
109
109
  define("playbooks.invoke", (input: OperationInput) => {
110
110
  const result = invokePlaybook(artifacts, string(input, "id"), {
111
111
  runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
@@ -129,3 +129,5 @@ export function playbooksOperations({ artifacts, events, scopes, artifactScopes,
129
129
  define("playbooks.undepend", (input: OperationInput) => undependPlaybook(artifacts, string(input, "id"), string(input, "dependency_id"), eventContext(input))),
130
130
  ];
131
131
  }
132
+
133
+
package/src/ops.ts CHANGED
@@ -75,7 +75,9 @@ function resolveCreateInput(db: Db, input: CreateInput): ResolvedCreateInput {
75
75
 
76
76
  const template = getArtifact(db, input.templateId);
77
77
  if (!template) throw new Error(`template "${input.templateId}" not found`);
78
- if (template.kind !== "skill" || template.subtype !== "artifact-template") {
78
+ // A template is any artifact carrying subtype=artifact-template metadata -- its own kind is
79
+ // irrelevant to its function as a defaults/required carrier for the *target* kind.
80
+ if (template.subtype !== "artifact-template") {
79
81
  throw new Error(`artifact "${input.templateId}" is not an artifact template`);
80
82
  }
81
83