@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/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 {
|
|
@@ -81,7 +84,7 @@ function resolveCreateInput(db: Db, input: CreateInput): ResolvedCreateInput {
|
|
|
81
84
|
throw new Error(`artifact "${input.templateId}" is not an artifact template`);
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
const targetKind = template.extra
|
|
87
|
+
const targetKind = template.extra.targetKind;
|
|
85
88
|
if (typeof targetKind !== "string" || targetKind.length === 0) {
|
|
86
89
|
throw new Error(`template "${input.templateId}" has no targetKind`);
|
|
87
90
|
}
|
|
@@ -89,13 +92,13 @@ function resolveCreateInput(db: Db, input: CreateInput): ResolvedCreateInput {
|
|
|
89
92
|
throw new Error(`template "${input.templateId}" targets kind "${targetKind}", not "${input.kind}"`);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
const defaults = isRecord(template.extra
|
|
95
|
+
const defaults = isRecord(template.extra.defaults) ? template.extra.defaults : {};
|
|
93
96
|
const { templateId: _templateId, ...overrides } = input;
|
|
94
97
|
const merged = deepMerge(defaults, overrides) as CreateInput;
|
|
95
98
|
merged.kind = targetKind;
|
|
96
99
|
|
|
97
|
-
const required = Array.isArray(template.extra
|
|
98
|
-
? template.extra
|
|
100
|
+
const required = Array.isArray(template.extra.required)
|
|
101
|
+
? template.extra.required.filter((field): field is string => typeof field === "string")
|
|
99
102
|
: ["title"];
|
|
100
103
|
for (const field of required) {
|
|
101
104
|
if (!isPresent(valueAtPath(merged, field))) {
|
|
@@ -118,16 +121,16 @@ function defaultStatusFor(db: Db, kind: string): string {
|
|
|
118
121
|
|
|
119
122
|
function rowToArtifact(row: Record<string, unknown>): Artifact {
|
|
120
123
|
return {
|
|
121
|
-
id: row
|
|
122
|
-
kind: row
|
|
123
|
-
title: row
|
|
124
|
-
status: row
|
|
125
|
-
subtype: (row
|
|
126
|
-
body: (row
|
|
127
|
-
labels: JSON.parse((row
|
|
128
|
-
extra: JSON.parse((row
|
|
129
|
-
created_at: row
|
|
130
|
-
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,
|
|
131
134
|
};
|
|
132
135
|
}
|
|
133
136
|
|
|
@@ -143,23 +146,25 @@ export function appendArtifactEvent(db: Db, input: AppendArtifactEvent): Artifac
|
|
|
143
146
|
const now = new Date().toISOString();
|
|
144
147
|
let id: number | bigint = 0;
|
|
145
148
|
inTransaction(db, () => {
|
|
146
|
-
const result = db
|
|
149
|
+
const result = db
|
|
150
|
+
.prepare(`
|
|
147
151
|
INSERT INTO artifact_events (
|
|
148
152
|
artifact_id, occurred_at, event_type, actor, source, session_id,
|
|
149
153
|
from_status, to_status, relation, related_id, event_schema_version
|
|
150
154
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
|
151
|
-
`)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
);
|
|
163
168
|
id = result.lastInsertRowid;
|
|
164
169
|
});
|
|
165
170
|
return {
|
|
@@ -215,19 +220,36 @@ export function queryArtifactEvents(db: Db, query: ArtifactEventQuery): Artifact
|
|
|
215
220
|
const { artifactId, actor, sessionId, since, limit, direction, cursor } = normalizeArtifactEventQuery(query);
|
|
216
221
|
const conditions: string[] = [];
|
|
217
222
|
const params: unknown[] = [];
|
|
218
|
-
if (artifactId) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
}
|
|
222
239
|
const comparator = direction === "desc" ? "<" : ">";
|
|
223
|
-
if (cursor !== undefined) {
|
|
240
|
+
if (cursor !== undefined) {
|
|
241
|
+
conditions.push(`id ${comparator} ?`);
|
|
242
|
+
params.push(cursor);
|
|
243
|
+
}
|
|
224
244
|
const order = direction === "desc" ? "DESC" : "ASC";
|
|
225
|
-
const rows = db
|
|
245
|
+
const rows = db
|
|
246
|
+
.prepare(`
|
|
226
247
|
SELECT * FROM artifact_events
|
|
227
248
|
WHERE ${conditions.join(" AND ")}
|
|
228
249
|
ORDER BY occurred_at ${order}, id ${order}
|
|
229
250
|
LIMIT ?
|
|
230
|
-
`)
|
|
251
|
+
`)
|
|
252
|
+
.all(...params, limit + 1) as ArtifactEventRow[];
|
|
231
253
|
const hasMore = rows.length > limit;
|
|
232
254
|
const events = rows.slice(0, limit).map(mapArtifactEventRow);
|
|
233
255
|
return { events, ...(hasMore ? { nextCursor: events.at(-1)!.id } : {}) };
|
|
@@ -262,7 +284,11 @@ export function getArtifact(db: Db, id: string, opts?: { tree?: boolean; depth?:
|
|
|
262
284
|
const depthLimit = Math.min(MAX_GRAPH_DEPTH, Math.max(0, Math.floor(opts.depth ?? DEFAULT_GRAPH_DEPTH)));
|
|
263
285
|
const nodeLimit = Math.min(MAX_GRAPH_NODES, Math.max(1, Math.floor(opts.maxNodes ?? DEFAULT_GRAPH_MAX_NODES)));
|
|
264
286
|
const queue: Array<{ id: string; depth: number }> = [{ id, depth: 0 }];
|
|
265
|
-
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
|
+
}[];
|
|
266
292
|
const reachable = new Set<string>([id]);
|
|
267
293
|
const adj = new Map<string, { from: string; relation: string; to: string }[]>();
|
|
268
294
|
for (const edge of allEdges) {
|
|
@@ -297,16 +323,31 @@ export function queryArtifacts(db: Db, filter: ArtifactQuery): Artifact[] {
|
|
|
297
323
|
conditions.push(`id IN (${filter.ids.map(() => "?").join(", ")})`);
|
|
298
324
|
params.push(...filter.ids);
|
|
299
325
|
}
|
|
300
|
-
if (filter.kind) {
|
|
301
|
-
|
|
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
|
+
}
|
|
302
334
|
if (filter.statuses) {
|
|
303
335
|
if (filter.statuses.length === 0) return [];
|
|
304
336
|
conditions.push(`status IN (${filter.statuses.map(() => "?").join(", ")})`);
|
|
305
337
|
params.push(...filter.statuses);
|
|
306
338
|
}
|
|
307
|
-
if (filter.subtype) {
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
351
|
for (const label of filter.labels ?? []) {
|
|
311
352
|
conditions.push("EXISTS (SELECT 1 FROM json_each(artifacts.labels) WHERE value = ?)");
|
|
312
353
|
params.push(label);
|
|
@@ -316,7 +357,7 @@ export function queryArtifacts(db: Db, filter: ArtifactQuery): Artifact[] {
|
|
|
316
357
|
conditions.push("json_extract(extra, ?) = ?");
|
|
317
358
|
params.push(`$.${key}`, value);
|
|
318
359
|
}
|
|
319
|
-
if (conditions.length) sql +=
|
|
360
|
+
if (conditions.length) sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
320
361
|
sql += " ORDER BY updated_at DESC";
|
|
321
362
|
if (filter.limit !== undefined) {
|
|
322
363
|
if (!Number.isInteger(filter.limit) || filter.limit < 1) throw new Error("artifact query limit must be a positive integer");
|
|
@@ -329,20 +370,25 @@ export function queryArtifacts(db: Db, filter: ArtifactQuery): Artifact[] {
|
|
|
329
370
|
|
|
330
371
|
function rowToTrashRecord(row: Record<string, unknown>): ArtifactTrashRecord {
|
|
331
372
|
return {
|
|
332
|
-
artifactId: row
|
|
333
|
-
trashedAt: row
|
|
334
|
-
purgeAfter: row
|
|
335
|
-
...(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 }),
|
|
336
377
|
};
|
|
337
378
|
}
|
|
338
379
|
|
|
339
380
|
export function getArtifactTrash(db: Db, id: string): ArtifactTrashRecord | null {
|
|
340
|
-
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;
|
|
341
385
|
return row ? rowToTrashRecord(row) : null;
|
|
342
386
|
}
|
|
343
387
|
|
|
344
388
|
export function listArtifactTrash(db: Db): ArtifactTrashRecord[] {
|
|
345
|
-
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>[];
|
|
346
392
|
return rows.map(rowToTrashRecord);
|
|
347
393
|
}
|
|
348
394
|
|
|
@@ -359,11 +405,16 @@ export function listArtifactTrash(db: Db): ArtifactTrashRecord[] {
|
|
|
359
405
|
* caller is not necessarily looking at right now. No other kind has an analogous "currently
|
|
360
406
|
* in use" signal to check.
|
|
361
407
|
*/
|
|
362
|
-
export function trashArtifact(
|
|
408
|
+
export function trashArtifact(
|
|
409
|
+
db: Db,
|
|
410
|
+
id: string,
|
|
411
|
+
options?: { reason?: string; now?: () => string; context?: ArtifactEventContext },
|
|
412
|
+
): ArtifactTrashRecord {
|
|
363
413
|
const artifact = getArtifact(db, id);
|
|
364
414
|
if (!artifact) throw new Error(`artifact "${id}" not found`);
|
|
365
415
|
const focusedScope = db.prepare("SELECT scope FROM task_focus WHERE task_id = ? LIMIT 1").get(id) as { scope: string } | null;
|
|
366
|
-
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`);
|
|
367
418
|
const now = options?.now ?? (() => new Date().toISOString());
|
|
368
419
|
const trashedAt = now();
|
|
369
420
|
const purgeAfter = new Date(new Date(trashedAt).getTime() + ARTIFACT_TRASH_RETENTION_MS).toISOString();
|
|
@@ -409,7 +460,9 @@ export function restoreArtifact(db: Db, id: string, context?: ArtifactEventConte
|
|
|
409
460
|
*/
|
|
410
461
|
export function purgeDueArtifacts(db: Db, now: () => string = () => new Date().toISOString()): number {
|
|
411
462
|
const nowIso = now();
|
|
412
|
-
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);
|
|
413
466
|
let purged = 0;
|
|
414
467
|
for (const id of due) {
|
|
415
468
|
inTransaction(db, () => {
|
|
@@ -502,7 +555,10 @@ export function updateExtra(db: Db, id: string, extra: Record<string, unknown>,
|
|
|
502
555
|
|
|
503
556
|
/** Active rules with inject metadata — for before_agent_start system prompt injection. */
|
|
504
557
|
export function injectableRules(db: Db): Array<{ id: string; title: string; body: string; extra: Record<string, unknown> }> {
|
|
505
|
-
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
|
+
>[];
|
|
506
562
|
return rows.map((row) => {
|
|
507
563
|
const art = rowToArtifact(row);
|
|
508
564
|
return { id: art.id, title: art.title, body: art.body, extra: art.extra };
|
|
@@ -538,15 +594,19 @@ function runProcessGateSync(gate: Gate, cwd?: string): GateResult {
|
|
|
538
594
|
if (result.error) return { gate, passed: false, output: result.error.message.slice(0, GATE_OUTPUT_LIMIT) };
|
|
539
595
|
const combined = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
540
596
|
const passed = result.status === 0 && (gate.expect ? combined.includes(gate.expect) : true);
|
|
541
|
-
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
|
+
};
|
|
542
602
|
}
|
|
543
603
|
|
|
544
604
|
export function runGates(db: Db, artifactId: string, options: GateRunOptions = {}): GateResult[] {
|
|
545
605
|
const art = getArtifact(db, artifactId);
|
|
546
606
|
if (!art) throw new Error("artifact not found");
|
|
547
|
-
const gates = (art.extra
|
|
607
|
+
const gates = (art.extra.gates as Gate[]) ?? [];
|
|
548
608
|
const cwd = options.cwd;
|
|
549
|
-
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)));
|
|
550
610
|
}
|
|
551
611
|
|
|
552
612
|
/**
|
|
@@ -560,7 +620,11 @@ export function runGates(db: Db, artifactId: string, options: GateRunOptions = {
|
|
|
560
620
|
* indefinitely after Papyrus considers the gate "timed out". Spawning detached (its own
|
|
561
621
|
* process group) and killing the negated pid on our own timer reaches the whole tree.
|
|
562
622
|
*/
|
|
563
|
-
function executeGateCommand(
|
|
623
|
+
function executeGateCommand(
|
|
624
|
+
command: string,
|
|
625
|
+
timeout: number,
|
|
626
|
+
cwd?: string,
|
|
627
|
+
): Promise<{ passed: boolean; output: string; matchable: string }> {
|
|
564
628
|
// `spawn(..., { shell: true, detached: true })` instead of the `exec()` convenience wrapper:
|
|
565
629
|
// `detached` (needed to make the shell the leader of its own process group, so the negated pid
|
|
566
630
|
// below reaches every descendant, not just the shell) is not part of Node's `exec()`/
|
|
@@ -636,7 +700,7 @@ function runNonProcessGate(gate: Gate): GateResult {
|
|
|
636
700
|
export async function runGatesAsync(db: Db, artifactId: string, options: GateRunOptions = {}): Promise<GateResult[]> {
|
|
637
701
|
const art = getArtifact(db, artifactId);
|
|
638
702
|
if (!art) throw new Error("artifact not found");
|
|
639
|
-
const gates = (art.extra
|
|
703
|
+
const gates = (art.extra.gates as Gate[]) ?? [];
|
|
640
704
|
const results: GateResult[] = [];
|
|
641
705
|
for (const gate of gates) {
|
|
642
706
|
const remainingMs = options.deadlineMs === undefined ? undefined : options.deadlineMs - Date.now();
|
|
@@ -659,4 +723,3 @@ export async function runGatesAsync(db: Db, artifactId: string, options: GateRun
|
|
|
659
723
|
}
|
|
660
724
|
return results;
|
|
661
725
|
}
|
|
662
|
-
|
|
@@ -24,7 +24,14 @@ import {
|
|
|
24
24
|
PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS,
|
|
25
25
|
} from "./constants.ts";
|
|
26
26
|
import type { Artifact } from "./domain/artifact.ts";
|
|
27
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
BlueprintDefinition,
|
|
29
|
+
BlueprintInputDefinition,
|
|
30
|
+
CallBlueprint,
|
|
31
|
+
DocBlueprint,
|
|
32
|
+
RuleBlueprint,
|
|
33
|
+
TaskBlueprint,
|
|
34
|
+
} from "./domain/blueprint-definition.ts";
|
|
28
35
|
import { validateBlueprintDefinition } from "./domain/blueprint-definition.ts";
|
|
29
36
|
import type { PlaybookArgument, PlaybookStep } from "./domain-services.ts";
|
|
30
37
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
@@ -60,20 +67,20 @@ function requirePlaybook(artifacts: ArtifactStore, id: string): Artifact {
|
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
function stepsOf(playbook: Artifact): PlaybookStep[] {
|
|
63
|
-
return Array.isArray(playbook.extra
|
|
70
|
+
return Array.isArray(playbook.extra.steps) ? (playbook.extra.steps as PlaybookStep[]) : [];
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
function toolsOf(playbook: Artifact): string[] {
|
|
67
|
-
return Array.isArray(playbook.extra
|
|
74
|
+
return Array.isArray(playbook.extra.tools) ? playbook.extra.tools.filter((tool): tool is string => typeof tool === "string") : [];
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
function argumentsOf(playbook: Artifact): PlaybookArgument[] {
|
|
71
|
-
return Array.isArray(playbook.extra
|
|
78
|
+
return Array.isArray(playbook.extra.arguments) ? (playbook.extra.arguments as PlaybookArgument[]) : [];
|
|
72
79
|
}
|
|
73
80
|
|
|
74
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). */
|
|
75
82
|
function rootTaskBody(playbook: Artifact): string {
|
|
76
|
-
const trigger = typeof playbook.extra
|
|
83
|
+
const trigger = typeof playbook.extra.trigger === "string" ? playbook.extra.trigger : "manual invocation";
|
|
77
84
|
const tools = toolsOf(playbook);
|
|
78
85
|
return [
|
|
79
86
|
`Playbook "${playbook.title}".`,
|
|
@@ -111,7 +118,9 @@ interface CompileNodeResult {
|
|
|
111
118
|
function mergeArgument(inputs: Record<string, BlueprintInputDefinition>, argument: PlaybookArgument): void {
|
|
112
119
|
const existing = inputs[argument.name];
|
|
113
120
|
if (existing && existing.type !== argument.type) {
|
|
114
|
-
throw new Error(
|
|
121
|
+
throw new Error(
|
|
122
|
+
`playbook composition declares conflicting types for argument "${argument.name}" (${existing.type} vs ${argument.type})`,
|
|
123
|
+
);
|
|
115
124
|
}
|
|
116
125
|
inputs[argument.name] = {
|
|
117
126
|
type: argument.type,
|
|
@@ -131,28 +140,44 @@ function compileNode(
|
|
|
131
140
|
incomingPrecedingRefs: string[],
|
|
132
141
|
): CompileNodeResult {
|
|
133
142
|
if (ancestorIds.has(playbookId)) throw new Error(`playbook composition cycle includes "${playbookId}"`);
|
|
134
|
-
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`);
|
|
135
145
|
const nextAncestors = new Set([...ancestorIds, playbookId]);
|
|
136
146
|
|
|
137
147
|
const playbook = requirePlaybook(artifacts, playbookId);
|
|
138
148
|
for (const argument of argumentsOf(playbook)) mergeArgument(ctx.inputs, argument);
|
|
139
149
|
|
|
140
150
|
const rootRef = `pb${ctx.refCounter.n++}`;
|
|
141
|
-
const rootBlueprint: TaskBlueprint = {
|
|
151
|
+
const rootBlueprint: TaskBlueprint = {
|
|
152
|
+
ref: rootRef,
|
|
153
|
+
title: playbook.title,
|
|
154
|
+
body: rootTaskBody(playbook),
|
|
155
|
+
...(parentRef ? { parent: parentRef } : {}),
|
|
156
|
+
};
|
|
142
157
|
ctx.tasks.push(rootBlueprint);
|
|
143
|
-
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`);
|
|
144
160
|
|
|
145
161
|
const edges = artifacts.relationships({ artifactIds: [playbookId] }).slice(0, PLAYBOOK_INVOCATION_MAX_LINKED_ARTIFACTS);
|
|
146
|
-
const composablePlaybookIds = nonTrashedPlaybookIds(
|
|
147
|
-
|
|
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)
|
|
148
169
|
.filter((id) => composablePlaybookIds.has(id));
|
|
149
|
-
const nestedIds = edges
|
|
170
|
+
const nestedIds = edges
|
|
171
|
+
.filter((edge) => edge.from === playbookId && edge.relation === "contains")
|
|
172
|
+
.map((edge) => edge.to)
|
|
150
173
|
.filter((id) => composablePlaybookIds.has(id));
|
|
151
174
|
for (const edge of edges) {
|
|
152
|
-
const isComposingFrom =
|
|
175
|
+
const isComposingFrom =
|
|
176
|
+
edge.from === playbookId && (edge.relation === "contains" || edge.relation === "depends_on") && composablePlaybookIds.has(edge.to);
|
|
153
177
|
if (isComposingFrom) continue;
|
|
154
178
|
if (edge.from === playbookId) ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.to, ownerIsFrom: true });
|
|
155
|
-
else if (edge.to === playbookId)
|
|
179
|
+
else if (edge.to === playbookId)
|
|
180
|
+
ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.from, ownerIsFrom: false });
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
const prerequisiteTailRefs: string[] = [];
|
|
@@ -175,13 +200,20 @@ function compileNode(
|
|
|
175
200
|
const title = typeof step === "string" ? stepTitle(step) : (step.title ?? stepTitle(body));
|
|
176
201
|
const stepRef = `${rootRef}-s${index}`;
|
|
177
202
|
ctx.tasks.push({ ref: stepRef, title, body, parent: rootRef, dependsOn: cursorPrecedingRefs });
|
|
178
|
-
if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS)
|
|
203
|
+
if (ctx.tasks.length > PLAYBOOK_INVOCATION_MAX_CREATED_TASKS)
|
|
204
|
+
throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} tasks`);
|
|
179
205
|
if (headRef === undefined) headRef = stepRef;
|
|
180
206
|
cursorPrecedingRefs = [stepRef];
|
|
181
207
|
tailRef = stepRef;
|
|
182
208
|
} else if (step.kind === "doc") {
|
|
183
209
|
const stepRef = `${rootRef}-d${index}`;
|
|
184
|
-
ctx.docs.push({
|
|
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
|
+
});
|
|
185
217
|
} else if (step.kind === "rule") {
|
|
186
218
|
const stepRef = `${rootRef}-r${index}`;
|
|
187
219
|
ctx.rules.push({
|
|
@@ -198,7 +230,14 @@ function compileNode(
|
|
|
198
230
|
// a pipeline step -- shares the same dependsOn chain as a task step, resolved
|
|
199
231
|
// polymorphically at execution time by workflow-execution.ts based on the target's kind.
|
|
200
232
|
const stepRef = `${rootRef}-c${index}`;
|
|
201
|
-
ctx.skills.push({
|
|
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
|
+
});
|
|
202
241
|
if (headRef === undefined) headRef = stepRef;
|
|
203
242
|
cursorPrecedingRefs = [stepRef];
|
|
204
243
|
tailRef = stepRef;
|
|
@@ -11,8 +11,13 @@ import type { BlueprintArgumentValue } from "./domain/blueprint-definition.ts";
|
|
|
11
11
|
import { compilePlaybookDefinition } from "./playbook-definition.ts";
|
|
12
12
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
13
13
|
import { requireAtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
|
|
14
|
-
import { applyPlaybookExternalLinks, materializeWorkflowDefinition, resolveRefToTaskId, type WorkflowRunHistory } from "./workflow-execution.ts";
|
|
15
14
|
import type { TaskExecutionPlan } from "./task-execution.ts";
|
|
15
|
+
import {
|
|
16
|
+
applyPlaybookExternalLinks,
|
|
17
|
+
materializeWorkflowDefinition,
|
|
18
|
+
resolveRefToTaskId,
|
|
19
|
+
type WorkflowRunHistory,
|
|
20
|
+
} from "./workflow-execution.ts";
|
|
16
21
|
|
|
17
22
|
export interface InvokePlaybookInput {
|
|
18
23
|
runId?: string;
|
|
@@ -36,7 +41,10 @@ export interface PlaybookMissingArguments {
|
|
|
36
41
|
missingArguments: string[];
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
function missingRequiredInputs(
|
|
44
|
+
function missingRequiredInputs(
|
|
45
|
+
inputs: Record<string, { required?: boolean; default?: BlueprintArgumentValue }>,
|
|
46
|
+
provided: Record<string, unknown>,
|
|
47
|
+
): string[] {
|
|
40
48
|
return Object.entries(inputs)
|
|
41
49
|
.filter(([name, input]) => input.required && provided[name] === undefined && input.default === undefined)
|
|
42
50
|
.map(([name]) => name);
|
|
@@ -52,7 +60,9 @@ export function invokePlaybook(
|
|
|
52
60
|
const missingArguments = missingRequiredInputs(compiled.definition.inputs, input.arguments ?? {});
|
|
53
61
|
if (missingArguments.length > 0) return { playbookId, missingArguments };
|
|
54
62
|
|
|
55
|
-
const atomic = history
|
|
63
|
+
const atomic = history
|
|
64
|
+
? history.events.atomic.bind(history.events)
|
|
65
|
+
: requireAtomicArtifactStore(artifacts).atomic.bind(requireAtomicArtifactStore(artifacts));
|
|
56
66
|
return atomic(() => {
|
|
57
67
|
const result = materializeWorkflowDefinition(
|
|
58
68
|
artifacts,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
normalizeNoteHistoryQuery,
|
|
3
|
-
validateNoteEvent,
|
|
4
2
|
type AppendNoteEvent,
|
|
5
3
|
type NoteEvent,
|
|
6
4
|
type NoteHistoryPage,
|
|
7
5
|
type NoteHistoryQuery,
|
|
6
|
+
normalizeNoteHistoryQuery,
|
|
7
|
+
validateNoteEvent,
|
|
8
8
|
} from "../domain/note-event.ts";
|
|
9
9
|
|
|
10
10
|
export interface NoteEventStore {
|
|
@@ -30,8 +30,10 @@ export class InMemoryNoteEventStore implements NoteEventStore {
|
|
|
30
30
|
history(noteId: string, query: NoteHistoryQuery = {}): NoteHistoryPage {
|
|
31
31
|
const { direction, limit, cursor } = normalizeNoteHistoryQuery(query);
|
|
32
32
|
const ordered = this.events
|
|
33
|
-
.filter(
|
|
34
|
-
|
|
33
|
+
.filter(
|
|
34
|
+
(event) => event.noteId === noteId && (cursor === undefined || (direction === "desc" ? event.id < cursor : event.id > cursor)),
|
|
35
|
+
)
|
|
36
|
+
.sort((left, right) => (direction === "desc" ? right.id - left.id : left.id - right.id));
|
|
35
37
|
const events = ordered.slice(0, limit);
|
|
36
38
|
return { events, ...(ordered.length > limit ? { nextCursor: events.at(-1)!.id } : {}) };
|
|
37
39
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AppendTaskEvent,
|
|
2
3
|
normalizeTaskEventFeedQuery,
|
|
3
4
|
normalizeTaskHistoryQuery,
|
|
4
|
-
validateTaskEvent,
|
|
5
|
-
type AppendTaskEvent,
|
|
6
5
|
type TaskEvent,
|
|
7
6
|
type TaskEventFeedPage,
|
|
8
7
|
type TaskEventFeedQuery,
|
|
9
8
|
type TaskHistoryPage,
|
|
10
9
|
type TaskHistoryQuery,
|
|
10
|
+
validateTaskEvent,
|
|
11
11
|
} from "../domain/task-event.ts";
|
|
12
12
|
|
|
13
13
|
export interface TaskEventStore {
|
|
@@ -25,8 +25,9 @@ export class InMemoryTaskEventStore implements TaskEventStore {
|
|
|
25
25
|
atomic<T>(operation: () => T): T {
|
|
26
26
|
const length = this.events.length;
|
|
27
27
|
const nextId = this.nextId;
|
|
28
|
-
try {
|
|
29
|
-
|
|
28
|
+
try {
|
|
29
|
+
return operation();
|
|
30
|
+
} catch (error) {
|
|
30
31
|
this.events.length = length;
|
|
31
32
|
this.nextId = nextId;
|
|
32
33
|
throw error;
|
|
@@ -47,8 +48,10 @@ export class InMemoryTaskEventStore implements TaskEventStore {
|
|
|
47
48
|
history(taskId: string, query: TaskHistoryQuery = {}): TaskHistoryPage {
|
|
48
49
|
const { direction, limit, cursor } = normalizeTaskHistoryQuery(query);
|
|
49
50
|
const ordered = this.events
|
|
50
|
-
.filter(
|
|
51
|
-
|
|
51
|
+
.filter(
|
|
52
|
+
(event) => event.taskId === taskId && (cursor === undefined || (direction === "desc" ? event.id < cursor : event.id > cursor)),
|
|
53
|
+
)
|
|
54
|
+
.sort((left, right) => (direction === "desc" ? right.id - left.id : left.id - right.id));
|
|
52
55
|
const events = ordered.slice(0, limit);
|
|
53
56
|
return { events, ...(ordered.length > limit ? { nextCursor: events.at(-1)!.id } : {}) };
|
|
54
57
|
}
|