@danypops/papyrus 0.41.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +8 -11
  2. package/package.json +2 -2
  3. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  4. package/src/adapters/sqlite-artifact-store.ts +7 -5
  5. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  6. package/src/adapters/sqlite-gate-runner.ts +1 -1
  7. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  8. package/src/adapters/sqlite-log-store.ts +36 -17
  9. package/src/adapters/sqlite-note-event-store.ts +20 -16
  10. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  11. package/src/adapters/sqlite-task-event-store.ts +29 -21
  12. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  13. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  14. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  15. package/src/artifact-relationship-view.ts +4 -4
  16. package/src/artifact-subtree.ts +4 -2
  17. package/src/authority-registry.ts +2 -1
  18. package/src/cli.ts +794 -354
  19. package/src/client.ts +6 -3
  20. package/src/constants.ts +34 -56
  21. package/src/daemon-state.ts +4 -12
  22. package/src/daemon.ts +31 -9
  23. package/src/db.ts +153 -105
  24. package/src/discussion-service.ts +109 -44
  25. package/src/domain/artifact-event.ts +18 -5
  26. package/src/domain/artifact.ts +3 -1
  27. package/src/domain/blueprint-definition.ts +268 -0
  28. package/src/domain/checklist.ts +20 -17
  29. package/src/domain/discussion.ts +37 -18
  30. package/src/domain/gate.ts +7 -7
  31. package/src/domain/log-entry.ts +1 -1
  32. package/src/domain/note-event.ts +20 -7
  33. package/src/domain/task-event.ts +17 -7
  34. package/src/domain-services.ts +362 -288
  35. package/src/graph-projection-service.ts +34 -8
  36. package/src/id-migration.ts +17 -4
  37. package/src/index.ts +16 -11
  38. package/src/log-service.ts +6 -5
  39. package/src/log.ts +19 -0
  40. package/src/modules/discuss.ts +63 -28
  41. package/src/modules/docs.ts +74 -17
  42. package/src/modules/graph-projection.ts +20 -9
  43. package/src/modules/logs.ts +34 -22
  44. package/src/modules/notes.ts +66 -28
  45. package/src/modules/playbooks.ts +93 -32
  46. package/src/modules/rules.ts +57 -15
  47. package/src/modules/session-identity.ts +6 -2
  48. package/src/modules/tasks.ts +142 -67
  49. package/src/note-service.ts +11 -7
  50. package/src/ops.ts +134 -69
  51. package/src/playbook-definition.ts +124 -39
  52. package/src/playbook-execution.ts +18 -25
  53. package/src/ports/artifact-scope-store.ts +1 -1
  54. package/src/ports/note-event-store.ts +6 -4
  55. package/src/ports/task-event-store.ts +9 -6
  56. package/src/ports/task-focus-store.ts +17 -4
  57. package/src/ports/task-lease-store.ts +9 -4
  58. package/src/ports/task-scope-store.ts +3 -1
  59. package/src/service.ts +148 -110
  60. package/src/session-identity-service.ts +10 -2
  61. package/src/task-context.ts +28 -16
  62. package/src/task-execution.ts +4 -12
  63. package/src/task-graph-view.ts +12 -12
  64. package/src/task-relationship-view.ts +1 -3
  65. package/src/task-service.ts +168 -73
  66. package/src/vehicle/artifact-trash-vehicle.ts +26 -14
  67. package/src/vehicle/artifact-vehicle-shared.ts +32 -13
  68. package/src/vehicle/docs-vehicle.ts +50 -18
  69. package/src/vehicle/notes-vehicle.ts +26 -8
  70. package/src/vehicle/papyrus-vehicle.ts +16 -8
  71. package/src/vehicle/playbooks-vehicle.ts +88 -19
  72. package/src/vehicle/rules-vehicle.ts +58 -21
  73. package/src/vehicle/tasks-vehicle.ts +366 -54
  74. package/src/version.ts +1 -1
  75. package/src/workflow-execution.ts +198 -109
  76. package/src/domain/skill-definition.ts +0 -270
  77. package/src/modules/skills.ts +0 -158
  78. package/src/vehicle/skills-vehicle.ts +0 -194
@@ -1,3 +1,4 @@
1
+ import type { ArtifactAction, AuthorityRegistry } from "./authority-registry.ts";
1
2
  import {
2
3
  ARTIFACT_BODY_MAX_LENGTH,
3
4
  ARTIFACT_LABEL_MAX_COUNT,
@@ -9,18 +10,22 @@ import {
9
10
  PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH,
10
11
  PLAYBOOK_INVOCATION_MAX_CALL_DEPTH,
11
12
  PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS,
13
+ PLAYBOOK_MAX_STEPS,
12
14
  RULE_TEXT_HARD_LIMIT_CHARACTERS,
13
- SKILL_INVOCATION_MAX_CALL_DEPTH,
14
- SKILL_INVOCATION_MAX_LINKED_ARTIFACTS,
15
+ SKILL_MAX_ENUM_VALUES,
15
16
  } from "./constants.ts";
16
- import { requireLocallyOwnedContent, type Artifact, type CreateArtifactInput } from "./domain/artifact.ts";
17
+ import { type Artifact, requireLocallyOwnedContent } from "./domain/artifact.ts";
17
18
  import type { ArtifactEventContext } from "./domain/artifact-event.ts";
19
+ import {
20
+ BLUEPRINT_INPUT_TYPES,
21
+ type BlueprintArgumentValue,
22
+ type BlueprintInputType,
23
+ validateArgumentValue,
24
+ } from "./domain/blueprint-definition.ts";
18
25
  import { normalizeProjectRoot } from "./domain/task-scope.ts";
19
- import { validateSkillDefinition } from "./domain/skill-definition.ts";
20
- import type { ArtifactStore } from "./ports/artifact-store.ts";
21
- import type { ArtifactScopeStore } from "./ports/artifact-scope-store.ts";
22
26
  import { NOTE_SUBTYPE } from "./note-service.ts";
23
- import { type ArtifactAction, type AuthorityRegistry } from "./authority-registry.ts";
27
+ import type { ArtifactScopeStore } from "./ports/artifact-scope-store.ts";
28
+ import type { ArtifactStore } from "./ports/artifact-store.ts";
24
29
 
25
30
  export interface UpdateContentInput {
26
31
  title?: string;
@@ -41,7 +46,8 @@ function assertTitleBounds(title: string | undefined): void {
41
46
  }
42
47
 
43
48
  function assertBodyBounds(body: string | undefined): void {
44
- if (body !== undefined && body.length > ARTIFACT_BODY_MAX_LENGTH) throw new Error(`body cannot exceed ${ARTIFACT_BODY_MAX_LENGTH} characters`);
49
+ if (body !== undefined && body.length > ARTIFACT_BODY_MAX_LENGTH)
50
+ throw new Error(`body cannot exceed ${ARTIFACT_BODY_MAX_LENGTH} characters`);
45
51
  }
46
52
 
47
53
  function assertLabelsBounds(labels: string[] | undefined): void {
@@ -61,14 +67,21 @@ export interface ListFilter {
61
67
  }
62
68
 
63
69
  /**
64
- * Shared by listDocuments/listRules/listSkills: when filter.projectRoot is given, resolve
70
+ * Shared by listDocuments/listRules/listPlaybooks: when filter.projectRoot is given, resolve
65
71
  * via ArtifactScopeStore first and post-filter by kind/status/text (mirrors Tasks.list's
66
72
  * established scoped-listing shape); otherwise fall back to the existing unscoped query
67
73
  * path unchanged, so every caller that predates project scoping keeps working exactly as
68
74
  * before.
69
75
  */
70
- function listScoped(artifacts: ArtifactStore, scopes: ArtifactScopeStore, kind: string, filter: ListFilter, excludeSubtype?: string): Artifact[] {
71
- if (filter.projectRoot === undefined) return artifacts.query({ kind, excludeSubtype, status: filter.status, text: filter.text, limit: filter.limit });
76
+ function listScoped(
77
+ artifacts: ArtifactStore,
78
+ scopes: ArtifactScopeStore,
79
+ kind: string,
80
+ filter: ListFilter,
81
+ excludeSubtype?: string,
82
+ ): Artifact[] {
83
+ if (filter.projectRoot === undefined)
84
+ return artifacts.query({ kind, excludeSubtype, status: filter.status, text: filter.text, limit: filter.limit });
72
85
  const limit = filter.limit ?? ARTIFACT_SCOPE_MAX_ARTIFACTS;
73
86
  if (!Number.isInteger(limit) || limit < 1 || limit > ARTIFACT_SCOPE_MAX_ARTIFACTS) {
74
87
  throw new Error(`list limit must be between 1 and ${ARTIFACT_SCOPE_MAX_ARTIFACTS}`);
@@ -85,10 +98,20 @@ function listScoped(artifacts: ArtifactStore, scopes: ArtifactScopeStore, kind:
85
98
  .slice(0, limit);
86
99
  }
87
100
 
88
- /** Shared by assignDocumentProject/assignRuleProject/assignSkillProject. */
89
- function assignArtifactProject(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string, kind: string, projectRoot: string | undefined): Artifact {
101
+ /** Shared by assignDocumentProject/assignRuleProject/assignPlaybookProject. */
102
+ function assignArtifactProject(
103
+ artifacts: ArtifactStore,
104
+ scopes: ArtifactScopeStore,
105
+ id: string,
106
+ kind: string,
107
+ projectRoot: string | undefined,
108
+ ): Artifact {
90
109
  requireKind(artifacts, id, kind);
91
- scopes.assign(id, projectRoot === undefined ? undefined : normalizeProjectRoot(projectRoot), projectRoot === undefined ? "unscoped" : "explicit");
110
+ scopes.assign(
111
+ id,
112
+ projectRoot === undefined ? undefined : normalizeProjectRoot(projectRoot),
113
+ projectRoot === undefined ? "unscoped" : "explicit",
114
+ );
92
115
  return artifacts.get(id)!;
93
116
  }
94
117
 
@@ -103,9 +126,13 @@ function rejectsNoteTemplate(artifacts: ArtifactStore, templateId: string | unde
103
126
  if (subtype === NOTE_SUBTYPE) return true;
104
127
  if (!templateId) return false;
105
128
  const template = artifacts.get(templateId);
106
- const defaults = template?.extra["defaults"];
107
- return typeof defaults === "object" && defaults !== null && !Array.isArray(defaults)
108
- && (defaults as Record<string, unknown>)["subtype"] === NOTE_SUBTYPE;
129
+ const defaults = template?.extra.defaults;
130
+ return (
131
+ typeof defaults === "object" &&
132
+ defaults !== null &&
133
+ !Array.isArray(defaults) &&
134
+ (defaults as Record<string, unknown>).subtype === NOTE_SUBTYPE
135
+ );
109
136
  }
110
137
 
111
138
  /** caller never owns NOTE_SUBTYPE, so requireArtifactAllowed always throws — the trailing throw only satisfies TypeScript's control-flow analysis for a `never`-returning function. */
@@ -116,9 +143,9 @@ function requireNotesFacade(authority: AuthorityRegistry, caller: string): never
116
143
 
117
144
  function templateSubtype(artifacts: ArtifactStore, templateId: string | undefined): string | undefined {
118
145
  if (!templateId) return undefined;
119
- const defaults = artifacts.get(templateId)?.extra["defaults"];
146
+ const defaults = artifacts.get(templateId)?.extra.defaults;
120
147
  if (typeof defaults !== "object" || defaults === null || Array.isArray(defaults)) return undefined;
121
- const subtype = (defaults as Record<string, unknown>)["subtype"];
148
+ const subtype = (defaults as Record<string, unknown>).subtype;
122
149
  return typeof subtype === "string" ? subtype : undefined;
123
150
  }
124
151
 
@@ -152,23 +179,32 @@ const DOCUMENT_TRANSITIONS: Record<DocumentTransition, { from: string[]; to: str
152
179
  reopen: { from: ["archived"], to: "draft" },
153
180
  };
154
181
 
155
- export function createDocument(artifacts: ArtifactStore, scopes: ArtifactScopeStore, input: CreateDocumentInput, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
182
+ export function createDocument(
183
+ artifacts: ArtifactStore,
184
+ scopes: ArtifactScopeStore,
185
+ input: CreateDocumentInput,
186
+ authority: AuthorityRegistry,
187
+ context?: ArtifactEventContext,
188
+ ): Artifact {
156
189
  if (rejectsNoteTemplate(artifacts, input.templateId, input.subtype)) requireNotesFacade(authority, "docs");
157
190
  authority.requireArtifactAllowed("doc", input.subtype ?? templateSubtype(artifacts, input.templateId), "create", "docs");
158
191
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
159
- const document = artifacts.create({
160
- kind: "doc",
161
- // Explicit, not defaultStatusFor's "first status row by rowid" fallback -- the same
162
- // heuristic that made Task creation non-deterministic on a migrated database. Every
163
- // creation path that has no caller-supplied initial status must set one explicitly.
164
- status: "draft",
165
- title: input.title,
166
- body: input.body,
167
- subtype: input.subtype,
168
- labels: input.labels,
169
- extra: input.extra,
170
- templateId: input.templateId,
171
- }, context);
192
+ const document = artifacts.create(
193
+ {
194
+ kind: "doc",
195
+ // Explicit, not defaultStatusFor's "first status row by rowid" fallback -- the same
196
+ // heuristic that made Task creation non-deterministic on a migrated database. Every
197
+ // creation path that has no caller-supplied initial status must set one explicitly.
198
+ status: "draft",
199
+ title: input.title,
200
+ body: input.body,
201
+ subtype: input.subtype,
202
+ labels: input.labels,
203
+ extra: input.extra,
204
+ templateId: input.templateId,
205
+ },
206
+ context,
207
+ );
172
208
  scopes.assign(document.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
173
209
  return document;
174
210
  }
@@ -177,9 +213,18 @@ export function listDocuments(artifacts: ArtifactStore, scopes: ArtifactScopeSto
177
213
  return listScoped(artifacts, scopes, "doc", filter, NOTE_SUBTYPE);
178
214
  }
179
215
 
180
- export function assignDocumentProject(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string, projectRoot: string | undefined): Artifact {
216
+ export function assignDocumentProject(
217
+ artifacts: ArtifactStore,
218
+ scopes: ArtifactScopeStore,
219
+ id: string,
220
+ projectRoot: string | undefined,
221
+ ): Artifact {
181
222
  requireDocument(artifacts, id); // rejects Notes -- project reassignment for notes goes through notes.* like everything else about them
182
- scopes.assign(id, projectRoot === undefined ? undefined : normalizeProjectRoot(projectRoot), projectRoot === undefined ? "unscoped" : "explicit");
223
+ scopes.assign(
224
+ id,
225
+ projectRoot === undefined ? undefined : normalizeProjectRoot(projectRoot),
226
+ projectRoot === undefined ? "unscoped" : "explicit",
227
+ );
183
228
  return artifacts.get(id)!;
184
229
  }
185
230
 
@@ -194,7 +239,13 @@ export function showDocument(artifacts: ArtifactStore, id: string): Artifact {
194
239
  return artifacts.get(id, { tree: true })!;
195
240
  }
196
241
 
197
- export function transitionDocument(artifacts: ArtifactStore, id: string, action: DocumentTransition, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
242
+ export function transitionDocument(
243
+ artifacts: ArtifactStore,
244
+ id: string,
245
+ action: DocumentTransition,
246
+ authority: AuthorityRegistry,
247
+ context?: ArtifactEventContext,
248
+ ): Artifact {
198
249
  const document = requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "status"));
199
250
  const transition = DOCUMENT_TRANSITIONS[action];
200
251
  if (!transition.from.includes(document.status)) throw new Error(`cannot ${action} document from ${document.status}`);
@@ -207,18 +258,31 @@ export function transitionDocument(artifacts: ArtifactStore, id: string, action:
207
258
  * refuses, on purpose: rewriting it here would silently fork from whatever system actually
208
259
  * owns it (e.g. web-spider's ingested pages), with nothing to ever reconcile the two again.
209
260
  */
210
- export function updateDocument(artifacts: ArtifactStore, id: string, input: UpdateDocumentInput, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
261
+ export function updateDocument(
262
+ artifacts: ArtifactStore,
263
+ id: string,
264
+ input: UpdateDocumentInput,
265
+ authority: AuthorityRegistry,
266
+ context?: ArtifactEventContext,
267
+ ): Artifact {
211
268
  requireContentUpdateFields(input);
212
269
  assertTitleBounds(input.title);
213
270
  assertBodyBounds(input.body);
214
271
  assertLabelsBounds(input.labels);
215
- const document = requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "update"));
272
+ const _document = requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "update"));
216
273
  const updated = artifacts.updateContent(id, input, context);
217
274
  if (!updated) throw new Error(`document "${id}" not found`);
218
275
  return updated;
219
276
  }
220
277
 
221
- export function linkDocument(artifacts: ArtifactStore, id: string, relation: DocumentRelation, targetId: string, authority: AuthorityRegistry, context?: ArtifactEventContext): Artifact {
278
+ export function linkDocument(
279
+ artifacts: ArtifactStore,
280
+ id: string,
281
+ relation: DocumentRelation,
282
+ targetId: string,
283
+ authority: AuthorityRegistry,
284
+ context?: ArtifactEventContext,
285
+ ): Artifact {
222
286
  requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "link"));
223
287
  const target = artifacts.get(targetId);
224
288
  if (!target) throw new Error(`target artifact "${targetId}" not found`);
@@ -259,22 +323,30 @@ function assertRuleTextWithinBounds(condition: string | undefined, action: strin
259
323
  }
260
324
  }
261
325
 
262
- export function createRule(artifacts: ArtifactStore, scopes: ArtifactScopeStore, input: CreateRuleInput, context?: ArtifactEventContext): Artifact {
326
+ export function createRule(
327
+ artifacts: ArtifactStore,
328
+ scopes: ArtifactScopeStore,
329
+ input: CreateRuleInput,
330
+ context?: ArtifactEventContext,
331
+ ): Artifact {
263
332
  assertRuleTextWithinBounds(input.condition, input.action, input.body);
264
333
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
265
- const rule = artifacts.create({
266
- kind: "rule",
267
- status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
268
- title: input.title,
269
- body: input.body,
270
- labels: input.labels,
271
- extra: {
272
- ...(input.extra ?? {}),
273
- ...(input.condition ? { condition: input.condition } : {}),
274
- ...(input.action ? { action: input.action } : {}),
275
- severity: input.severity ?? "info",
334
+ const rule = artifacts.create(
335
+ {
336
+ kind: "rule",
337
+ status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
338
+ title: input.title,
339
+ body: input.body,
340
+ labels: input.labels,
341
+ extra: {
342
+ ...(input.extra ?? {}),
343
+ ...(input.condition ? { condition: input.condition } : {}),
344
+ ...(input.action ? { action: input.action } : {}),
345
+ severity: input.severity ?? "info",
346
+ },
276
347
  },
277
- }, context);
348
+ context,
349
+ );
278
350
  scopes.assign(rule.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
279
351
  return rule;
280
352
  }
@@ -283,19 +355,31 @@ export function listRules(artifacts: ArtifactStore, scopes: ArtifactScopeStore,
283
355
  return listScoped(artifacts, scopes, "rule", filter);
284
356
  }
285
357
 
286
- export function assignRuleProject(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string, projectRoot: string | undefined): Artifact {
358
+ export function assignRuleProject(
359
+ artifacts: ArtifactStore,
360
+ scopes: ArtifactScopeStore,
361
+ id: string,
362
+ projectRoot: string | undefined,
363
+ ): Artifact {
287
364
  return assignArtifactProject(artifacts, scopes, id, "rule", projectRoot);
288
365
  }
289
366
 
290
- /** Global rules always apply; scoped workflow rules apply only while their run owns active focus. */
367
+ /**
368
+ * Global rules always apply; scoped workflow-run rules apply only while their run owns active
369
+ * focus. Both a workflow-definition target's own run scope ("skill-run", written by
370
+ * workflow-execution.ts's runWorkflowSteps for that target kind) and a Playbook's own run scope
371
+ * ("playbook-run", same call for a Playbook target) are recognized -- confirmed live that only
372
+ * "skill-run" was ever checked here, silently breaking Playbook-run-scoped rule injection since
373
+ * Playbook gained its own doc/rule structured steps.
374
+ */
291
375
  export function listInjectableRules(artifacts: ArtifactStore, activeTaskId?: string): Artifact[] {
292
376
  return artifacts.query({ kind: "rule", status: "active" }).filter((rule) => {
293
- const scope = rule.extra["scope"];
377
+ const scope = rule.extra.scope;
294
378
  if (scope === undefined) return true;
295
379
  if (typeof scope !== "object" || scope === null || Array.isArray(scope)) return false;
296
380
  const value = scope as Record<string, unknown>;
297
- if (value["type"] !== "skill-run" || !Array.isArray(value["taskIds"])) return false;
298
- return activeTaskId !== undefined && value["taskIds"].some((id) => id === activeTaskId);
381
+ if ((value.type !== "skill-run" && value.type !== "playbook-run") || !Array.isArray(value.taskIds)) return false;
382
+ return activeTaskId !== undefined && value.taskIds.some((id) => id === activeTaskId);
299
383
  });
300
384
  }
301
385
 
@@ -306,8 +390,8 @@ export function showRule(artifacts: ArtifactStore, id: string): Artifact {
306
390
 
307
391
  export function previewRule(artifacts: ArtifactStore, id: string): string {
308
392
  const rule = requireKind(artifacts, id, "rule");
309
- const condition = typeof rule.extra["condition"] === "string" ? ` (when: ${rule.extra["condition"]})` : "";
310
- const action = rule.body || (typeof rule.extra["action"] === "string" ? rule.extra["action"] : "");
393
+ const condition = typeof rule.extra.condition === "string" ? ` (when: ${rule.extra.condition})` : "";
394
+ const action = rule.body || (typeof rule.extra.action === "string" ? rule.extra.action : "");
311
395
  return `• ${rule.title}${condition}\n ${action}`;
312
396
  }
313
397
 
@@ -328,8 +412,8 @@ export function updateRule(artifacts: ArtifactStore, id: string, input: UpdateRu
328
412
  assertLabelsBounds(input.labels);
329
413
  const rule = requireLocallyOwnedContent(requireKind(artifacts, id, "rule"));
330
414
  if (input.body !== undefined) {
331
- const condition = typeof rule.extra["condition"] === "string" ? rule.extra["condition"] : undefined;
332
- const action = typeof rule.extra["action"] === "string" ? rule.extra["action"] : undefined;
415
+ const condition = typeof rule.extra.condition === "string" ? rule.extra.condition : undefined;
416
+ const action = typeof rule.extra.action === "string" ? rule.extra.action : undefined;
333
417
  assertRuleTextWithinBounds(condition, action, input.body);
334
418
  }
335
419
  const updated = artifacts.updateContent(id, input, context);
@@ -344,191 +428,11 @@ export function gateTaskWithRule(artifacts: ArtifactStore, ruleId: string, taskI
344
428
  return showRule(artifacts, ruleId);
345
429
  }
346
430
 
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
- /**
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
431
  /**
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
432
+ * Playbooks: a trigger and an ordered list of steps -- authored as prose. But playbooks.invoke
433
+ * (playbook-execution.ts) recycles the shared blueprint materialization engine: it compiles a
434
+ * Playbook into a BlueprintDefinition and mechanically instantiates real Tasks from it.
435
+ * `playbookInvocation` below is the OTHER, older
532
436
  * path -- rendered text with no side effects, now exposed as the `preview` action for a human
533
437
  * who wants to just read a playbook before invoking it, not the primary way of running one.
534
438
  * Like Tasks, a Playbook can be nested or chained with another Playbook: `contains`/`part_of`
@@ -546,30 +450,147 @@ export interface PlaybookArgument {
546
450
  description?: string;
547
451
  /** Defaults true: naming an argument at all is a signal it matters, so an author must opt out explicitly to make one optional. */
548
452
  required: boolean;
453
+ /** 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. */
454
+ type: BlueprintInputType;
455
+ enum?: BlueprintArgumentValue[];
456
+ default?: BlueprintArgumentValue;
549
457
  }
550
458
 
551
459
  /** Rejects malformed input rather than silently dropping a bad entry -- the same posture creation validation already takes everywhere else. */
552
460
  function validatePlaybookArguments(value: unknown): PlaybookArgument[] | undefined {
553
461
  if (value === undefined) return undefined;
554
462
  if (!Array.isArray(value)) throw new Error("playbook arguments must be an array");
555
- if (value.length > PLAYBOOK_ARGUMENT_MAX_COUNT) throw new Error(`playbook arguments cannot exceed ${PLAYBOOK_ARGUMENT_MAX_COUNT} entries`);
463
+ if (value.length > PLAYBOOK_ARGUMENT_MAX_COUNT)
464
+ throw new Error(`playbook arguments cannot exceed ${PLAYBOOK_ARGUMENT_MAX_COUNT} entries`);
556
465
  const seen = new Set<string>();
557
466
  return value.map((entry, index) => {
558
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`argument at index ${index} must be an object`);
467
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
468
+ throw new Error(`argument at index ${index} must be an object`);
559
469
  const record = entry as Record<string, unknown>;
560
- const name = record["name"];
470
+ const name = record.name;
561
471
  if (typeof name !== "string" || name.trim().length === 0 || name.length > PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH) {
562
472
  throw new Error(`argument name must be between 1 and ${PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH} characters`);
563
473
  }
564
474
  if (seen.has(name)) throw new Error(`argument name "${name}" is declared more than once`);
565
475
  seen.add(name);
566
- const description = record["description"];
476
+ const description = record.description;
567
477
  if (description !== undefined && (typeof description !== "string" || description.length > PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH)) {
568
478
  throw new Error(`argument "${name}" description cannot exceed ${PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH} characters`);
569
479
  }
570
- const required = record["required"];
480
+ const required = record.required;
571
481
  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 };
482
+ const type = record.type === undefined ? "string" : record.type;
483
+ if (!BLUEPRINT_INPUT_TYPES.has(type as BlueprintInputType)) throw new Error(`argument "${name}" has unsupported type`);
484
+ const result: PlaybookArgument = { name, required: required !== false, type: type as BlueprintInputType };
485
+ if (description !== undefined) result.description = description as string;
486
+ if (record.default !== undefined) result.default = validateArgumentValue(name, result.type, record.default);
487
+ if (record.enum !== undefined) {
488
+ const values = record.enum;
489
+ if (!Array.isArray(values) || values.length === 0 || values.length > SKILL_MAX_ENUM_VALUES) {
490
+ throw new Error(`argument "${name}" enum must contain 1-${SKILL_MAX_ENUM_VALUES} values`);
491
+ }
492
+ result.enum = values.map((entry_) => validateArgumentValue(name, result.type, entry_));
493
+ if (result.default !== undefined && !result.enum.includes(result.default)) {
494
+ throw new Error(`argument "${name}" default must be one of its enum values`);
495
+ }
496
+ }
497
+ return result;
498
+ });
499
+ }
500
+
501
+ /** 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. */
502
+ export type PlaybookStep =
503
+ | string
504
+ | { kind: "task"; title?: string; body: string }
505
+ | { kind: "doc"; title: string; body?: string; subtype?: string; labels?: string[] }
506
+ | {
507
+ kind: "rule";
508
+ title: string;
509
+ body?: string;
510
+ condition?: string;
511
+ action?: string;
512
+ severity?: "block" | "warn" | "info";
513
+ labels?: string[];
514
+ }
515
+ | { kind: "call"; title: string; playbookId: string; arguments?: Record<string, unknown> };
516
+
517
+ function validateStructuredStep(value: Record<string, unknown>, index: number): PlaybookStep {
518
+ const kind = value.kind;
519
+ const title = value.title;
520
+ if (kind === "task") {
521
+ const body = value.body;
522
+ if (typeof body !== "string" || body.trim().length === 0) throw new Error(`step ${index} (task) requires a non-empty body`);
523
+ return { kind: "task", body, ...(typeof title === "string" && title.length > 0 ? { title } : {}) };
524
+ }
525
+ if (typeof title !== "string" || title.trim().length === 0 || title.length > ARTIFACT_TITLE_MAX_LENGTH) {
526
+ throw new Error(`step ${index} (${String(kind)}) requires a title between 1 and ${ARTIFACT_TITLE_MAX_LENGTH} characters`);
527
+ }
528
+ if (kind === "doc" || kind === "rule") {
529
+ const body = value.body;
530
+ if (body !== undefined && typeof body !== "string") throw new Error(`step ${index} (${kind}) body must be a string`);
531
+ const labels = value.labels;
532
+ if (labels !== undefined && (!Array.isArray(labels) || labels.some((label) => typeof label !== "string"))) {
533
+ throw new Error(`step ${index} (${kind}) labels must be a string array`);
534
+ }
535
+ if (kind === "doc") {
536
+ const subtype = value.subtype;
537
+ if (subtype !== undefined && typeof subtype !== "string") throw new Error(`step ${index} (doc) subtype must be a string`);
538
+ return {
539
+ kind: "doc",
540
+ title,
541
+ ...(body !== undefined ? { body: body as string } : {}),
542
+ ...(subtype !== undefined ? { subtype: subtype as string } : {}),
543
+ ...(labels !== undefined ? { labels: labels as string[] } : {}),
544
+ };
545
+ }
546
+ const condition = value.condition;
547
+ const action = value.action;
548
+ const severity = value.severity;
549
+ if (condition !== undefined && typeof condition !== "string") throw new Error(`step ${index} (rule) condition must be a string`);
550
+ if (action !== undefined && typeof action !== "string") throw new Error(`step ${index} (rule) action must be a string`);
551
+ if (severity !== undefined && severity !== "block" && severity !== "warn" && severity !== "info") {
552
+ throw new Error(`step ${index} (rule) severity must be block, warn, or info`);
553
+ }
554
+ return {
555
+ kind: "rule",
556
+ title,
557
+ ...(body !== undefined ? { body: body as string } : {}),
558
+ ...(condition !== undefined ? { condition: condition as string } : {}),
559
+ ...(action !== undefined ? { action: action as string } : {}),
560
+ ...(severity !== undefined ? { severity: severity as "block" | "warn" | "info" } : {}),
561
+ ...(labels !== undefined ? { labels: labels as string[] } : {}),
562
+ };
563
+ }
564
+ if (kind === "call") {
565
+ const playbookId = value.playbookId;
566
+ if (typeof playbookId !== "string" || playbookId.trim().length === 0) throw new Error(`step ${index} (call) requires a playbookId`);
567
+ const callArguments = value.arguments;
568
+ if (callArguments !== undefined && (typeof callArguments !== "object" || callArguments === null || Array.isArray(callArguments))) {
569
+ throw new Error(`step ${index} (call) arguments must be an object`);
570
+ }
571
+ return {
572
+ kind: "call",
573
+ title,
574
+ playbookId,
575
+ ...(callArguments !== undefined ? { arguments: callArguments as Record<string, unknown> } : {}),
576
+ };
577
+ }
578
+ throw new Error(`step ${index} has unknown kind "${String(kind)}"`);
579
+ }
580
+
581
+ /** 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. */
582
+ function validatePlaybookSteps(value: unknown): PlaybookStep[] | undefined {
583
+ if (value === undefined) return undefined;
584
+ if (!Array.isArray(value)) throw new Error("playbook steps must be an array");
585
+ if (value.length > PLAYBOOK_MAX_STEPS) throw new Error(`playbook steps cannot exceed ${PLAYBOOK_MAX_STEPS} entries`);
586
+ return value.map((entry, index) => {
587
+ if (typeof entry === "string") {
588
+ if (entry.trim().length === 0) throw new Error(`step ${index} must not be empty`);
589
+ return entry;
590
+ }
591
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
592
+ throw new Error(`step ${index} must be a string or a structured step object`);
593
+ return validateStructuredStep(entry as Record<string, unknown>, index);
573
594
  });
574
595
  }
575
596
 
@@ -577,7 +598,7 @@ export interface CreatePlaybookInput {
577
598
  title: string;
578
599
  body?: string;
579
600
  trigger?: string;
580
- steps?: string[];
601
+ steps?: unknown;
581
602
  tools?: string[];
582
603
  /** Declares named arguments this Playbook needs -- see playbookInvocation for how a missing required one surfaces. */
583
604
  arguments?: unknown;
@@ -589,23 +610,32 @@ export interface CreatePlaybookInput {
589
610
  export type PlaybookTransition = "enable" | "disable";
590
611
  export type UpdatePlaybookInput = UpdateContentInput;
591
612
 
592
- export function createPlaybook(artifacts: ArtifactStore, scopes: ArtifactScopeStore, input: CreatePlaybookInput, context?: ArtifactEventContext): Artifact {
613
+ export function createPlaybook(
614
+ artifacts: ArtifactStore,
615
+ scopes: ArtifactScopeStore,
616
+ input: CreatePlaybookInput,
617
+ context?: ArtifactEventContext,
618
+ ): Artifact {
593
619
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
594
620
  const declaredArguments = validatePlaybookArguments(input.arguments);
595
- const playbook = artifacts.create({
596
- kind: "playbook",
597
- status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
598
- title: input.title,
599
- body: input.body,
600
- labels: input.labels,
601
- extra: {
602
- ...(input.extra ?? {}),
603
- ...(input.trigger ? { trigger: input.trigger } : {}),
604
- ...(input.steps ? { steps: input.steps } : {}),
605
- ...(input.tools ? { tools: input.tools } : {}),
606
- ...(declaredArguments ? { arguments: declaredArguments } : {}),
621
+ const declaredSteps = validatePlaybookSteps(input.steps);
622
+ const playbook = artifacts.create(
623
+ {
624
+ kind: "playbook",
625
+ status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
626
+ title: input.title,
627
+ body: input.body,
628
+ labels: input.labels,
629
+ extra: {
630
+ ...(input.extra ?? {}),
631
+ ...(input.trigger ? { trigger: input.trigger } : {}),
632
+ ...(declaredSteps ? { steps: declaredSteps } : {}),
633
+ ...(input.tools ? { tools: input.tools } : {}),
634
+ ...(declaredArguments ? { arguments: declaredArguments } : {}),
635
+ },
607
636
  },
608
- }, context);
637
+ context,
638
+ );
609
639
  scopes.assign(playbook.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
610
640
  return playbook;
611
641
  }
@@ -614,7 +644,12 @@ export function listPlaybooks(artifacts: ArtifactStore, scopes: ArtifactScopeSto
614
644
  return listScoped(artifacts, scopes, "playbook", filter);
615
645
  }
616
646
 
617
- export function assignPlaybookProject(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string, projectRoot: string | undefined): Artifact {
647
+ export function assignPlaybookProject(
648
+ artifacts: ArtifactStore,
649
+ scopes: ArtifactScopeStore,
650
+ id: string,
651
+ projectRoot: string | undefined,
652
+ ): Artifact {
618
653
  return assignArtifactProject(artifacts, scopes, id, "playbook", projectRoot);
619
654
  }
620
655
 
@@ -634,7 +669,12 @@ export function updatePlaybook(artifacts: ArtifactStore, id: string, input: Upda
634
669
  return updated;
635
670
  }
636
671
 
637
- export function transitionPlaybook(artifacts: ArtifactStore, id: string, action: PlaybookTransition, context?: ArtifactEventContext): Artifact {
672
+ export function transitionPlaybook(
673
+ artifacts: ArtifactStore,
674
+ id: string,
675
+ action: PlaybookTransition,
676
+ context?: ArtifactEventContext,
677
+ ): Artifact {
638
678
  const playbook = requireLocallyOwnedContent(requireKind(artifacts, id, "playbook"));
639
679
  const expected = action === "enable" ? "deprecated" : "active";
640
680
  const target = action === "enable" ? "active" : "deprecated";
@@ -678,28 +718,47 @@ export function undependPlaybook(artifacts: ArtifactStore, id: string, dependenc
678
718
  return showPlaybook(artifacts, id);
679
719
  }
680
720
 
721
+ /** 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. */
722
+ function stepText(step: PlaybookStep, index: number): string {
723
+ if (typeof step === "string") return `${index + 1}. ${step}`;
724
+ if (step.kind === "task") return `${index + 1}. ${step.title ? `${step.title} -- ` : ""}${step.body}`;
725
+ if (step.kind === "doc") return `${index + 1}. [creates Doc] "${step.title}"${step.subtype ? ` (${step.subtype})` : ""}`;
726
+ if (step.kind === "rule") return `${index + 1}. [creates Rule] "${step.title}"${step.condition ? ` -- when: ${step.condition}` : ""}`;
727
+ return `${index + 1}. [calls playbook] "${step.title}" -> ${step.playbookId}`;
728
+ }
729
+
730
+ function argumentQualifier(argument: PlaybookArgument): string {
731
+ const qualifier = argument.required ? "required" : "optional";
732
+ const type = argument.type === "string" ? "" : `, ${argument.type}`;
733
+ const options = argument.enum ? `, one of: ${argument.enum.join(", ")}` : "";
734
+ return `${qualifier}${type}${options}`;
735
+ }
736
+
681
737
  /** 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 {
683
- 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") : [];
685
- const tools = Array.isArray(playbook.extra["tools"]) ? playbook.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
686
- const declaredArguments = Array.isArray(playbook.extra["arguments"]) ? (playbook.extra["arguments"] as PlaybookArgument[]) : [];
738
+ function playbookInvocationBody(playbook: Artifact, provided: Record<string, unknown>): string {
739
+ const trigger = typeof playbook.extra.trigger === "string" ? playbook.extra.trigger : "manual invocation";
740
+ const steps = Array.isArray(playbook.extra.steps) ? (playbook.extra.steps as PlaybookStep[]) : [];
741
+ const tools = Array.isArray(playbook.extra.tools) ? playbook.extra.tools.filter((tool): tool is string => typeof tool === "string") : [];
742
+ const declaredArguments = Array.isArray(playbook.extra.arguments) ? (playbook.extra.arguments as PlaybookArgument[]) : [];
687
743
  const argumentLines = declaredArguments.map((argument) => {
688
744
  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`;
745
+ if (value !== undefined) return `- ${argument.name}: ${String(value)}`;
746
+ return `- ${argument.name} (${argumentQualifier(argument)}${argument.description ? `: ${argument.description}` : ""}) -- not yet provided`;
692
747
  });
693
- const missingRequired = declaredArguments.filter((argument) => argument.required && provided[argument.name] === undefined);
748
+ const missingRequired = declaredArguments.filter(
749
+ (argument) => argument.required && provided[argument.name] === undefined && argument.default === undefined,
750
+ );
694
751
  return [
695
752
  `Apply Papyrus playbook "${playbook.title}".`,
696
753
  `Trigger: ${trigger}`,
697
754
  ...(playbook.body ? [`Context: ${playbook.body}`] : []),
698
755
  ...(argumentLines.length > 0 ? ["Arguments:", ...argumentLines] : []),
699
756
  ...(missingRequired.length > 0
700
- ? [`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.`]
757
+ ? [
758
+ `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.`,
759
+ ]
701
760
  : []),
702
- ...(steps.length ? ["Steps:", ...steps.map((step, index) => `${index + 1}. ${step}`)] : []),
761
+ ...(steps.length ? ["Steps:", ...steps.map((step, index) => stepText(step, index))] : []),
703
762
  ...(tools.length ? [`Tools: ${tools.join(", ")}`] : []),
704
763
  ].join("\n");
705
764
  }
@@ -711,18 +770,27 @@ function playbookInvocationBody(playbook: Artifact, provided: Record<string, str
711
770
  * "run as part of this one". `depends_on` chains a prerequisite -- its full steps render BEFORE
712
771
  * this playbook's own, as "complete this first". Every other relation (references, relates_to,
713
772
  * 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.
773
+ * cycle-safe -- a composition cycle degrades to a marker instead of infinite-looping, the same
774
+ * cycle-safety discipline task dependency graphs already established.
716
775
  * `provided` is the caller's already-known argument values (e.g. from the conversation so far);
717
776
  * any declared *required* argument missing from it is called out explicitly, directing the agent
718
777
  * to discuss (live:true) rather than guess or silently proceed. `visited` and `depth` are
719
778
  * recursion-internal; callers should not pass them.
720
779
  */
721
- export function playbookInvocation(artifacts: ArtifactStore, id: string, provided: Record<string, string> = {}, visited: Set<string> = new Set(), depth = 0): string {
780
+ export function playbookInvocation(
781
+ artifacts: ArtifactStore,
782
+ id: string,
783
+ provided: Record<string, unknown> = {},
784
+ visited: Set<string> = new Set(),
785
+ depth = 0,
786
+ ): string {
722
787
  const playbook = requireKind(artifacts, id, "playbook");
723
788
  visited.add(id);
724
789
 
725
- const edges = artifacts.relationships({ artifactIds: [id] }).filter((edge) => edge.from === id).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
790
+ const edges = artifacts
791
+ .relationships({ artifactIds: [id] })
792
+ .filter((edge) => edge.from === id)
793
+ .slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
726
794
  const linkedArtifactLines: string[] = [];
727
795
  const nestedSections: string[] = []; // contains -- rendered after this playbook's own body
728
796
  const prerequisiteSections: string[] = []; // depends_on -- rendered before this playbook's own body
@@ -737,14 +805,20 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
737
805
  const bucket = edge.relation === "contains" ? nestedSections : prerequisiteSections;
738
806
  const role = edge.relation === "contains" ? "nested" : "prerequisite";
739
807
  if (visited.has(target.id)) {
740
- bucket.push(`Also linked via ${edge.relation} to ${role} playbook "${target.title}" -- already invoked above in this chain, not repeated.`);
808
+ bucket.push(
809
+ `Also linked via ${edge.relation} to ${role} playbook "${target.title}" -- already invoked above in this chain, not repeated.`,
810
+ );
741
811
  } else if (depth + 1 > PLAYBOOK_INVOCATION_MAX_CALL_DEPTH) {
742
- bucket.push(`Also linked via ${edge.relation} to ${role} playbook "${target.title}" -- call depth limit reached, invoke it separately.`);
812
+ bucket.push(
813
+ `Also linked via ${edge.relation} to ${role} playbook "${target.title}" -- call depth limit reached, invoke it separately.`,
814
+ );
743
815
  } else {
744
816
  const nested = playbookInvocation(artifacts, target.id, provided, visited, depth + 1);
745
- bucket.push(edge.relation === "contains"
746
- ? `Nested playbook (contains) "${target.title}" -- run as part of this one:\n${nested}`
747
- : `Prerequisite playbook (depends_on) "${target.title}" -- complete this FIRST, before the steps below:\n${nested}`);
817
+ bucket.push(
818
+ edge.relation === "contains"
819
+ ? `Nested playbook (contains) "${target.title}" -- run as part of this one:\n${nested}`
820
+ : `Prerequisite playbook (depends_on) "${target.title}" -- complete this FIRST, before the steps below:\n${nested}`,
821
+ );
748
822
  }
749
823
  }
750
824