@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/ops.ts
CHANGED
|
@@ -3,36 +3,40 @@
|
|
|
3
3
|
* Enforces the schema protocol (kinds, statuses, relations) via FK + app validation.
|
|
4
4
|
*/
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { ARTIFACT_TRASH_RETENTION_MS, DEFAULT_STATUS_BY_KIND } from "./constants.ts";
|
|
6
7
|
import type { Db } from "./db.ts";
|
|
7
8
|
import { inTransaction } from "./db.ts";
|
|
8
|
-
import { ARTIFACT_TRASH_RETENTION_MS, DEFAULT_STATUS_BY_KIND } from "./constants.ts";
|
|
9
9
|
import type { Artifact, ArtifactQuery, CreateArtifactInput, UpdateArtifactInput } from "./domain/artifact.ts";
|
|
10
10
|
import type { ArtifactTrashRecord } from "./domain/artifact-trash.ts";
|
|
11
|
+
|
|
11
12
|
export type { ArtifactTrashRecord } from "./domain/artifact-trash.ts";
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
import {
|
|
14
|
-
normalizeArtifactEventQuery,
|
|
15
|
-
resolveArtifactEvent,
|
|
16
15
|
type AppendArtifactEvent,
|
|
17
16
|
type ArtifactEvent,
|
|
18
17
|
type ArtifactEventContext,
|
|
19
18
|
type ArtifactEventPage,
|
|
20
19
|
type ArtifactEventQuery,
|
|
21
20
|
type ArtifactEventType,
|
|
21
|
+
normalizeArtifactEventQuery,
|
|
22
|
+
resolveArtifactEvent,
|
|
22
23
|
} from "./domain/artifact-event.ts";
|
|
24
|
+
import type { Gate, GateResult, GateRunOptions } from "./domain/gate.ts";
|
|
25
|
+
|
|
23
26
|
export type { Artifact } from "./domain/artifact.ts";
|
|
24
27
|
export type { Gate, GateResult } from "./domain/gate.ts";
|
|
25
28
|
export type CreateInput = CreateArtifactInput;
|
|
29
|
+
|
|
26
30
|
import {
|
|
27
31
|
DEFAULT_GRAPH_DEPTH,
|
|
28
32
|
DEFAULT_GRAPH_MAX_NODES,
|
|
29
|
-
MAX_GRAPH_DEPTH,
|
|
30
|
-
MAX_GRAPH_NODES,
|
|
31
33
|
GATE_COMMAND_TIMEOUT_MS,
|
|
32
|
-
GATE_TEST_TIMEOUT_MS,
|
|
33
|
-
GATE_OUTPUT_LIMIT,
|
|
34
|
-
GATE_MAX_BUFFER_BYTES,
|
|
35
34
|
GATE_FILE_MAX_BYTES,
|
|
35
|
+
GATE_MAX_BUFFER_BYTES,
|
|
36
|
+
GATE_OUTPUT_LIMIT,
|
|
37
|
+
GATE_TEST_TIMEOUT_MS,
|
|
38
|
+
MAX_GRAPH_DEPTH,
|
|
39
|
+
MAX_GRAPH_NODES,
|
|
36
40
|
} from "./constants.ts";
|
|
37
41
|
|
|
38
42
|
const require_ = createRequire(import.meta.url);
|
|
@@ -58,8 +62,7 @@ function deepMerge(base: unknown, override: unknown): unknown {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function valueAtPath(value: unknown, path: string): unknown {
|
|
61
|
-
return path.split(".").reduce<unknown>((current, segment) =>
|
|
62
|
-
isRecord(current) ? current[segment] : undefined, value);
|
|
65
|
+
return path.split(".").reduce<unknown>((current, segment) => (isRecord(current) ? current[segment] : undefined), value);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
function isPresent(value: unknown): boolean {
|
|
@@ -75,11 +78,13 @@ function resolveCreateInput(db: Db, input: CreateInput): ResolvedCreateInput {
|
|
|
75
78
|
|
|
76
79
|
const template = getArtifact(db, input.templateId);
|
|
77
80
|
if (!template) throw new Error(`template "${input.templateId}" not found`);
|
|
78
|
-
|
|
81
|
+
// A template is any artifact carrying subtype=artifact-template metadata -- its own kind is
|
|
82
|
+
// irrelevant to its function as a defaults/required carrier for the *target* kind.
|
|
83
|
+
if (template.subtype !== "artifact-template") {
|
|
79
84
|
throw new Error(`artifact "${input.templateId}" is not an artifact template`);
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
const targetKind = template.extra
|
|
87
|
+
const targetKind = template.extra.targetKind;
|
|
83
88
|
if (typeof targetKind !== "string" || targetKind.length === 0) {
|
|
84
89
|
throw new Error(`template "${input.templateId}" has no targetKind`);
|
|
85
90
|
}
|
|
@@ -87,13 +92,13 @@ function resolveCreateInput(db: Db, input: CreateInput): ResolvedCreateInput {
|
|
|
87
92
|
throw new Error(`template "${input.templateId}" targets kind "${targetKind}", not "${input.kind}"`);
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
const defaults = isRecord(template.extra
|
|
95
|
+
const defaults = isRecord(template.extra.defaults) ? template.extra.defaults : {};
|
|
91
96
|
const { templateId: _templateId, ...overrides } = input;
|
|
92
97
|
const merged = deepMerge(defaults, overrides) as CreateInput;
|
|
93
98
|
merged.kind = targetKind;
|
|
94
99
|
|
|
95
|
-
const required = Array.isArray(template.extra
|
|
96
|
-
? template.extra
|
|
100
|
+
const required = Array.isArray(template.extra.required)
|
|
101
|
+
? template.extra.required.filter((field): field is string => typeof field === "string")
|
|
97
102
|
: ["title"];
|
|
98
103
|
for (const field of required) {
|
|
99
104
|
if (!isPresent(valueAtPath(merged, field))) {
|
|
@@ -116,16 +121,16 @@ function defaultStatusFor(db: Db, kind: string): string {
|
|
|
116
121
|
|
|
117
122
|
function rowToArtifact(row: Record<string, unknown>): Artifact {
|
|
118
123
|
return {
|
|
119
|
-
id: row
|
|
120
|
-
kind: row
|
|
121
|
-
title: row
|
|
122
|
-
status: row
|
|
123
|
-
subtype: (row
|
|
124
|
-
body: (row
|
|
125
|
-
labels: JSON.parse((row
|
|
126
|
-
extra: JSON.parse((row
|
|
127
|
-
created_at: row
|
|
128
|
-
updated_at: row
|
|
124
|
+
id: row.id as string,
|
|
125
|
+
kind: row.kind as string,
|
|
126
|
+
title: row.title as string,
|
|
127
|
+
status: row.status as string,
|
|
128
|
+
subtype: (row.subtype as string) ?? "",
|
|
129
|
+
body: (row.body as string) ?? "",
|
|
130
|
+
labels: JSON.parse((row.labels as string) ?? "[]"),
|
|
131
|
+
extra: JSON.parse((row.extra as string) ?? "{}"),
|
|
132
|
+
created_at: row.created_at as string,
|
|
133
|
+
updated_at: row.updated_at as string,
|
|
129
134
|
};
|
|
130
135
|
}
|
|
131
136
|
|
|
@@ -141,23 +146,25 @@ export function appendArtifactEvent(db: Db, input: AppendArtifactEvent): Artifac
|
|
|
141
146
|
const now = new Date().toISOString();
|
|
142
147
|
let id: number | bigint = 0;
|
|
143
148
|
inTransaction(db, () => {
|
|
144
|
-
const result = db
|
|
149
|
+
const result = db
|
|
150
|
+
.prepare(`
|
|
145
151
|
INSERT INTO artifact_events (
|
|
146
152
|
artifact_id, occurred_at, event_type, actor, source, session_id,
|
|
147
153
|
from_status, to_status, relation, related_id, event_schema_version
|
|
148
154
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
149
|
-
`)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
`)
|
|
156
|
+
.run(
|
|
157
|
+
event.artifactId,
|
|
158
|
+
now,
|
|
159
|
+
event.type,
|
|
160
|
+
event.actor,
|
|
161
|
+
event.source,
|
|
162
|
+
event.sessionId ?? null,
|
|
163
|
+
event.fromStatus ?? null,
|
|
164
|
+
event.toStatus ?? null,
|
|
165
|
+
event.relation ?? null,
|
|
166
|
+
event.relatedId ?? null,
|
|
167
|
+
);
|
|
161
168
|
id = result.lastInsertRowid;
|
|
162
169
|
});
|
|
163
170
|
return {
|
|
@@ -213,19 +220,36 @@ export function queryArtifactEvents(db: Db, query: ArtifactEventQuery): Artifact
|
|
|
213
220
|
const { artifactId, actor, sessionId, since, limit, direction, cursor } = normalizeArtifactEventQuery(query);
|
|
214
221
|
const conditions: string[] = [];
|
|
215
222
|
const params: unknown[] = [];
|
|
216
|
-
if (artifactId) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
if (artifactId) {
|
|
224
|
+
conditions.push("(artifact_id = ? OR related_id = ?)");
|
|
225
|
+
params.push(artifactId, artifactId);
|
|
226
|
+
}
|
|
227
|
+
if (actor) {
|
|
228
|
+
conditions.push("actor = ?");
|
|
229
|
+
params.push(actor);
|
|
230
|
+
}
|
|
231
|
+
if (sessionId) {
|
|
232
|
+
conditions.push("session_id = ?");
|
|
233
|
+
params.push(sessionId);
|
|
234
|
+
}
|
|
235
|
+
if (since) {
|
|
236
|
+
conditions.push("occurred_at >= ?");
|
|
237
|
+
params.push(since);
|
|
238
|
+
}
|
|
220
239
|
const comparator = direction === "desc" ? "<" : ">";
|
|
221
|
-
if (cursor !== undefined) {
|
|
240
|
+
if (cursor !== undefined) {
|
|
241
|
+
conditions.push(`id ${comparator} ?`);
|
|
242
|
+
params.push(cursor);
|
|
243
|
+
}
|
|
222
244
|
const order = direction === "desc" ? "DESC" : "ASC";
|
|
223
|
-
const rows = db
|
|
245
|
+
const rows = db
|
|
246
|
+
.prepare(`
|
|
224
247
|
SELECT * FROM artifact_events
|
|
225
248
|
WHERE ${conditions.join(" AND ")}
|
|
226
249
|
ORDER BY occurred_at ${order}, id ${order}
|
|
227
250
|
LIMIT ?
|
|
228
|
-
`)
|
|
251
|
+
`)
|
|
252
|
+
.all(...params, limit + 1) as ArtifactEventRow[];
|
|
229
253
|
const hasMore = rows.length > limit;
|
|
230
254
|
const events = rows.slice(0, limit).map(mapArtifactEventRow);
|
|
231
255
|
return { events, ...(hasMore ? { nextCursor: events.at(-1)!.id } : {}) };
|
|
@@ -260,7 +284,11 @@ export function getArtifact(db: Db, id: string, opts?: { tree?: boolean; depth?:
|
|
|
260
284
|
const depthLimit = Math.min(MAX_GRAPH_DEPTH, Math.max(0, Math.floor(opts.depth ?? DEFAULT_GRAPH_DEPTH)));
|
|
261
285
|
const nodeLimit = Math.min(MAX_GRAPH_NODES, Math.max(1, Math.floor(opts.maxNodes ?? DEFAULT_GRAPH_MAX_NODES)));
|
|
262
286
|
const queue: Array<{ id: string; depth: number }> = [{ id, depth: 0 }];
|
|
263
|
-
const allEdges = db.prepare('SELECT from_id AS "from", relation, to_id AS "to" FROM edges').all() as {
|
|
287
|
+
const allEdges = db.prepare('SELECT from_id AS "from", relation, to_id AS "to" FROM edges').all() as {
|
|
288
|
+
from: string;
|
|
289
|
+
relation: string;
|
|
290
|
+
to: string;
|
|
291
|
+
}[];
|
|
264
292
|
const reachable = new Set<string>([id]);
|
|
265
293
|
const adj = new Map<string, { from: string; relation: string; to: string }[]>();
|
|
266
294
|
for (const edge of allEdges) {
|
|
@@ -295,16 +323,31 @@ export function queryArtifacts(db: Db, filter: ArtifactQuery): Artifact[] {
|
|
|
295
323
|
conditions.push(`id IN (${filter.ids.map(() => "?").join(", ")})`);
|
|
296
324
|
params.push(...filter.ids);
|
|
297
325
|
}
|
|
298
|
-
if (filter.kind) {
|
|
299
|
-
|
|
326
|
+
if (filter.kind) {
|
|
327
|
+
conditions.push("kind = ?");
|
|
328
|
+
params.push(filter.kind);
|
|
329
|
+
}
|
|
330
|
+
if (filter.status) {
|
|
331
|
+
conditions.push("status = ?");
|
|
332
|
+
params.push(filter.status);
|
|
333
|
+
}
|
|
300
334
|
if (filter.statuses) {
|
|
301
335
|
if (filter.statuses.length === 0) return [];
|
|
302
336
|
conditions.push(`status IN (${filter.statuses.map(() => "?").join(", ")})`);
|
|
303
337
|
params.push(...filter.statuses);
|
|
304
338
|
}
|
|
305
|
-
if (filter.subtype) {
|
|
306
|
-
|
|
307
|
-
|
|
339
|
+
if (filter.subtype) {
|
|
340
|
+
conditions.push("subtype = ?");
|
|
341
|
+
params.push(filter.subtype);
|
|
342
|
+
}
|
|
343
|
+
if (filter.excludeSubtype) {
|
|
344
|
+
conditions.push("subtype != ?");
|
|
345
|
+
params.push(filter.excludeSubtype);
|
|
346
|
+
}
|
|
347
|
+
if (filter.text) {
|
|
348
|
+
conditions.push("(title LIKE ? OR body LIKE ?)");
|
|
349
|
+
params.push(`%${filter.text}%`, `%${filter.text}%`);
|
|
350
|
+
}
|
|
308
351
|
for (const label of filter.labels ?? []) {
|
|
309
352
|
conditions.push("EXISTS (SELECT 1 FROM json_each(artifacts.labels) WHERE value = ?)");
|
|
310
353
|
params.push(label);
|
|
@@ -314,7 +357,7 @@ export function queryArtifacts(db: Db, filter: ArtifactQuery): Artifact[] {
|
|
|
314
357
|
conditions.push("json_extract(extra, ?) = ?");
|
|
315
358
|
params.push(`$.${key}`, value);
|
|
316
359
|
}
|
|
317
|
-
if (conditions.length) sql +=
|
|
360
|
+
if (conditions.length) sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
318
361
|
sql += " ORDER BY updated_at DESC";
|
|
319
362
|
if (filter.limit !== undefined) {
|
|
320
363
|
if (!Number.isInteger(filter.limit) || filter.limit < 1) throw new Error("artifact query limit must be a positive integer");
|
|
@@ -327,20 +370,25 @@ export function queryArtifacts(db: Db, filter: ArtifactQuery): Artifact[] {
|
|
|
327
370
|
|
|
328
371
|
function rowToTrashRecord(row: Record<string, unknown>): ArtifactTrashRecord {
|
|
329
372
|
return {
|
|
330
|
-
artifactId: row
|
|
331
|
-
trashedAt: row
|
|
332
|
-
purgeAfter: row
|
|
333
|
-
...(row
|
|
373
|
+
artifactId: row.artifact_id as string,
|
|
374
|
+
trashedAt: row.trashed_at as string,
|
|
375
|
+
purgeAfter: row.purge_after as string,
|
|
376
|
+
...(row.reason == null ? {} : { reason: row.reason as string }),
|
|
334
377
|
};
|
|
335
378
|
}
|
|
336
379
|
|
|
337
380
|
export function getArtifactTrash(db: Db, id: string): ArtifactTrashRecord | null {
|
|
338
|
-
const row = db.prepare("SELECT artifact_id, trashed_at, purge_after, reason FROM artifact_trash WHERE artifact_id = ?").get(id) as Record<
|
|
381
|
+
const row = db.prepare("SELECT artifact_id, trashed_at, purge_after, reason FROM artifact_trash WHERE artifact_id = ?").get(id) as Record<
|
|
382
|
+
string,
|
|
383
|
+
unknown
|
|
384
|
+
> | null;
|
|
339
385
|
return row ? rowToTrashRecord(row) : null;
|
|
340
386
|
}
|
|
341
387
|
|
|
342
388
|
export function listArtifactTrash(db: Db): ArtifactTrashRecord[] {
|
|
343
|
-
const rows = db
|
|
389
|
+
const rows = db
|
|
390
|
+
.prepare("SELECT artifact_id, trashed_at, purge_after, reason FROM artifact_trash ORDER BY purge_after ASC")
|
|
391
|
+
.all() as Record<string, unknown>[];
|
|
344
392
|
return rows.map(rowToTrashRecord);
|
|
345
393
|
}
|
|
346
394
|
|
|
@@ -357,11 +405,16 @@ export function listArtifactTrash(db: Db): ArtifactTrashRecord[] {
|
|
|
357
405
|
* caller is not necessarily looking at right now. No other kind has an analogous "currently
|
|
358
406
|
* in use" signal to check.
|
|
359
407
|
*/
|
|
360
|
-
export function trashArtifact(
|
|
408
|
+
export function trashArtifact(
|
|
409
|
+
db: Db,
|
|
410
|
+
id: string,
|
|
411
|
+
options?: { reason?: string; now?: () => string; context?: ArtifactEventContext },
|
|
412
|
+
): ArtifactTrashRecord {
|
|
361
413
|
const artifact = getArtifact(db, id);
|
|
362
414
|
if (!artifact) throw new Error(`artifact "${id}" not found`);
|
|
363
415
|
const focusedScope = db.prepare("SELECT scope FROM task_focus WHERE task_id = ? LIMIT 1").get(id) as { scope: string } | null;
|
|
364
|
-
if (focusedScope)
|
|
416
|
+
if (focusedScope)
|
|
417
|
+
throw new Error(`artifact "${id}" is the active Task Focus in scope "${focusedScope.scope}"; clear focus before removing it`);
|
|
365
418
|
const now = options?.now ?? (() => new Date().toISOString());
|
|
366
419
|
const trashedAt = now();
|
|
367
420
|
const purgeAfter = new Date(new Date(trashedAt).getTime() + ARTIFACT_TRASH_RETENTION_MS).toISOString();
|
|
@@ -407,7 +460,9 @@ export function restoreArtifact(db: Db, id: string, context?: ArtifactEventConte
|
|
|
407
460
|
*/
|
|
408
461
|
export function purgeDueArtifacts(db: Db, now: () => string = () => new Date().toISOString()): number {
|
|
409
462
|
const nowIso = now();
|
|
410
|
-
const due = (
|
|
463
|
+
const due = (
|
|
464
|
+
db.prepare("SELECT artifact_id FROM artifact_trash WHERE purge_after <= ?").all(nowIso) as Array<{ artifact_id: string }>
|
|
465
|
+
).map((row) => row.artifact_id);
|
|
411
466
|
let purged = 0;
|
|
412
467
|
for (const id of due) {
|
|
413
468
|
inTransaction(db, () => {
|
|
@@ -500,7 +555,10 @@ export function updateExtra(db: Db, id: string, extra: Record<string, unknown>,
|
|
|
500
555
|
|
|
501
556
|
/** Active rules with inject metadata — for before_agent_start system prompt injection. */
|
|
502
557
|
export function injectableRules(db: Db): Array<{ id: string; title: string; body: string; extra: Record<string, unknown> }> {
|
|
503
|
-
const rows = db.prepare("SELECT * FROM artifacts WHERE kind = 'rule' AND status = 'active' ORDER BY updated_at DESC").all() as Record<
|
|
558
|
+
const rows = db.prepare("SELECT * FROM artifacts WHERE kind = 'rule' AND status = 'active' ORDER BY updated_at DESC").all() as Record<
|
|
559
|
+
string,
|
|
560
|
+
unknown
|
|
561
|
+
>[];
|
|
504
562
|
return rows.map((row) => {
|
|
505
563
|
const art = rowToArtifact(row);
|
|
506
564
|
return { id: art.id, title: art.title, body: art.body, extra: art.extra };
|
|
@@ -536,15 +594,19 @@ function runProcessGateSync(gate: Gate, cwd?: string): GateResult {
|
|
|
536
594
|
if (result.error) return { gate, passed: false, output: result.error.message.slice(0, GATE_OUTPUT_LIMIT) };
|
|
537
595
|
const combined = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
538
596
|
const passed = result.status === 0 && (gate.expect ? combined.includes(gate.expect) : true);
|
|
539
|
-
return {
|
|
597
|
+
return {
|
|
598
|
+
gate,
|
|
599
|
+
passed,
|
|
600
|
+
output: combined.slice(0, GATE_OUTPUT_LIMIT) || (result.status === 0 ? "ok" : `command exited with code ${result.status}`),
|
|
601
|
+
};
|
|
540
602
|
}
|
|
541
603
|
|
|
542
604
|
export function runGates(db: Db, artifactId: string, options: GateRunOptions = {}): GateResult[] {
|
|
543
605
|
const art = getArtifact(db, artifactId);
|
|
544
606
|
if (!art) throw new Error("artifact not found");
|
|
545
|
-
const gates = (art.extra
|
|
607
|
+
const gates = (art.extra.gates as Gate[]) ?? [];
|
|
546
608
|
const cwd = options.cwd;
|
|
547
|
-
return gates.map((gate) => (gate.type === "command" || gate.type === "test"
|
|
609
|
+
return gates.map((gate) => (gate.type === "command" || gate.type === "test" ? runProcessGateSync(gate, cwd) : runNonProcessGate(gate)));
|
|
548
610
|
}
|
|
549
611
|
|
|
550
612
|
/**
|
|
@@ -558,7 +620,11 @@ export function runGates(db: Db, artifactId: string, options: GateRunOptions = {
|
|
|
558
620
|
* indefinitely after Papyrus considers the gate "timed out". Spawning detached (its own
|
|
559
621
|
* process group) and killing the negated pid on our own timer reaches the whole tree.
|
|
560
622
|
*/
|
|
561
|
-
function executeGateCommand(
|
|
623
|
+
function executeGateCommand(
|
|
624
|
+
command: string,
|
|
625
|
+
timeout: number,
|
|
626
|
+
cwd?: string,
|
|
627
|
+
): Promise<{ passed: boolean; output: string; matchable: string }> {
|
|
562
628
|
// `spawn(..., { shell: true, detached: true })` instead of the `exec()` convenience wrapper:
|
|
563
629
|
// `detached` (needed to make the shell the leader of its own process group, so the negated pid
|
|
564
630
|
// below reaches every descendant, not just the shell) is not part of Node's `exec()`/
|
|
@@ -634,7 +700,7 @@ function runNonProcessGate(gate: Gate): GateResult {
|
|
|
634
700
|
export async function runGatesAsync(db: Db, artifactId: string, options: GateRunOptions = {}): Promise<GateResult[]> {
|
|
635
701
|
const art = getArtifact(db, artifactId);
|
|
636
702
|
if (!art) throw new Error("artifact not found");
|
|
637
|
-
const gates = (art.extra
|
|
703
|
+
const gates = (art.extra.gates as Gate[]) ?? [];
|
|
638
704
|
const results: GateResult[] = [];
|
|
639
705
|
for (const gate of gates) {
|
|
640
706
|
const remainingMs = options.deadlineMs === undefined ? undefined : options.deadlineMs - Date.now();
|
|
@@ -657,4 +723,3 @@ export async function runGatesAsync(db: Db, artifactId: string, options: GateRun
|
|
|
657
723
|
}
|
|
658
724
|
return results;
|
|
659
725
|
}
|
|
660
|
-
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* playbook-definition.ts — compiles a Playbook's own steps/trigger/tools/arguments, plus its
|
|
3
|
-
* `contains` (nested) and `depends_on` (prerequisite) composition tree, into
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* `contains` (nested) and `depends_on` (prerequisite) whole-artifact composition tree, into
|
|
4
|
+
* an in-memory BlueprintDefinition. Rather than a second graph-materialization engine, a
|
|
5
|
+
* Playbook becomes a definition that compiles down to the exact Blueprint shape a
|
|
6
|
+
* workflow-definition target already uses, then hands off to workflow-execution.ts's shared
|
|
7
|
+
* materializeWorkflowDefinition for the actual artifact creation.
|
|
8
8
|
*
|
|
9
9
|
* One task blueprint per playbook-node root (a container, never itself gated) plus one per
|
|
10
10
|
* step (chained by sequential dependsOn); `contains`-linked playbooks nest their own root
|
|
11
11
|
* under the parent root and continue the parent's own step chain ("run as part of this one",
|
|
12
12
|
* after the parent's own steps); `depends_on`-linked playbooks compile as independent
|
|
13
|
-
* subtrees whose tails gate this node's first step ("complete this FIRST").
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* subtrees whose tails gate this node's first step ("complete this FIRST"). That whole-artifact
|
|
14
|
+
* composition tree is fully known and owned at compile time and is always inlined directly
|
|
15
|
+
* (no CallBlueprint indirection). A step-level `call` (one step within a single playbook
|
|
16
|
+
* node, not the composition tree above) is the one place this compiler DOES emit a
|
|
17
|
+
* CallBlueprint -- a finer-grained, in-blueprint nested-run reference resolved and executed
|
|
18
|
+
* independently by workflow-execution.ts, exactly like a workflow-definition target's own
|
|
19
|
+
* nested pipeline step, since its target's own definition is not knowable at this compile time.
|
|
17
20
|
*/
|
|
18
21
|
import {
|
|
19
22
|
PLAYBOOK_INVOCATION_MAX_CALL_DEPTH,
|
|
@@ -21,9 +24,16 @@ import {
|
|
|
21
24
|
PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS,
|
|
22
25
|
} from "./constants.ts";
|
|
23
26
|
import type { Artifact } from "./domain/artifact.ts";
|
|
24
|
-
import type {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
import type {
|
|
28
|
+
BlueprintDefinition,
|
|
29
|
+
BlueprintInputDefinition,
|
|
30
|
+
CallBlueprint,
|
|
31
|
+
DocBlueprint,
|
|
32
|
+
RuleBlueprint,
|
|
33
|
+
TaskBlueprint,
|
|
34
|
+
} from "./domain/blueprint-definition.ts";
|
|
35
|
+
import { validateBlueprintDefinition } from "./domain/blueprint-definition.ts";
|
|
36
|
+
import type { PlaybookArgument, PlaybookStep } from "./domain-services.ts";
|
|
27
37
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
28
38
|
|
|
29
39
|
/** A non-composing edge touching a playbook node, to be mirrored onto that node's generated root task once real task ids exist -- e.g. a Rule `gates` this playbook, or this playbook `references`/`documents` a Doc. Direction is preserved exactly: `from`/`to` name whichever side is NOT the playbook, and `ownerIsFrom` says which side the playbook (now the generated root task) occupies. */
|
|
@@ -36,7 +46,7 @@ export interface PlaybookExternalLink {
|
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
export interface CompiledPlaybook {
|
|
39
|
-
definition:
|
|
49
|
+
definition: BlueprintDefinition;
|
|
40
50
|
/** The very first real leaf task in the whole tree's reading order -- what a caller should focus once materialized. */
|
|
41
51
|
entryRef: string;
|
|
42
52
|
externalLinks: PlaybookExternalLink[];
|
|
@@ -56,21 +66,21 @@ function requirePlaybook(artifacts: ArtifactStore, id: string): Artifact {
|
|
|
56
66
|
return playbook;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
function stepsOf(playbook: Artifact):
|
|
60
|
-
return Array.isArray(playbook.extra
|
|
69
|
+
function stepsOf(playbook: Artifact): PlaybookStep[] {
|
|
70
|
+
return Array.isArray(playbook.extra.steps) ? (playbook.extra.steps as PlaybookStep[]) : [];
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
function toolsOf(playbook: Artifact): string[] {
|
|
64
|
-
return Array.isArray(playbook.extra
|
|
74
|
+
return Array.isArray(playbook.extra.tools) ? playbook.extra.tools.filter((tool): tool is string => typeof tool === "string") : [];
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
function argumentsOf(playbook: Artifact): PlaybookArgument[] {
|
|
68
|
-
return Array.isArray(playbook.extra
|
|
78
|
+
return Array.isArray(playbook.extra.arguments) ? (playbook.extra.arguments as PlaybookArgument[]) : [];
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
/** The generated container task's own body -- purpose and context only. Steps are separate child tasks, so they are not re-listed here (that was the old text-dump shape). */
|
|
72
82
|
function rootTaskBody(playbook: Artifact): string {
|
|
73
|
-
const trigger = typeof playbook.extra
|
|
83
|
+
const trigger = typeof playbook.extra.trigger === "string" ? playbook.extra.trigger : "manual invocation";
|
|
74
84
|
const tools = toolsOf(playbook);
|
|
75
85
|
return [
|
|
76
86
|
`Playbook "${playbook.title}".`,
|
|
@@ -87,8 +97,11 @@ function stepTitle(step: string): string {
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
interface CompileContext {
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
docs: DocBlueprint[];
|
|
101
|
+
rules: RuleBlueprint[];
|
|
102
|
+
tasks: TaskBlueprint[];
|
|
103
|
+
skills: CallBlueprint[];
|
|
104
|
+
inputs: Record<string, BlueprintInputDefinition>;
|
|
92
105
|
externalLinks: PlaybookExternalLink[];
|
|
93
106
|
refCounter: { n: number };
|
|
94
107
|
}
|
|
@@ -101,9 +114,20 @@ interface CompileNodeResult {
|
|
|
101
114
|
tailRef: string;
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
|
|
117
|
+
/** A composition tree can declare the same argument name from more than one node (e.g. two prerequisite playbooks both need a `target`); required OR-accumulates the same way it always did, but the type must agree everywhere -- silently picking one node's type over another's would compile a definition whose placeholder substitution disagrees with what one of the two authors actually declared. */
|
|
118
|
+
function mergeArgument(inputs: Record<string, BlueprintInputDefinition>, argument: PlaybookArgument): void {
|
|
105
119
|
const existing = inputs[argument.name];
|
|
106
|
-
|
|
120
|
+
if (existing && existing.type !== argument.type) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`playbook composition declares conflicting types for argument "${argument.name}" (${existing.type} vs ${argument.type})`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
inputs[argument.name] = {
|
|
126
|
+
type: argument.type,
|
|
127
|
+
required: (existing?.required ?? false) || argument.required,
|
|
128
|
+
...(argument.enum ? { enum: argument.enum } : {}),
|
|
129
|
+
...(argument.default !== undefined ? { default: argument.default } : {}),
|
|
130
|
+
};
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
function compileNode(
|
|
@@ -116,28 +140,44 @@ function compileNode(
|
|
|
116
140
|
incomingPrecedingRefs: string[],
|
|
117
141
|
): CompileNodeResult {
|
|
118
142
|
if (ancestorIds.has(playbookId)) throw new Error(`playbook composition cycle includes "${playbookId}"`);
|
|
119
|
-
if (depth > PLAYBOOK_INVOCATION_MAX_CALL_DEPTH)
|
|
143
|
+
if (depth > PLAYBOOK_INVOCATION_MAX_CALL_DEPTH)
|
|
144
|
+
throw new Error(`playbook composition exceeds ${PLAYBOOK_INVOCATION_MAX_CALL_DEPTH} levels`);
|
|
120
145
|
const nextAncestors = new Set([...ancestorIds, playbookId]);
|
|
121
146
|
|
|
122
147
|
const playbook = requirePlaybook(artifacts, playbookId);
|
|
123
148
|
for (const argument of argumentsOf(playbook)) mergeArgument(ctx.inputs, argument);
|
|
124
149
|
|
|
125
150
|
const rootRef = `pb${ctx.refCounter.n++}`;
|
|
126
|
-
const rootBlueprint:
|
|
151
|
+
const rootBlueprint: TaskBlueprint = {
|
|
152
|
+
ref: rootRef,
|
|
153
|
+
title: playbook.title,
|
|
154
|
+
body: rootTaskBody(playbook),
|
|
155
|
+
...(parentRef ? { parent: parentRef } : {}),
|
|
156
|
+
};
|
|
127
157
|
ctx.tasks.push(rootBlueprint);
|
|
128
|
-
if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS)
|
|
158
|
+
if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS)
|
|
159
|
+
throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} tasks`);
|
|
129
160
|
|
|
130
161
|
const edges = artifacts.relationships({ artifactIds: [playbookId] }).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
|
|
131
|
-
const composablePlaybookIds = nonTrashedPlaybookIds(
|
|
132
|
-
|
|
162
|
+
const composablePlaybookIds = nonTrashedPlaybookIds(
|
|
163
|
+
artifacts,
|
|
164
|
+
edges.filter((edge) => edge.from === playbookId).map((edge) => edge.to),
|
|
165
|
+
);
|
|
166
|
+
const prerequisiteIds = edges
|
|
167
|
+
.filter((edge) => edge.from === playbookId && edge.relation === "depends_on")
|
|
168
|
+
.map((edge) => edge.to)
|
|
133
169
|
.filter((id) => composablePlaybookIds.has(id));
|
|
134
|
-
const nestedIds = edges
|
|
170
|
+
const nestedIds = edges
|
|
171
|
+
.filter((edge) => edge.from === playbookId && edge.relation === "contains")
|
|
172
|
+
.map((edge) => edge.to)
|
|
135
173
|
.filter((id) => composablePlaybookIds.has(id));
|
|
136
174
|
for (const edge of edges) {
|
|
137
|
-
const isComposingFrom =
|
|
175
|
+
const isComposingFrom =
|
|
176
|
+
edge.from === playbookId && (edge.relation === "contains" || edge.relation === "depends_on") && composablePlaybookIds.has(edge.to);
|
|
138
177
|
if (isComposingFrom) continue;
|
|
139
178
|
if (edge.from === playbookId) ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.to, ownerIsFrom: true });
|
|
140
|
-
else if (edge.to === playbookId)
|
|
179
|
+
else if (edge.to === playbookId)
|
|
180
|
+
ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.from, ownerIsFrom: false });
|
|
141
181
|
}
|
|
142
182
|
|
|
143
183
|
const prerequisiteTailRefs: string[] = [];
|
|
@@ -148,15 +188,60 @@ function compileNode(
|
|
|
148
188
|
if (headRef === undefined) headRef = result.headRef;
|
|
149
189
|
}
|
|
150
190
|
|
|
191
|
+
// Doc/rule steps are not gated tasks (DocBlueprint/RuleBlueprint have no dependsOn/parent of
|
|
192
|
+
// their own -- workflow-execution.ts always creates them unconditionally alongside the run)
|
|
193
|
+
// -- they do not touch cursorPrecedingRefs/headRef/tailRef at all. A task or call step DOES
|
|
194
|
+
// occupy a position in the sequential chain, exactly as a plain-string step always did.
|
|
151
195
|
let cursorPrecedingRefs = [...incomingPrecedingRefs, ...prerequisiteTailRefs];
|
|
152
196
|
let tailRef = rootRef;
|
|
153
197
|
for (const [index, step] of stepsOf(playbook).entries()) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
198
|
+
if (typeof step === "string" || step.kind === "task") {
|
|
199
|
+
const body = typeof step === "string" ? step : step.body;
|
|
200
|
+
const title = typeof step === "string" ? stepTitle(step) : (step.title ?? stepTitle(body));
|
|
201
|
+
const stepRef = `${rootRef}-s${index}`;
|
|
202
|
+
ctx.tasks.push({ ref: stepRef, title, body, parent: rootRef, dependsOn: cursorPrecedingRefs });
|
|
203
|
+
if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS)
|
|
204
|
+
throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} tasks`);
|
|
205
|
+
if (headRef === undefined) headRef = stepRef;
|
|
206
|
+
cursorPrecedingRefs = [stepRef];
|
|
207
|
+
tailRef = stepRef;
|
|
208
|
+
} else if (step.kind === "doc") {
|
|
209
|
+
const stepRef = `${rootRef}-d${index}`;
|
|
210
|
+
ctx.docs.push({
|
|
211
|
+
ref: stepRef,
|
|
212
|
+
title: step.title,
|
|
213
|
+
...(step.body ? { body: step.body } : {}),
|
|
214
|
+
...(step.subtype ? { subtype: step.subtype } : {}),
|
|
215
|
+
...(step.labels ? { labels: step.labels } : {}),
|
|
216
|
+
});
|
|
217
|
+
} else if (step.kind === "rule") {
|
|
218
|
+
const stepRef = `${rootRef}-r${index}`;
|
|
219
|
+
ctx.rules.push({
|
|
220
|
+
ref: stepRef,
|
|
221
|
+
title: step.title,
|
|
222
|
+
...(step.body ? { body: step.body } : {}),
|
|
223
|
+
...(step.condition ? { condition: step.condition } : {}),
|
|
224
|
+
...(step.action ? { action: step.action } : {}),
|
|
225
|
+
...(step.severity ? { severity: step.severity } : {}),
|
|
226
|
+
...(step.labels ? { labels: step.labels } : {}),
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
// kind === "call": nests another Playbook's (or a workflow-definition target's) run as
|
|
230
|
+
// a pipeline step -- shares the same dependsOn chain as a task step, resolved
|
|
231
|
+
// polymorphically at execution time by workflow-execution.ts based on the target's kind.
|
|
232
|
+
const stepRef = `${rootRef}-c${index}`;
|
|
233
|
+
ctx.skills.push({
|
|
234
|
+
ref: stepRef,
|
|
235
|
+
title: step.title,
|
|
236
|
+
targetId: step.playbookId,
|
|
237
|
+
...(step.arguments ? { arguments: step.arguments } : {}),
|
|
238
|
+
parent: rootRef,
|
|
239
|
+
dependsOn: cursorPrecedingRefs,
|
|
240
|
+
});
|
|
241
|
+
if (headRef === undefined) headRef = stepRef;
|
|
242
|
+
cursorPrecedingRefs = [stepRef];
|
|
243
|
+
tailRef = stepRef;
|
|
244
|
+
}
|
|
160
245
|
}
|
|
161
246
|
|
|
162
247
|
for (const nestedId of nestedIds) {
|
|
@@ -172,12 +257,12 @@ function compileNode(
|
|
|
172
257
|
|
|
173
258
|
/** Pure and read-only: creates no artifacts. Cycle/depth-bounded exactly like playbookInvocation's own traversal, but a composition cycle here is a hard error (real Tasks would be created, unlike a text render degrading to a marker). */
|
|
174
259
|
export function compilePlaybookDefinition(artifacts: ArtifactStore, playbookId: string): CompiledPlaybook {
|
|
175
|
-
const ctx: CompileContext = { tasks: [], inputs: {}, externalLinks: [], refCounter: { n: 0 } };
|
|
260
|
+
const ctx: CompileContext = { docs: [], rules: [], tasks: [], skills: [], inputs: {}, externalLinks: [], refCounter: { n: 0 } };
|
|
176
261
|
const { headRef } = compileNode(artifacts, playbookId, ctx, new Set(), 0, undefined, []);
|
|
177
|
-
const definition =
|
|
262
|
+
const definition = validateBlueprintDefinition({
|
|
178
263
|
version: 1,
|
|
179
264
|
inputs: ctx.inputs,
|
|
180
|
-
blueprints: { docs:
|
|
265
|
+
blueprints: { docs: ctx.docs, rules: ctx.rules, tasks: ctx.tasks, skills: ctx.skills },
|
|
181
266
|
links: [],
|
|
182
267
|
});
|
|
183
268
|
return { definition, entryRef: headRef, externalLinks: ctx.externalLinks };
|