@danypops/papyrus 0.40.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.
- package/README.md +9 -12
- package/package.json +1 -1
- package/src/artifact-relationship-view.ts +1 -1
- package/src/cli.ts +12 -178
- package/src/constants.ts +19 -52
- package/src/db.ts +34 -7
- package/src/domain/artifact-event.ts +1 -1
- package/src/domain/blueprint-definition.ts +273 -0
- package/src/domain-services.ts +147 -204
- package/src/modules/logs.ts +1 -1
- package/src/modules/playbooks.ts +9 -7
- package/src/ops.ts +3 -1
- package/src/playbook-definition.ts +75 -29
- package/src/playbook-execution.ts +7 -24
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/service.ts +5 -21
- package/src/task-service.ts +1 -1
- package/src/vehicle/artifact-trash-vehicle.ts +1 -1
- package/src/vehicle/artifact-vehicle-shared.ts +4 -6
- package/src/vehicle/docs-vehicle.ts +2 -2
- package/src/vehicle/notes-vehicle.ts +2 -2
- package/src/vehicle/papyrus-vehicle.ts +6 -7
- package/src/vehicle/playbooks-vehicle.ts +1 -1
- package/src/vehicle/tasks-vehicle.ts +407 -0
- package/src/workflow-execution.ts +139 -69
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- package/src/vehicle/skills-vehicle.ts +0 -194
package/src/domain-services.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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/
|
|
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/
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
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
|
-
|
|
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?:
|
|
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
|
-
...(
|
|
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,
|
|
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"]
|
|
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
|
-
|
|
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) =>
|
|
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,
|
|
715
|
-
*
|
|
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,
|
|
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
|
|
package/src/modules/logs.ts
CHANGED
|
@@ -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/
|
|
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
|
*/
|
package/src/modules/playbooks.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* modules/playbooks.ts — Playbooks as a Papyrus-native registered module.
|
|
3
3
|
*
|
|
4
|
-
* A Playbook
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* recycles the
|
|
8
|
-
*
|
|
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"]
|
|
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,
|
|
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
|
-
|
|
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
|
|