@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.
- package/README.md +8 -11
- package/package.json +2 -2
- package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
- package/src/adapters/sqlite-artifact-store.ts +7 -5
- package/src/adapters/sqlite-discussion-round-store.ts +26 -17
- package/src/adapters/sqlite-gate-runner.ts +1 -1
- package/src/adapters/sqlite-graph-projection-store.ts +14 -10
- package/src/adapters/sqlite-log-store.ts +36 -17
- package/src/adapters/sqlite-note-event-store.ts +20 -16
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/adapters/sqlite-task-event-store.ts +29 -21
- package/src/adapters/sqlite-task-focus-store.ts +36 -10
- package/src/adapters/sqlite-task-lease-store.ts +12 -6
- package/src/adapters/sqlite-task-scope-store.ts +25 -14
- package/src/artifact-relationship-view.ts +4 -4
- package/src/artifact-subtree.ts +4 -2
- package/src/authority-registry.ts +2 -1
- package/src/cli.ts +794 -354
- package/src/client.ts +6 -3
- package/src/constants.ts +34 -56
- package/src/daemon-state.ts +4 -12
- package/src/daemon.ts +31 -9
- package/src/db.ts +153 -105
- package/src/discussion-service.ts +109 -44
- package/src/domain/artifact-event.ts +18 -5
- package/src/domain/artifact.ts +3 -1
- package/src/domain/blueprint-definition.ts +268 -0
- package/src/domain/checklist.ts +20 -17
- package/src/domain/discussion.ts +37 -18
- package/src/domain/gate.ts +7 -7
- package/src/domain/log-entry.ts +1 -1
- package/src/domain/note-event.ts +20 -7
- package/src/domain/task-event.ts +17 -7
- package/src/domain-services.ts +362 -288
- package/src/graph-projection-service.ts +34 -8
- package/src/id-migration.ts +17 -4
- package/src/index.ts +16 -11
- package/src/log-service.ts +6 -5
- package/src/log.ts +19 -0
- package/src/modules/discuss.ts +63 -28
- package/src/modules/docs.ts +74 -17
- package/src/modules/graph-projection.ts +20 -9
- package/src/modules/logs.ts +34 -22
- package/src/modules/notes.ts +66 -28
- package/src/modules/playbooks.ts +93 -32
- package/src/modules/rules.ts +57 -15
- package/src/modules/session-identity.ts +6 -2
- package/src/modules/tasks.ts +142 -67
- package/src/note-service.ts +11 -7
- package/src/ops.ts +134 -69
- package/src/playbook-definition.ts +124 -39
- package/src/playbook-execution.ts +18 -25
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/ports/note-event-store.ts +6 -4
- package/src/ports/task-event-store.ts +9 -6
- package/src/ports/task-focus-store.ts +17 -4
- package/src/ports/task-lease-store.ts +9 -4
- package/src/ports/task-scope-store.ts +3 -1
- package/src/service.ts +148 -110
- package/src/session-identity-service.ts +10 -2
- package/src/task-context.ts +28 -16
- package/src/task-execution.ts +4 -12
- package/src/task-graph-view.ts +12 -12
- package/src/task-relationship-view.ts +1 -3
- package/src/task-service.ts +168 -73
- package/src/vehicle/artifact-trash-vehicle.ts +26 -14
- package/src/vehicle/artifact-vehicle-shared.ts +32 -13
- package/src/vehicle/docs-vehicle.ts +50 -18
- package/src/vehicle/notes-vehicle.ts +26 -8
- package/src/vehicle/papyrus-vehicle.ts +16 -8
- package/src/vehicle/playbooks-vehicle.ts +88 -19
- package/src/vehicle/rules-vehicle.ts +58 -21
- package/src/vehicle/tasks-vehicle.ts +366 -54
- package/src/version.ts +1 -1
- package/src/workflow-execution.ts +198 -109
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- package/src/vehicle/skills-vehicle.ts +0 -194
package/src/domain-services.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
SKILL_INVOCATION_MAX_LINKED_ARTIFACTS,
|
|
15
|
+
SKILL_MAX_ENUM_VALUES,
|
|
15
16
|
} from "./constants.ts";
|
|
16
|
-
import {
|
|
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
|
|
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)
|
|
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/
|
|
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(
|
|
71
|
-
|
|
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/
|
|
89
|
-
function assignArtifactProject(
|
|
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(
|
|
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
|
|
107
|
-
return
|
|
108
|
-
|
|
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
|
|
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>)
|
|
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(
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
298
|
-
return activeTaskId !== undefined && value
|
|
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
|
|
310
|
-
const action = rule.body || (typeof rule.extra
|
|
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
|
|
332
|
-
const action = typeof rule.extra
|
|
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
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
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)
|
|
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))
|
|
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
|
|
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
|
|
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
|
|
480
|
+
const required = record.required;
|
|
571
481
|
if (required !== undefined && typeof required !== "boolean") throw new Error(`argument "${name}" required must be a boolean`);
|
|
572
|
-
|
|
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?:
|
|
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(
|
|
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
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
683
|
-
const trigger = typeof playbook.extra
|
|
684
|
-
const steps = Array.isArray(playbook.extra
|
|
685
|
-
const tools = Array.isArray(playbook.extra
|
|
686
|
-
const declaredArguments = Array.isArray(playbook.extra
|
|
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
|
-
|
|
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(
|
|
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
|
-
? [
|
|
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) =>
|
|
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,
|
|
715
|
-
*
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|