@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.
- 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 +3 -3
- package/src/artifact-subtree.ts +4 -2
- package/src/authority-registry.ts +2 -1
- package/src/cli.ts +785 -179
- package/src/client.ts +46 -20
- package/src/constants.ts +15 -4
- package/src/daemon-state.ts +4 -12
- package/src/daemon.ts +31 -9
- package/src/db.ts +119 -98
- package/src/discussion-service.ts +109 -44
- package/src/domain/artifact-event.ts +17 -4
- package/src/domain/artifact.ts +3 -1
- package/src/domain/blueprint-definition.ts +26 -31
- 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 +241 -110
- 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 +33 -21
- package/src/modules/notes.ts +66 -28
- package/src/modules/playbooks.ts +88 -29
- 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 +131 -68
- package/src/playbook-definition.ts +56 -17
- package/src/playbook-execution.ts +13 -3
- 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 +143 -89
- 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 +167 -72
- package/src/vehicle/artifact-trash-vehicle.ts +25 -13
- package/src/vehicle/artifact-vehicle-shared.ts +28 -7
- package/src/vehicle/docs-vehicle.ts +48 -16
- package/src/vehicle/notes-vehicle.ts +24 -6
- package/src/vehicle/papyrus-vehicle.ts +14 -3
- package/src/vehicle/playbooks-vehicle.ts +87 -18
- 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 +71 -52
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,
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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)
|
|
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(
|
|
76
|
-
|
|
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(
|
|
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(
|
|
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
|
|
112
|
-
return
|
|
113
|
-
|
|
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
|
|
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>)
|
|
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(
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
310
|
-
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);
|
|
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
|
|
322
|
-
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 : "");
|
|
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
|
|
344
|
-
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;
|
|
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)
|
|
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))
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
413
|
-
if (record
|
|
414
|
-
const values = record
|
|
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
|
-
| {
|
|
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
|
|
437
|
-
const title = value
|
|
518
|
+
const kind = value.kind;
|
|
519
|
+
const title = value.title;
|
|
438
520
|
if (kind === "task") {
|
|
439
|
-
const body = value
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
459
|
-
const action = value
|
|
460
|
-
const severity = value
|
|
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
|
|
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
|
|
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 {
|
|
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))
|
|
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(
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
628
|
-
const steps = Array.isArray(playbook.extra
|
|
629
|
-
const tools = Array.isArray(playbook.extra
|
|
630
|
-
const declaredArguments = Array.isArray(playbook.extra
|
|
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(
|
|
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
|
-
? [
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
689
|
-
|
|
690
|
-
|
|
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
|
|