@danypops/papyrus 0.42.0 → 0.42.2

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 (73) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  3. package/src/adapters/sqlite-artifact-store.ts +7 -5
  4. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  5. package/src/adapters/sqlite-gate-runner.ts +1 -1
  6. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  7. package/src/adapters/sqlite-log-store.ts +36 -17
  8. package/src/adapters/sqlite-note-event-store.ts +20 -16
  9. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  10. package/src/adapters/sqlite-task-event-store.ts +29 -21
  11. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  12. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  13. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  14. package/src/artifact-relationship-view.ts +3 -3
  15. package/src/artifact-subtree.ts +4 -2
  16. package/src/authority-registry.ts +2 -1
  17. package/src/cli.ts +785 -179
  18. package/src/client.ts +46 -20
  19. package/src/constants.ts +15 -4
  20. package/src/daemon-state.ts +4 -12
  21. package/src/daemon.ts +31 -9
  22. package/src/db.ts +119 -98
  23. package/src/discussion-service.ts +109 -44
  24. package/src/domain/artifact-event.ts +17 -4
  25. package/src/domain/artifact.ts +3 -1
  26. package/src/domain/blueprint-definition.ts +26 -31
  27. package/src/domain/checklist.ts +20 -17
  28. package/src/domain/discussion.ts +37 -18
  29. package/src/domain/gate.ts +7 -7
  30. package/src/domain/log-entry.ts +1 -1
  31. package/src/domain/note-event.ts +20 -7
  32. package/src/domain/task-event.ts +17 -7
  33. package/src/domain-services.ts +241 -110
  34. package/src/graph-projection-service.ts +34 -8
  35. package/src/id-migration.ts +17 -4
  36. package/src/index.ts +16 -11
  37. package/src/log-service.ts +6 -5
  38. package/src/log.ts +19 -0
  39. package/src/modules/discuss.ts +63 -28
  40. package/src/modules/docs.ts +74 -17
  41. package/src/modules/graph-projection.ts +20 -9
  42. package/src/modules/logs.ts +33 -21
  43. package/src/modules/notes.ts +66 -28
  44. package/src/modules/playbooks.ts +88 -29
  45. package/src/modules/rules.ts +57 -15
  46. package/src/modules/session-identity.ts +6 -2
  47. package/src/modules/tasks.ts +142 -67
  48. package/src/note-service.ts +11 -7
  49. package/src/ops.ts +131 -68
  50. package/src/playbook-definition.ts +56 -17
  51. package/src/playbook-execution.ts +13 -3
  52. package/src/ports/note-event-store.ts +6 -4
  53. package/src/ports/task-event-store.ts +9 -6
  54. package/src/ports/task-focus-store.ts +17 -4
  55. package/src/ports/task-lease-store.ts +9 -4
  56. package/src/ports/task-scope-store.ts +3 -1
  57. package/src/service.ts +143 -89
  58. package/src/session-identity-service.ts +10 -2
  59. package/src/task-context.ts +28 -16
  60. package/src/task-execution.ts +4 -12
  61. package/src/task-graph-view.ts +12 -12
  62. package/src/task-relationship-view.ts +1 -3
  63. package/src/task-service.ts +167 -72
  64. package/src/vehicle/artifact-trash-vehicle.ts +25 -13
  65. package/src/vehicle/artifact-vehicle-shared.ts +28 -7
  66. package/src/vehicle/docs-vehicle.ts +48 -16
  67. package/src/vehicle/notes-vehicle.ts +24 -6
  68. package/src/vehicle/papyrus-vehicle.ts +14 -3
  69. package/src/vehicle/playbooks-vehicle.ts +87 -18
  70. package/src/vehicle/rules-vehicle.ts +58 -21
  71. package/src/vehicle/tasks-vehicle.ts +366 -54
  72. package/src/version.ts +1 -1
  73. package/src/workflow-execution.ts +71 -52
@@ -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,
@@ -13,19 +14,18 @@ import {
13
14
  RULE_TEXT_HARD_LIMIT_CHARACTERS,
14
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";
18
- import { normalizeProjectRoot } from "./domain/task-scope.ts";
19
19
  import {
20
20
  BLUEPRINT_INPUT_TYPES,
21
- validateArgumentValue,
22
21
  type BlueprintArgumentValue,
23
22
  type BlueprintInputType,
23
+ validateArgumentValue,
24
24
  } from "./domain/blueprint-definition.ts";
25
- import type { ArtifactStore } from "./ports/artifact-store.ts";
26
- import type { ArtifactScopeStore } from "./ports/artifact-scope-store.ts";
25
+ import { normalizeProjectRoot } from "./domain/task-scope.ts";
27
26
  import { NOTE_SUBTYPE } from "./note-service.ts";
28
- 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";
29
29
 
30
30
  export interface UpdateContentInput {
31
31
  title?: string;
@@ -46,7 +46,8 @@ function assertTitleBounds(title: string | undefined): void {
46
46
  }
47
47
 
48
48
  function assertBodyBounds(body: string | undefined): void {
49
- 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`);
50
51
  }
51
52
 
52
53
  function assertLabelsBounds(labels: string[] | undefined): void {
@@ -72,8 +73,15 @@ export interface ListFilter {
72
73
  * path unchanged, so every caller that predates project scoping keeps working exactly as
73
74
  * before.
74
75
  */
75
- function listScoped(artifacts: ArtifactStore, scopes: ArtifactScopeStore, kind: string, filter: ListFilter, excludeSubtype?: string): Artifact[] {
76
- 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 });
77
85
  const limit = filter.limit ?? ARTIFACT_SCOPE_MAX_ARTIFACTS;
78
86
  if (!Number.isInteger(limit) || limit < 1 || limit > ARTIFACT_SCOPE_MAX_ARTIFACTS) {
79
87
  throw new Error(`list limit must be between 1 and ${ARTIFACT_SCOPE_MAX_ARTIFACTS}`);
@@ -91,9 +99,19 @@ function listScoped(artifacts: ArtifactStore, scopes: ArtifactScopeStore, kind:
91
99
  }
92
100
 
93
101
  /** Shared by assignDocumentProject/assignRuleProject/assignPlaybookProject. */
94
- function assignArtifactProject(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string, kind: string, projectRoot: string | undefined): Artifact {
102
+ function assignArtifactProject(
103
+ artifacts: ArtifactStore,
104
+ scopes: ArtifactScopeStore,
105
+ id: string,
106
+ kind: string,
107
+ projectRoot: string | undefined,
108
+ ): Artifact {
95
109
  requireKind(artifacts, id, kind);
96
- 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
+ );
97
115
  return artifacts.get(id)!;
98
116
  }
99
117
 
@@ -108,9 +126,13 @@ function rejectsNoteTemplate(artifacts: ArtifactStore, templateId: string | unde
108
126
  if (subtype === NOTE_SUBTYPE) return true;
109
127
  if (!templateId) return false;
110
128
  const template = artifacts.get(templateId);
111
- const defaults = template?.extra["defaults"];
112
- return typeof defaults === "object" && defaults !== null && !Array.isArray(defaults)
113
- && (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
+ );
114
136
  }
115
137
 
116
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. */
@@ -121,9 +143,9 @@ function requireNotesFacade(authority: AuthorityRegistry, caller: string): never
121
143
 
122
144
  function templateSubtype(artifacts: ArtifactStore, templateId: string | undefined): string | undefined {
123
145
  if (!templateId) return undefined;
124
- const defaults = artifacts.get(templateId)?.extra["defaults"];
146
+ const defaults = artifacts.get(templateId)?.extra.defaults;
125
147
  if (typeof defaults !== "object" || defaults === null || Array.isArray(defaults)) return undefined;
126
- const subtype = (defaults as Record<string, unknown>)["subtype"];
148
+ const subtype = (defaults as Record<string, unknown>).subtype;
127
149
  return typeof subtype === "string" ? subtype : undefined;
128
150
  }
129
151
 
@@ -157,23 +179,32 @@ const DOCUMENT_TRANSITIONS: Record<DocumentTransition, { from: string[]; to: str
157
179
  reopen: { from: ["archived"], to: "draft" },
158
180
  };
159
181
 
160
- 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 {
161
189
  if (rejectsNoteTemplate(artifacts, input.templateId, input.subtype)) requireNotesFacade(authority, "docs");
162
190
  authority.requireArtifactAllowed("doc", input.subtype ?? templateSubtype(artifacts, input.templateId), "create", "docs");
163
191
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
164
- const document = artifacts.create({
165
- kind: "doc",
166
- // Explicit, not defaultStatusFor's "first status row by rowid" fallback -- the same
167
- // heuristic that made Task creation non-deterministic on a migrated database. Every
168
- // creation path that has no caller-supplied initial status must set one explicitly.
169
- status: "draft",
170
- title: input.title,
171
- body: input.body,
172
- subtype: input.subtype,
173
- labels: input.labels,
174
- extra: input.extra,
175
- templateId: input.templateId,
176
- }, 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
+ );
177
208
  scopes.assign(document.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
178
209
  return document;
179
210
  }
@@ -182,9 +213,18 @@ export function listDocuments(artifacts: ArtifactStore, scopes: ArtifactScopeSto
182
213
  return listScoped(artifacts, scopes, "doc", filter, NOTE_SUBTYPE);
183
214
  }
184
215
 
185
- 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 {
186
222
  requireDocument(artifacts, id); // rejects Notes -- project reassignment for notes goes through notes.* like everything else about them
187
- 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
+ );
188
228
  return artifacts.get(id)!;
189
229
  }
190
230
 
@@ -199,7 +239,13 @@ export function showDocument(artifacts: ArtifactStore, id: string): Artifact {
199
239
  return artifacts.get(id, { tree: true })!;
200
240
  }
201
241
 
202
- 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 {
203
249
  const document = requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "status"));
204
250
  const transition = DOCUMENT_TRANSITIONS[action];
205
251
  if (!transition.from.includes(document.status)) throw new Error(`cannot ${action} document from ${document.status}`);
@@ -212,18 +258,31 @@ export function transitionDocument(artifacts: ArtifactStore, id: string, action:
212
258
  * refuses, on purpose: rewriting it here would silently fork from whatever system actually
213
259
  * owns it (e.g. web-spider's ingested pages), with nothing to ever reconcile the two again.
214
260
  */
215
- 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 {
216
268
  requireContentUpdateFields(input);
217
269
  assertTitleBounds(input.title);
218
270
  assertBodyBounds(input.body);
219
271
  assertLabelsBounds(input.labels);
220
- const document = requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "update"));
272
+ const _document = requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "update"));
221
273
  const updated = artifacts.updateContent(id, input, context);
222
274
  if (!updated) throw new Error(`document "${id}" not found`);
223
275
  return updated;
224
276
  }
225
277
 
226
- 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 {
227
286
  requireLocallyOwnedContent(requireMutableDocument(requireDocument(artifacts, id), authority, "link"));
228
287
  const target = artifacts.get(targetId);
229
288
  if (!target) throw new Error(`target artifact "${targetId}" not found`);
@@ -264,22 +323,30 @@ function assertRuleTextWithinBounds(condition: string | undefined, action: strin
264
323
  }
265
324
  }
266
325
 
267
- 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 {
268
332
  assertRuleTextWithinBounds(input.condition, input.action, input.body);
269
333
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
270
- const rule = artifacts.create({
271
- kind: "rule",
272
- status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
273
- title: input.title,
274
- body: input.body,
275
- labels: input.labels,
276
- extra: {
277
- ...(input.extra ?? {}),
278
- ...(input.condition ? { condition: input.condition } : {}),
279
- ...(input.action ? { action: input.action } : {}),
280
- 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
+ },
281
347
  },
282
- }, context);
348
+ context,
349
+ );
283
350
  scopes.assign(rule.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
284
351
  return rule;
285
352
  }
@@ -288,7 +355,12 @@ export function listRules(artifacts: ArtifactStore, scopes: ArtifactScopeStore,
288
355
  return listScoped(artifacts, scopes, "rule", filter);
289
356
  }
290
357
 
291
- 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 {
292
364
  return assignArtifactProject(artifacts, scopes, id, "rule", projectRoot);
293
365
  }
294
366
 
@@ -302,12 +374,12 @@ export function assignRuleProject(artifacts: ArtifactStore, scopes: ArtifactScop
302
374
  */
303
375
  export function listInjectableRules(artifacts: ArtifactStore, activeTaskId?: string): Artifact[] {
304
376
  return artifacts.query({ kind: "rule", status: "active" }).filter((rule) => {
305
- const scope = rule.extra["scope"];
377
+ const scope = rule.extra.scope;
306
378
  if (scope === undefined) return true;
307
379
  if (typeof scope !== "object" || scope === null || Array.isArray(scope)) return false;
308
380
  const value = scope as Record<string, unknown>;
309
- if ((value["type"] !== "skill-run" && value["type"] !== "playbook-run") || !Array.isArray(value["taskIds"])) return false;
310
- 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);
311
383
  });
312
384
  }
313
385
 
@@ -318,8 +390,8 @@ export function showRule(artifacts: ArtifactStore, id: string): Artifact {
318
390
 
319
391
  export function previewRule(artifacts: ArtifactStore, id: string): string {
320
392
  const rule = requireKind(artifacts, id, "rule");
321
- const condition = typeof rule.extra["condition"] === "string" ? ` (when: ${rule.extra["condition"]})` : "";
322
- 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 : "");
323
395
  return `• ${rule.title}${condition}\n ${action}`;
324
396
  }
325
397
 
@@ -340,8 +412,8 @@ export function updateRule(artifacts: ArtifactStore, id: string, input: UpdateRu
340
412
  assertLabelsBounds(input.labels);
341
413
  const rule = requireLocallyOwnedContent(requireKind(artifacts, id, "rule"));
342
414
  if (input.body !== undefined) {
343
- const condition = typeof rule.extra["condition"] === "string" ? rule.extra["condition"] : undefined;
344
- 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;
345
417
  assertRuleTextWithinBounds(condition, action, input.body);
346
418
  }
347
419
  const updated = artifacts.updateContent(id, input, context);
@@ -388,30 +460,32 @@ export interface PlaybookArgument {
388
460
  function validatePlaybookArguments(value: unknown): PlaybookArgument[] | undefined {
389
461
  if (value === undefined) return undefined;
390
462
  if (!Array.isArray(value)) throw new Error("playbook arguments must be an array");
391
- 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`);
392
465
  const seen = new Set<string>();
393
466
  return value.map((entry, index) => {
394
- 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`);
395
469
  const record = entry as Record<string, unknown>;
396
- const name = record["name"];
470
+ const name = record.name;
397
471
  if (typeof name !== "string" || name.trim().length === 0 || name.length > PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH) {
398
472
  throw new Error(`argument name must be between 1 and ${PLAYBOOK_ARGUMENT_NAME_MAX_LENGTH} characters`);
399
473
  }
400
474
  if (seen.has(name)) throw new Error(`argument name "${name}" is declared more than once`);
401
475
  seen.add(name);
402
- const description = record["description"];
476
+ const description = record.description;
403
477
  if (description !== undefined && (typeof description !== "string" || description.length > PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH)) {
404
478
  throw new Error(`argument "${name}" description cannot exceed ${PLAYBOOK_ARGUMENT_DESCRIPTION_MAX_LENGTH} characters`);
405
479
  }
406
- const required = record["required"];
480
+ const required = record.required;
407
481
  if (required !== undefined && typeof required !== "boolean") throw new Error(`argument "${name}" required must be a boolean`);
408
- const type = record["type"] === undefined ? "string" : record["type"];
482
+ const type = record.type === undefined ? "string" : record.type;
409
483
  if (!BLUEPRINT_INPUT_TYPES.has(type as BlueprintInputType)) throw new Error(`argument "${name}" has unsupported type`);
410
484
  const result: PlaybookArgument = { name, required: required !== false, type: type as BlueprintInputType };
411
485
  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"];
486
+ if (record.default !== undefined) result.default = validateArgumentValue(name, result.type, record.default);
487
+ if (record.enum !== undefined) {
488
+ const values = record.enum;
415
489
  if (!Array.isArray(values) || values.length === 0 || values.length > SKILL_MAX_ENUM_VALUES) {
416
490
  throw new Error(`argument "${name}" enum must contain 1-${SKILL_MAX_ENUM_VALUES} values`);
417
491
  }
@@ -429,14 +503,22 @@ export type PlaybookStep =
429
503
  | string
430
504
  | { kind: "task"; title?: string; body: string }
431
505
  | { 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[] }
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
+ }
433
515
  | { kind: "call"; title: string; playbookId: string; arguments?: Record<string, unknown> };
434
516
 
435
517
  function validateStructuredStep(value: Record<string, unknown>, index: number): PlaybookStep {
436
- const kind = value["kind"];
437
- const title = value["title"];
518
+ const kind = value.kind;
519
+ const title = value.title;
438
520
  if (kind === "task") {
439
- const body = value["body"];
521
+ const body = value.body;
440
522
  if (typeof body !== "string" || body.trim().length === 0) throw new Error(`step ${index} (task) requires a non-empty body`);
441
523
  return { kind: "task", body, ...(typeof title === "string" && title.length > 0 ? { title } : {}) };
442
524
  }
@@ -444,20 +526,26 @@ function validateStructuredStep(value: Record<string, unknown>, index: number):
444
526
  throw new Error(`step ${index} (${String(kind)}) requires a title between 1 and ${ARTIFACT_TITLE_MAX_LENGTH} characters`);
445
527
  }
446
528
  if (kind === "doc" || kind === "rule") {
447
- const body = value["body"];
529
+ const body = value.body;
448
530
  if (body !== undefined && typeof body !== "string") throw new Error(`step ${index} (${kind}) body must be a string`);
449
- const labels = value["labels"];
531
+ const labels = value.labels;
450
532
  if (labels !== undefined && (!Array.isArray(labels) || labels.some((label) => typeof label !== "string"))) {
451
533
  throw new Error(`step ${index} (${kind}) labels must be a string array`);
452
534
  }
453
535
  if (kind === "doc") {
454
- const subtype = value["subtype"];
536
+ const subtype = value.subtype;
455
537
  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[] } : {}) };
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
+ };
457
545
  }
458
- const condition = value["condition"];
459
- const action = value["action"];
460
- const severity = value["severity"];
546
+ const condition = value.condition;
547
+ const action = value.action;
548
+ const severity = value.severity;
461
549
  if (condition !== undefined && typeof condition !== "string") throw new Error(`step ${index} (rule) condition must be a string`);
462
550
  if (action !== undefined && typeof action !== "string") throw new Error(`step ${index} (rule) action must be a string`);
463
551
  if (severity !== undefined && severity !== "block" && severity !== "warn" && severity !== "info") {
@@ -474,13 +562,18 @@ function validateStructuredStep(value: Record<string, unknown>, index: number):
474
562
  };
475
563
  }
476
564
  if (kind === "call") {
477
- const playbookId = value["playbookId"];
565
+ const playbookId = value.playbookId;
478
566
  if (typeof playbookId !== "string" || playbookId.trim().length === 0) throw new Error(`step ${index} (call) requires a playbookId`);
479
- const callArguments = value["arguments"];
567
+ const callArguments = value.arguments;
480
568
  if (callArguments !== undefined && (typeof callArguments !== "object" || callArguments === null || Array.isArray(callArguments))) {
481
569
  throw new Error(`step ${index} (call) arguments must be an object`);
482
570
  }
483
- return { kind: "call", title, playbookId, ...(callArguments !== undefined ? { arguments: callArguments as Record<string, unknown> } : {}) };
571
+ return {
572
+ kind: "call",
573
+ title,
574
+ playbookId,
575
+ ...(callArguments !== undefined ? { arguments: callArguments as Record<string, unknown> } : {}),
576
+ };
484
577
  }
485
578
  throw new Error(`step ${index} has unknown kind "${String(kind)}"`);
486
579
  }
@@ -495,7 +588,8 @@ function validatePlaybookSteps(value: unknown): PlaybookStep[] | undefined {
495
588
  if (entry.trim().length === 0) throw new Error(`step ${index} must not be empty`);
496
589
  return entry;
497
590
  }
498
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`step ${index} must be a string or a structured step object`);
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`);
499
593
  return validateStructuredStep(entry as Record<string, unknown>, index);
500
594
  });
501
595
  }
@@ -516,24 +610,32 @@ export interface CreatePlaybookInput {
516
610
  export type PlaybookTransition = "enable" | "disable";
517
611
  export type UpdatePlaybookInput = UpdateContentInput;
518
612
 
519
- 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 {
520
619
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
521
620
  const declaredArguments = validatePlaybookArguments(input.arguments);
522
621
  const declaredSteps = validatePlaybookSteps(input.steps);
523
- const playbook = artifacts.create({
524
- kind: "playbook",
525
- status: "active", // explicit; see createDocument for why defaultStatusFor is not trusted here
526
- title: input.title,
527
- body: input.body,
528
- labels: input.labels,
529
- extra: {
530
- ...(input.extra ?? {}),
531
- ...(input.trigger ? { trigger: input.trigger } : {}),
532
- ...(declaredSteps ? { steps: declaredSteps } : {}),
533
- ...(input.tools ? { tools: input.tools } : {}),
534
- ...(declaredArguments ? { arguments: declaredArguments } : {}),
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
+ },
535
636
  },
536
- }, context);
637
+ context,
638
+ );
537
639
  scopes.assign(playbook.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
538
640
  return playbook;
539
641
  }
@@ -542,7 +644,12 @@ export function listPlaybooks(artifacts: ArtifactStore, scopes: ArtifactScopeSto
542
644
  return listScoped(artifacts, scopes, "playbook", filter);
543
645
  }
544
646
 
545
- 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 {
546
653
  return assignArtifactProject(artifacts, scopes, id, "playbook", projectRoot);
547
654
  }
548
655
 
@@ -562,7 +669,12 @@ export function updatePlaybook(artifacts: ArtifactStore, id: string, input: Upda
562
669
  return updated;
563
670
  }
564
671
 
565
- 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 {
566
678
  const playbook = requireLocallyOwnedContent(requireKind(artifacts, id, "playbook"));
567
679
  const expected = action === "enable" ? "deprecated" : "active";
568
680
  const target = action === "enable" ? "active" : "deprecated";
@@ -624,23 +736,27 @@ function argumentQualifier(argument: PlaybookArgument): string {
624
736
 
625
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. */
626
738
  function playbookInvocationBody(playbook: Artifact, provided: Record<string, unknown>): string {
627
- const trigger = typeof playbook.extra["trigger"] === "string" ? playbook.extra["trigger"] : "manual invocation";
628
- const steps = Array.isArray(playbook.extra["steps"]) ? (playbook.extra["steps"] as PlaybookStep[]) : [];
629
- const tools = Array.isArray(playbook.extra["tools"]) ? playbook.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
630
- const declaredArguments = Array.isArray(playbook.extra["arguments"]) ? (playbook.extra["arguments"] as PlaybookArgument[]) : [];
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[]) : [];
631
743
  const argumentLines = declaredArguments.map((argument) => {
632
744
  const value = provided[argument.name];
633
745
  if (value !== undefined) return `- ${argument.name}: ${String(value)}`;
634
746
  return `- ${argument.name} (${argumentQualifier(argument)}${argument.description ? `: ${argument.description}` : ""}) -- not yet provided`;
635
747
  });
636
- const missingRequired = declaredArguments.filter((argument) => argument.required && provided[argument.name] === undefined && argument.default === undefined);
748
+ const missingRequired = declaredArguments.filter(
749
+ (argument) => argument.required && provided[argument.name] === undefined && argument.default === undefined,
750
+ );
637
751
  return [
638
752
  `Apply Papyrus playbook "${playbook.title}".`,
639
753
  `Trigger: ${trigger}`,
640
754
  ...(playbook.body ? [`Context: ${playbook.body}`] : []),
641
755
  ...(argumentLines.length > 0 ? ["Arguments:", ...argumentLines] : []),
642
756
  ...(missingRequired.length > 0
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.`]
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
+ ]
644
760
  : []),
645
761
  ...(steps.length ? ["Steps:", ...steps.map((step, index) => stepText(step, index))] : []),
646
762
  ...(tools.length ? [`Tools: ${tools.join(", ")}`] : []),
@@ -661,11 +777,20 @@ function playbookInvocationBody(playbook: Artifact, provided: Record<string, unk
661
777
  * to discuss (live:true) rather than guess or silently proceed. `visited` and `depth` are
662
778
  * recursion-internal; callers should not pass them.
663
779
  */
664
- export function playbookInvocation(artifacts: ArtifactStore, id: string, provided: Record<string, unknown> = {}, 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 {
665
787
  const playbook = requireKind(artifacts, id, "playbook");
666
788
  visited.add(id);
667
789
 
668
- 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);
669
794
  const linkedArtifactLines: string[] = [];
670
795
  const nestedSections: string[] = []; // contains -- rendered after this playbook's own body
671
796
  const prerequisiteSections: string[] = []; // depends_on -- rendered before this playbook's own body
@@ -680,14 +805,20 @@ export function playbookInvocation(artifacts: ArtifactStore, id: string, provide
680
805
  const bucket = edge.relation === "contains" ? nestedSections : prerequisiteSections;
681
806
  const role = edge.relation === "contains" ? "nested" : "prerequisite";
682
807
  if (visited.has(target.id)) {
683
- 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
+ );
684
811
  } else if (depth + 1 > PLAYBOOK_INVOCATION_MAX_CALL_DEPTH) {
685
- 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
+ );
686
815
  } else {
687
816
  const nested = playbookInvocation(artifacts, target.id, provided, visited, depth + 1);
688
- bucket.push(edge.relation === "contains"
689
- ? `Nested playbook (contains) "${target.title}" -- run as part of this one:\n${nested}`
690
- : `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
+ );
691
822
  }
692
823
  }
693
824