@danypops/papyrus 0.42.0 → 0.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  3. package/src/adapters/sqlite-artifact-store.ts +7 -5
  4. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  5. package/src/adapters/sqlite-gate-runner.ts +1 -1
  6. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  7. package/src/adapters/sqlite-log-store.ts +36 -17
  8. package/src/adapters/sqlite-note-event-store.ts +20 -16
  9. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  10. package/src/adapters/sqlite-task-event-store.ts +29 -21
  11. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  12. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  13. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  14. package/src/artifact-relationship-view.ts +3 -3
  15. package/src/artifact-subtree.ts +4 -2
  16. package/src/authority-registry.ts +2 -1
  17. package/src/cli.ts +785 -179
  18. package/src/client.ts +46 -20
  19. package/src/constants.ts +15 -4
  20. package/src/daemon-state.ts +4 -12
  21. package/src/daemon.ts +31 -9
  22. package/src/db.ts +119 -98
  23. package/src/discussion-service.ts +109 -44
  24. package/src/domain/artifact-event.ts +17 -4
  25. package/src/domain/artifact.ts +3 -1
  26. package/src/domain/blueprint-definition.ts +26 -31
  27. package/src/domain/checklist.ts +20 -17
  28. package/src/domain/discussion.ts +37 -18
  29. package/src/domain/gate.ts +7 -7
  30. package/src/domain/log-entry.ts +1 -1
  31. package/src/domain/note-event.ts +20 -7
  32. package/src/domain/task-event.ts +17 -7
  33. package/src/domain-services.ts +241 -110
  34. package/src/graph-projection-service.ts +34 -8
  35. package/src/id-migration.ts +17 -4
  36. package/src/index.ts +16 -11
  37. package/src/log-service.ts +6 -5
  38. package/src/log.ts +19 -0
  39. package/src/modules/discuss.ts +63 -28
  40. package/src/modules/docs.ts +74 -17
  41. package/src/modules/graph-projection.ts +20 -9
  42. package/src/modules/logs.ts +33 -21
  43. package/src/modules/notes.ts +66 -28
  44. package/src/modules/playbooks.ts +88 -29
  45. package/src/modules/rules.ts +57 -15
  46. package/src/modules/session-identity.ts +6 -2
  47. package/src/modules/tasks.ts +142 -67
  48. package/src/note-service.ts +11 -7
  49. package/src/ops.ts +131 -68
  50. package/src/playbook-definition.ts +56 -17
  51. package/src/playbook-execution.ts +13 -3
  52. package/src/ports/note-event-store.ts +6 -4
  53. package/src/ports/task-event-store.ts +9 -6
  54. package/src/ports/task-focus-store.ts +17 -4
  55. package/src/ports/task-lease-store.ts +9 -4
  56. package/src/ports/task-scope-store.ts +3 -1
  57. package/src/service.ts +143 -89
  58. package/src/session-identity-service.ts +10 -2
  59. package/src/task-context.ts +28 -16
  60. package/src/task-execution.ts +4 -12
  61. package/src/task-graph-view.ts +12 -12
  62. package/src/task-relationship-view.ts +1 -3
  63. package/src/task-service.ts +167 -72
  64. package/src/vehicle/artifact-trash-vehicle.ts +25 -13
  65. package/src/vehicle/artifact-vehicle-shared.ts +28 -7
  66. package/src/vehicle/docs-vehicle.ts +48 -16
  67. package/src/vehicle/notes-vehicle.ts +24 -6
  68. package/src/vehicle/papyrus-vehicle.ts +14 -3
  69. package/src/vehicle/playbooks-vehicle.ts +87 -18
  70. package/src/vehicle/rules-vehicle.ts +58 -21
  71. package/src/vehicle/tasks-vehicle.ts +366 -54
  72. package/src/version.ts +1 -1
  73. package/src/workflow-execution.ts +71 -52
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
- import type { Gate, GateResult, GateRunOptions } from "./domain/gate.ts";
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["targetKind"];
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["defaults"]) ? template.extra["defaults"] : {};
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["required"])
98
- ? template.extra["required"].filter((field): field is string => typeof field === "string")
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["id"] as string,
122
- kind: row["kind"] as string,
123
- title: row["title"] as string,
124
- status: row["status"] as string,
125
- subtype: (row["subtype"] as string) ?? "",
126
- body: (row["body"] as string) ?? "",
127
- labels: JSON.parse((row["labels"] as string) ?? "[]"),
128
- extra: JSON.parse((row["extra"] as string) ?? "{}"),
129
- created_at: row["created_at"] as string,
130
- updated_at: row["updated_at"] as string,
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.prepare(`
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
- `).run(
152
- event.artifactId,
153
- now,
154
- event.type,
155
- event.actor,
156
- event.source,
157
- event.sessionId ?? null,
158
- event.fromStatus ?? null,
159
- event.toStatus ?? null,
160
- event.relation ?? null,
161
- event.relatedId ?? null,
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) { conditions.push("(artifact_id = ? OR related_id = ?)"); params.push(artifactId, artifactId); }
219
- if (actor) { conditions.push("actor = ?"); params.push(actor); }
220
- if (sessionId) { conditions.push("session_id = ?"); params.push(sessionId); }
221
- if (since) { conditions.push("occurred_at >= ?"); params.push(since); }
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) { conditions.push(`id ${comparator} ?`); params.push(cursor); }
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.prepare(`
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
- `).all(...params, limit + 1) as ArtifactEventRow[];
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 { from: string; relation: string; to: string }[];
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) { conditions.push("kind = ?"); params.push(filter.kind); }
301
- if (filter.status) { conditions.push("status = ?"); params.push(filter.status); }
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) { conditions.push("subtype = ?"); params.push(filter.subtype); }
308
- if (filter.excludeSubtype) { conditions.push("subtype != ?"); params.push(filter.excludeSubtype); }
309
- if (filter.text) { conditions.push("(title LIKE ? OR body LIKE ?)"); params.push(`%${filter.text}%`, `%${filter.text}%`); }
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 += " WHERE " + conditions.join(" AND ");
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["artifact_id"] as string,
333
- trashedAt: row["trashed_at"] as string,
334
- purgeAfter: row["purge_after"] as string,
335
- ...(row["reason"] == null ? {} : { reason: row["reason"] as string }),
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<string, unknown> | null;
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.prepare("SELECT artifact_id, trashed_at, purge_after, reason FROM artifact_trash ORDER BY purge_after ASC").all() as Record<string, unknown>[];
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(db: Db, id: string, options?: { reason?: string; now?: () => string; context?: ArtifactEventContext }): ArtifactTrashRecord {
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) throw new Error(`artifact "${id}" is the active Task Focus in scope "${focusedScope.scope}"; clear focus before removing it`);
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 = (db.prepare("SELECT artifact_id FROM artifact_trash WHERE purge_after <= ?").all(nowIso) as Array<{ artifact_id: string }>).map((row) => row.artifact_id);
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<string, unknown>[];
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 { gate, passed, output: combined.slice(0, GATE_OUTPUT_LIMIT) || (result.status === 0 ? "ok" : `command exited with code ${result.status}`) };
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["gates"] as Gate[]) ?? [];
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") ? runProcessGateSync(gate, cwd) : runNonProcessGate(gate));
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(command: string, timeout: number, cwd?: string): Promise<{ passed: boolean; output: string; matchable: string }> {
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["gates"] as Gate[]) ?? [];
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 { CallBlueprint, BlueprintDefinition, DocBlueprint, BlueprintInputDefinition, RuleBlueprint, TaskBlueprint } from "./domain/blueprint-definition.ts";
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["steps"]) ? (playbook.extra["steps"] as PlaybookStep[]) : [];
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["tools"]) ? playbook.extra["tools"].filter((tool): tool is string => typeof tool === "string") : [];
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["arguments"]) ? (playbook.extra["arguments"] as PlaybookArgument[]) : [];
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["trigger"] === "string" ? playbook.extra["trigger"] : "manual invocation";
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(`playbook composition declares conflicting types for argument "${argument.name}" (${existing.type} vs ${argument.type})`);
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) throw new Error(`playbook composition exceeds ${PLAYBOOK_INVOCATION_MAX_CALL_DEPTH} levels`);
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 = { ref: rootRef, title: playbook.title, body: rootTaskBody(playbook), ...(parentRef ? { parent: parentRef } : {}) };
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) throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} 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(artifacts, edges.filter((edge) => edge.from === playbookId).map((edge) => edge.to));
147
- const prerequisiteIds = edges.filter((edge) => edge.from === playbookId && edge.relation === "depends_on").map((edge) => edge.to)
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.filter((edge) => edge.from === playbookId && edge.relation === "contains").map((edge) => edge.to)
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 = edge.from === playbookId && (edge.relation === "contains" || edge.relation === "depends_on") && composablePlaybookIds.has(edge.to);
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) ctx.externalLinks.push({ rootRef, relation: edge.relation, otherArtifactId: edge.from, ownerIsFrom: false });
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) throw new Error(`playbook invocation exceeds ${PLAYBOOK_INVOCATION_MAX_CREATED_TASKS} 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({ ref: stepRef, title: step.title, ...(step.body ? { body: step.body } : {}), ...(step.subtype ? { subtype: step.subtype } : {}), ...(step.labels ? { labels: step.labels } : {}) });
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({ ref: stepRef, title: step.title, targetId: step.playbookId, ...(step.arguments ? { arguments: step.arguments } : {}), parent: rootRef, dependsOn: cursorPrecedingRefs });
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(inputs: Record<string, { required?: boolean; default?: BlueprintArgumentValue }>, provided: Record<string, unknown>): string[] {
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 ? history.events.atomic.bind(history.events) : requireAtomicArtifactStore(artifacts).atomic.bind(requireAtomicArtifactStore(artifacts));
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((event) => event.noteId === noteId && (cursor === undefined || (direction === "desc" ? event.id < cursor : event.id > cursor)))
34
- .sort((left, right) => direction === "desc" ? right.id - left.id : left.id - right.id);
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 { return operation(); }
29
- catch (error) {
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((event) => event.taskId === taskId && (cursor === undefined || (direction === "desc" ? event.id < cursor : event.id > cursor)))
51
- .sort((left, right) => direction === "desc" ? right.id - left.id : left.id - right.id);
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
  }