@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
@@ -4,24 +4,38 @@ import {
4
4
  TASK_EXECUTION_MAX_DEGREE,
5
5
  TASK_EXECUTION_MAX_EDGES,
6
6
  TASK_EXECUTION_MAX_NODES,
7
+ TASK_FOCUS_STALE_AFTER_MS,
7
8
  TASK_LABEL_MAX_COUNT,
8
9
  TASK_LABEL_MAX_LENGTH,
9
- TASK_FOCUS_STALE_AFTER_MS,
10
10
  TASK_SCOPE_MAX_TASKS,
11
11
  TASK_TITLE_MAX_LENGTH,
12
12
  } from "./constants.ts";
13
13
  import type { Artifact } from "./domain/artifact.ts";
14
- import { checklistEntries, validateChecklist, type Checklist, type ProofReference } from "./domain/checklist.ts";
14
+ import { type Checklist, checklistEntries, type ProofReference, validateChecklist } from "./domain/checklist.ts";
15
15
  import { DISCUSSION_SUBTYPE, isDiscussionArtifact, readDiscussionExtra } from "./domain/discussion.ts";
16
- import { validateGates, type Gate, type GateResult } from "./domain/gate.ts";
17
- import type { AppendTaskEvent, TaskEventContext, TaskEventFeedPage, TaskEventFeedQuery, TaskHistoryPage, TaskHistoryQuery, TaskLifecycleStatus } from "./domain/task-event.ts";
18
- import { normalizeProjectRoot, taskScopeLabel, type TaskScopeSource, type TaskViewMode, type TaskViewSelection } from "./domain/task-scope.ts";
16
+ import { type Gate, type GateResult, validateGates } from "./domain/gate.ts";
17
+ import type {
18
+ AppendTaskEvent,
19
+ TaskEventContext,
20
+ TaskEventFeedPage,
21
+ TaskEventFeedQuery,
22
+ TaskHistoryPage,
23
+ TaskHistoryQuery,
24
+ TaskLifecycleStatus,
25
+ } from "./domain/task-event.ts";
26
+ import type { TaskLease } from "./domain/task-lease.ts";
27
+ import {
28
+ normalizeProjectRoot,
29
+ type TaskScopeSource,
30
+ type TaskViewMode,
31
+ type TaskViewSelection,
32
+ taskScopeLabel,
33
+ } from "./domain/task-scope.ts";
19
34
  import type { ArtifactStore } from "./ports/artifact-store.ts";
20
35
  import type { GateRunner } from "./ports/gate-runner.ts";
21
- import { InMemoryTaskFocusStore, type TaskFocusStatus, type TaskFocusStore } from "./ports/task-focus-store.ts";
22
36
  import { InMemoryTaskEventStore, type TaskEventStore } from "./ports/task-event-store.ts";
37
+ import { InMemoryTaskFocusStore, type TaskFocusStatus, type TaskFocusStore } from "./ports/task-focus-store.ts";
23
38
  import { InMemoryTaskLeaseStore, type TaskLeaseStore } from "./ports/task-lease-store.ts";
24
- import type { TaskLease } from "./domain/task-lease.ts";
25
39
  import { InMemoryTaskScopeStore, type TaskScopeStore } from "./ports/task-scope-store.ts";
26
40
  import { assertDependencyEdgeAllowed } from "./task-execution.ts";
27
41
 
@@ -147,8 +161,8 @@ export class Tasks {
147
161
  if (input.parentId) this.require(input.parentId);
148
162
  for (const dependency of input.dependsOn ?? []) this.require(dependency);
149
163
  const extra: Record<string, unknown> = { ...(input.extra ?? {}) };
150
- if (input.gates !== undefined) extra["gates"] = validateGates(input.gates);
151
- if (input.checklist !== undefined) extra["checklist"] = validateChecklist(input.checklist);
164
+ if (input.gates !== undefined) extra.gates = validateGates(input.gates);
165
+ if (input.checklist !== undefined) extra.checklist = validateChecklist(input.checklist);
152
166
  const projectRoot = input.projectRoot === undefined ? undefined : normalizeProjectRoot(input.projectRoot);
153
167
  if (input.parentId && this.scopes.get(input.parentId)?.projectRoot !== projectRoot) {
154
168
  throw new Error(`parent task "${input.parentId}" is outside project scope`);
@@ -179,18 +193,26 @@ export class Tasks {
179
193
  if (task.status !== "done" && task.status !== "canceled") throw new Error(`cannot recover task creation from ${task.status}`);
180
194
  const history = this.events.history(id, { direction: "asc", limit: 2 });
181
195
  const created = history.events[0];
182
- if (history.events.length !== 1 || history.nextCursor !== undefined || created?.type !== "created" || created.toStatus !== task.status) {
196
+ if (
197
+ history.events.length !== 1 ||
198
+ history.nextCursor !== undefined ||
199
+ created?.type !== "created" ||
200
+ created.toStatus !== task.status
201
+ ) {
183
202
  throw new Error("task was not terminal at creation");
184
203
  }
185
204
  const recovered = this.artifacts.setStatus(id, "todo");
186
205
  if (!recovered) throw new Error(`task "${id}" not found`);
187
- this.appendEvent({
188
- taskId: id,
189
- type: "creation_recovered",
190
- fromStatus: task.status as TaskStatus,
191
- toStatus: "todo",
192
- evidence: { result: "terminal-at-creation" },
193
- }, context);
206
+ this.appendEvent(
207
+ {
208
+ taskId: id,
209
+ type: "creation_recovered",
210
+ fromStatus: task.status as TaskStatus,
211
+ toStatus: "todo",
212
+ evidence: { result: "terminal-at-creation" },
213
+ },
214
+ context,
215
+ );
194
216
  return recovered;
195
217
  });
196
218
  }
@@ -204,11 +226,13 @@ export class Tasks {
204
226
  return this.recoverCreation(id, context);
205
227
  }
206
228
  const fields = (["title", "body", "labels"] as const).filter((field) => input[field] !== undefined);
207
- if (fields.length === 0) throw new Error("task update requires title, body, or labels; status todo is only valid for creation recovery");
229
+ if (fields.length === 0)
230
+ throw new Error("task update requires title, body, or labels; status todo is only valid for creation recovery");
208
231
  if (input.title !== undefined && (input.title.trim().length === 0 || input.title.length > TASK_TITLE_MAX_LENGTH)) {
209
232
  throw new Error(`title must be between 1 and ${TASK_TITLE_MAX_LENGTH} characters`);
210
233
  }
211
- if (input.body !== undefined && input.body.length > TASK_BODY_MAX_LENGTH) throw new Error(`body cannot exceed ${TASK_BODY_MAX_LENGTH} characters`);
234
+ if (input.body !== undefined && input.body.length > TASK_BODY_MAX_LENGTH)
235
+ throw new Error(`body cannot exceed ${TASK_BODY_MAX_LENGTH} characters`);
212
236
  if (input.labels !== undefined) {
213
237
  if (input.labels.length > TASK_LABEL_MAX_COUNT) throw new Error(`labels cannot exceed ${TASK_LABEL_MAX_COUNT} entries`);
214
238
  if (input.labels.some((label) => label.length === 0 || label.length > TASK_LABEL_MAX_LENGTH)) {
@@ -231,7 +255,14 @@ export class Tasks {
231
255
  throw new Error(`task list limit must be between 1 and ${TASK_SCOPE_MAX_TASKS + 1}`);
232
256
  }
233
257
  if (selection.mode === "all") {
234
- return this.artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE, status: filter.status, text: filter.text, labels: filter.labels, limit });
258
+ return this.artifacts.query({
259
+ kind: "task",
260
+ excludeSubtype: DISCUSSION_SUBTYPE,
261
+ status: filter.status,
262
+ text: filter.text,
263
+ labels: filter.labels,
264
+ limit,
265
+ });
235
266
  }
236
267
  const ids = this.scopes.taskIds(selection.projectRoot, TASK_SCOPE_MAX_TASKS + 1);
237
268
  if (ids.length > TASK_SCOPE_MAX_TASKS) throw new Error(`task project scope exceeds ${TASK_SCOPE_MAX_TASKS} tasks`);
@@ -244,7 +275,9 @@ export class Tasks {
244
275
  // leak a trashed task back into list results for the entire grace period. Scoped by ids
245
276
  // rather than a bare kind query, so this stays bounded to exactly the already-bounded
246
277
  // selectedIds set instead of scanning every task in the database.
247
- const notTrashed = new Map(this.artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE, ids: [...selectedIds] }).map((task) => [task.id, task]));
278
+ const notTrashed = new Map(
279
+ this.artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE, ids: [...selectedIds] }).map((task) => [task.id, task]),
280
+ );
248
281
  return [...selectedIds]
249
282
  .map((id) => notTrashed.get(id))
250
283
  .filter((task): task is Artifact => task !== undefined)
@@ -256,7 +289,8 @@ export class Tasks {
256
289
  }
257
290
 
258
291
  scopeSelection(projectRoot?: string, mode?: TaskViewMode, rootTaskId?: string): TaskViewSelection {
259
- if (mode !== undefined && mode !== "project" && mode !== "graph" && mode !== "all") throw new Error("task scope must be project, graph, or all");
292
+ if (mode !== undefined && mode !== "project" && mode !== "graph" && mode !== "all")
293
+ throw new Error("task scope must be project, graph, or all");
260
294
  if (projectRoot === undefined) return { mode: "all", label: taskScopeLabel("all") };
261
295
  const normalized = normalizeProjectRoot(projectRoot);
262
296
  const persisted = this.scopes.view(normalized);
@@ -302,14 +336,19 @@ export class Tasks {
302
336
  const focus = this.focusStore.get(filter.sessionId);
303
337
  const focusedId = focus?.taskId;
304
338
  const focusStatus = focus?.status;
305
- const nodes = new Map(tasks.map((task) => [task.id, {
306
- task,
307
- active: task.id === focusedId,
308
- ...(task.id === focusedId ? { focusStatus } : {}),
309
- parentIds: [] as string[],
310
- childIds: [] as string[],
311
- dependencyIds: [] as string[],
312
- }]));
339
+ const nodes = new Map(
340
+ tasks.map((task) => [
341
+ task.id,
342
+ {
343
+ task,
344
+ active: task.id === focusedId,
345
+ ...(task.id === focusedId ? { focusStatus } : {}),
346
+ parentIds: [] as string[],
347
+ childIds: [] as string[],
348
+ dependencyIds: [] as string[],
349
+ },
350
+ ]),
351
+ );
313
352
  const relationships = this.artifacts.relationships({
314
353
  kind: "task",
315
354
  artifactIds: [...byId.keys()],
@@ -349,12 +388,17 @@ export class Tasks {
349
388
  const focus = this.focusStore.get(filter?.sessionId);
350
389
  if (!focus) return null;
351
390
  const task = this.artifacts.get(focus.taskId);
352
- if (!task || task.kind !== "task" || task.status === "done" || task.status === "canceled") {
391
+ if (task?.kind !== "task" || task.status === "done" || task.status === "canceled") {
353
392
  this.focusStore.clear(focus.taskId, filter?.sessionId);
354
393
  return null;
355
394
  }
356
395
  if (filter?.projectRoot && !this.list(filter).some((candidate) => candidate.id === task.id)) return null;
357
- return { artifact: task, status: focus.status, updatedAt: focus.updatedAt, ...(focus.pauseReason ? { pauseReason: focus.pauseReason } : {}) };
396
+ return {
397
+ artifact: task,
398
+ status: focus.status,
399
+ updatedAt: focus.updatedAt,
400
+ ...(focus.pauseReason ? { pauseReason: focus.pauseReason } : {}),
401
+ };
358
402
  }
359
403
 
360
404
  active(filter?: TaskFilter): Artifact | null {
@@ -378,7 +422,12 @@ export class Tasks {
378
422
  if (!focus) throw new Error("no focused task");
379
423
  const state = this.focusStore.pause(focus.artifact.id, context.reason, context.sessionId);
380
424
  this.appendEvent({ taskId: focus.artifact.id, type: "focus_paused" }, context);
381
- return { artifact: focus.artifact, status: state.status, updatedAt: state.updatedAt, ...(state.pauseReason ? { pauseReason: state.pauseReason } : {}) };
425
+ return {
426
+ artifact: focus.artifact,
427
+ status: state.status,
428
+ updatedAt: state.updatedAt,
429
+ ...(state.pauseReason ? { pauseReason: state.pauseReason } : {}),
430
+ };
382
431
  });
383
432
  }
384
433
 
@@ -446,12 +495,19 @@ export class Tasks {
446
495
  const transition = TASK_TRANSITIONS[action];
447
496
  if (!transition.from.includes(task.status as TaskStatus)) throw new Error(`cannot ${action} task from ${task.status}`);
448
497
  if (action === "start") {
449
- const blocking = this.dependencyIds(id).map((dependencyId) => this.require(dependencyId)).filter((dependency) => dependency.status !== "done");
450
- if (blocking.length > 0) throw new Error(`task "${task.title}" is blocked by dependencies: ${blocking.map((dependency) => `"${dependency.title}"`).join(", ")}`);
498
+ const blocking = this.dependencyIds(id)
499
+ .map((dependencyId) => this.require(dependencyId))
500
+ .filter((dependency) => dependency.status !== "done");
501
+ if (blocking.length > 0)
502
+ throw new Error(
503
+ `task "${task.title}" is blocked by dependencies: ${blocking.map((dependency) => `"${dependency.title}"`).join(", ")}`,
504
+ );
451
505
  this.focusStore.set(id, context.sessionId);
452
506
  }
453
507
  const updated = this.artifacts.setStatus(id, transition.to)!;
454
- const eventType = { start: "started", submit: "submitted", reject: "review_rejected", retry: "retried", cancel: "canceled" }[action] as AppendTaskEvent["type"];
508
+ const eventType = { start: "started", submit: "submitted", reject: "review_rejected", retry: "retried", cancel: "canceled" }[
509
+ action
510
+ ] as AppendTaskEvent["type"];
455
511
  this.appendEvent({ taskId: id, type: eventType, fromStatus: task.status as TaskStatus, toStatus: transition.to }, context);
456
512
  if (action === "start" || action === "retry") this.propagateProgressToAncestors(id, context);
457
513
  if (action === "retry") this.focusStore.set(id, context.sessionId);
@@ -480,8 +536,9 @@ export class Tasks {
480
536
  visited.add(current);
481
537
  if (visited.size > TASK_CANCEL_SUBTREE_MAX_NODES) throw new Error(`cancelSubtree exceeds ${TASK_CANCEL_SUBTREE_MAX_NODES} tasks`);
482
538
  const task = this.artifacts.get(current);
483
- if (!task || task.kind !== "task") continue;
484
- const childIds = this.artifacts.relationships({ artifactIds: [current] })
539
+ if (task?.kind !== "task") continue;
540
+ const childIds = this.artifacts
541
+ .relationships({ artifactIds: [current] })
485
542
  .filter((edge) => edge.from === current && edge.relation === "contains")
486
543
  .map((edge) => edge.to);
487
544
  queue.push(...childIds);
@@ -499,7 +556,9 @@ export class Tasks {
499
556
  const task = this.requireReview(id);
500
557
  this.requireNotBlocked(task);
501
558
  const attemptId = crypto.randomUUID();
502
- this.events.atomic(() => this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context));
559
+ this.events.atomic(() =>
560
+ this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context),
561
+ );
503
562
  const checklist = this.reviewChecklist(task);
504
563
  const results = this.gates.run(id, { cwd: this.scopes.get(id)?.projectRoot });
505
564
  return this.resolveCompletion(id, attemptId, results, checklist, context, options);
@@ -509,7 +568,9 @@ export class Tasks {
509
568
  const task = this.requireReview(id);
510
569
  this.requireNotBlocked(task);
511
570
  const attemptId = crypto.randomUUID();
512
- this.events.atomic(() => this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context));
571
+ this.events.atomic(() =>
572
+ this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context),
573
+ );
513
574
  const checklist = this.reviewChecklist(task);
514
575
  // project_root, never the daemon's own inherited process cwd -- see GateRunOptions.cwd's doc
515
576
  // comment for the real incident this fixes (a command gate once tested the daemon's entire
@@ -522,7 +583,16 @@ export class Tasks {
522
583
  async runGates(id: string, context: TaskEventContext = {}): Promise<GateResult[]> {
523
584
  this.require(id);
524
585
  const results = await this.gates.runAsync(id, { cwd: this.scopes.get(id)?.projectRoot });
525
- this.events.atomic(() => this.appendEvent({ taskId: id, type: "gates_evaluated", evidence: { gates: results, result: results.every((gate) => gate.passed) ? "passed" : "failed" } }, context));
586
+ this.events.atomic(() =>
587
+ this.appendEvent(
588
+ {
589
+ taskId: id,
590
+ type: "gates_evaluated",
591
+ evidence: { gates: results, result: results.every((gate) => gate.passed) ? "passed" : "failed" },
592
+ },
593
+ context,
594
+ ),
595
+ );
526
596
  return results;
527
597
  }
528
598
 
@@ -593,7 +663,9 @@ export class Tasks {
593
663
  return this.events.atomic(() => {
594
664
  this.require(parentId);
595
665
  this.require(childId);
596
- const alreadyContained = this.relationships(parentId).some((edge) => edge.relation === "contains" && edge.from === parentId && edge.to === childId);
666
+ const alreadyContained = this.relationships(parentId).some(
667
+ (edge) => edge.relation === "contains" && edge.from === parentId && edge.to === childId,
668
+ );
597
669
  this.artifacts.link({ from: parentId, relation: "contains", to: childId }, context);
598
670
  this.artifacts.link({ from: childId, relation: "part_of", to: parentId }, context);
599
671
  if (!alreadyContained) this.appendEvent({ taskId: parentId, type: "containment_added", reason: context.reason }, context);
@@ -621,7 +693,8 @@ export class Tasks {
621
693
  artifactIds: projectTaskIds,
622
694
  limit: TASK_EXECUTION_MAX_EDGES + 1,
623
695
  });
624
- if (relationships.length > TASK_EXECUTION_MAX_EDGES) throw new Error(`task project scope exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
696
+ if (relationships.length > TASK_EXECUTION_MAX_EDGES)
697
+ throw new Error(`task project scope exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
625
698
  const children = new Map<string, string[]>();
626
699
  for (const edge of relationships) {
627
700
  const parentId = edge.relation === "contains" ? edge.from : edge.relation === "part_of" ? edge.to : undefined;
@@ -676,22 +749,25 @@ export class Tasks {
676
749
  const parent = this.require(parentId);
677
750
  if (parent.status === "todo") {
678
751
  this.artifacts.setStatus(parentId, "in-progress");
679
- this.appendEvent({ taskId: parentId, type: "started", fromStatus: "todo", toStatus: "in-progress" }, {
680
- ...context,
681
- source: "task-ancestry",
682
- reason: `nested task ${id} entered progress`,
683
- });
752
+ this.appendEvent(
753
+ { taskId: parentId, type: "started", fromStatus: "todo", toStatus: "in-progress" },
754
+ {
755
+ ...context,
756
+ source: "task-ancestry",
757
+ reason: `nested task ${id} entered progress`,
758
+ },
759
+ );
684
760
  }
685
761
  pending.push(...this.parentIds(parentId));
686
762
  }
687
763
  }
688
764
 
689
765
  private reviewChecklist(task: Artifact): ChecklistReview[] {
690
- return checklistEntries(task.extra["checklist"]).map((entry) => ({
766
+ return checklistEntries(task.extra.checklist).map((entry) => ({
691
767
  item: entry.item,
692
768
  proof: entry.proof,
693
769
  accepted: !entry.legacy && entry.proof.length > 0,
694
- ...((entry.legacy || entry.proof.length === 0) ? { reason: "typed proof reference required" } : {}),
770
+ ...(entry.legacy || entry.proof.length === 0 ? { reason: "typed proof reference required" } : {}),
695
771
  }));
696
772
  }
697
773
 
@@ -717,21 +793,31 @@ export class Tasks {
717
793
  if (failed) {
718
794
  return this.events.atomic(() => {
719
795
  const artifact = this.artifacts.setStatus(id, "rejected")!;
720
- this.appendEvent({
721
- taskId: id,
722
- type: "review_rejected",
723
- fromStatus: "review",
724
- toStatus: "rejected",
725
- attemptId,
726
- evidence: { gates, checklist, result: "rejected" },
727
- }, context);
796
+ this.appendEvent(
797
+ {
798
+ taskId: id,
799
+ type: "review_rejected",
800
+ fromStatus: "review",
801
+ toStatus: "rejected",
802
+ attemptId,
803
+ evidence: { gates, checklist, result: "rejected" },
804
+ },
805
+ context,
806
+ );
728
807
  return { artifact, gates, checklist, completed: false, focused: this.active({ sessionId: context.sessionId }), blocked: [] };
729
808
  });
730
809
  }
731
810
  return this.events.atomic(() => this.finish(id, attemptId, gates, checklist, context, options));
732
811
  }
733
812
 
734
- private finish(id: string, attemptId: string, gates: GateResult[], checklist: ChecklistReview[], context: TaskEventContext, options: TaskCompletionOptions): TaskCompletion {
813
+ private finish(
814
+ id: string,
815
+ attemptId: string,
816
+ gates: GateResult[],
817
+ checklist: ChecklistReview[],
818
+ context: TaskEventContext,
819
+ options: TaskCompletionOptions,
820
+ ): TaskCompletion {
735
821
  const successorIds = this.relationships(id)
736
822
  .filter((edge) => edge.relation === "depends_on" && edge.to === id)
737
823
  .map((edge) => edge.from);
@@ -739,22 +825,24 @@ export class Tasks {
739
825
  throw new Error(`task "${id}" exceeds ${TASK_EXECUTION_MAX_DEGREE} successors`);
740
826
  }
741
827
  const artifact = this.artifacts.setStatus(id, "done")!;
742
- this.appendEvent({
743
- taskId: id,
744
- type: "completed",
745
- fromStatus: "review",
746
- toStatus: "done",
747
- attemptId,
748
- evidence: { gates, checklist, result: "completed" },
749
- }, context);
828
+ this.appendEvent(
829
+ {
830
+ taskId: id,
831
+ type: "completed",
832
+ fromStatus: "review",
833
+ toStatus: "done",
834
+ attemptId,
835
+ evidence: { gates, checklist, result: "completed" },
836
+ },
837
+ context,
838
+ );
750
839
  this.focusStore.clearEverywhere(id);
751
840
  const blocked: TaskBlockage[] = [];
752
841
  let focused: Artifact | null = null;
753
842
  for (const successorId of [...successorIds].sort()) {
754
843
  const successor = this.require(successorId);
755
844
  if (successor.status === "done" || successor.status === "canceled") continue;
756
- const dependencyIds = this.dependencyIds(successorId)
757
- .filter((dependencyId) => this.require(dependencyId).status !== "done");
845
+ const dependencyIds = this.dependencyIds(successorId).filter((dependencyId) => this.require(dependencyId).status !== "done");
758
846
  if (dependencyIds.length > 0) {
759
847
  blocked.push({ artifact: successor, dependencyIds });
760
848
  continue;
@@ -794,19 +882,26 @@ export class Tasks {
794
882
  * unrecognized shape.
795
883
  */
796
884
  private blockingDiscussions(id: string): Artifact[] {
797
- return this.artifacts.relationships({ artifactIds: [id] })
885
+ return this.artifacts
886
+ .relationships({ artifactIds: [id] })
798
887
  .filter((edge) => edge.relation === "blocks" && edge.to === id)
799
888
  .map((edge) => this.artifacts.get(edge.from))
800
889
  .filter((source): source is Artifact => source !== null && isDiscussionArtifact(source))
801
890
  .filter((discussion) => {
802
- try { return readDiscussionExtra(discussion.extra).state === "active"; } catch { return false; }
891
+ try {
892
+ return readDiscussionExtra(discussion.extra).state === "active";
893
+ } catch {
894
+ return false;
895
+ }
803
896
  });
804
897
  }
805
898
 
806
899
  private requireNotBlocked(task: Artifact): void {
807
900
  const blockers = this.blockingDiscussions(task.id);
808
901
  if (blockers.length > 0) {
809
- throw new Error(`task "${task.title}" is blocked by ${blockers.length} active Discussion(s): ${blockers.map((discussion) => `"${discussion.title}"`).join(", ")}`);
902
+ throw new Error(
903
+ `task "${task.title}" is blocked by ${blockers.length} active Discussion(s): ${blockers.map((discussion) => `"${discussion.title}"`).join(", ")}`,
904
+ );
810
905
  }
811
906
  }
812
907
  }
@@ -3,7 +3,7 @@
3
3
  * artifact's kind. Registered once here, shared by every domain, instead of
4
4
  * duplicated as rules.remove/docs.remove/etc.
5
5
  */
6
- import { defineVehicleOperation, bindVehicleOperation } from "@danypops/vehicle-core";
6
+ import { bindVehicleOperation, defineVehicleOperation } from "@danypops/vehicle-core";
7
7
  import type { VehicleRegistry } from "@danypops/vehicle-server";
8
8
  import { removeArtifactSubtree } from "../artifact-subtree.ts";
9
9
  import type { ArtifactStore } from "../ports/artifact-store.ts";
@@ -14,9 +14,9 @@ const OWNER = "artifact";
14
14
  const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
15
15
 
16
16
  function eventContext(input: Record<string, unknown>): { actor?: string; source?: string; sessionId?: string } {
17
- const actor = input["actor"];
18
- const source = input["source"];
19
- const sessionId = input["session_id"] ?? input["sessionId"];
17
+ const actor = input.actor;
18
+ const source = input.source;
19
+ const sessionId = input.session_id ?? input.sessionId;
20
20
  return {
21
21
  actor: typeof actor === "string" ? actor : undefined,
22
22
  source: typeof source === "string" ? source : undefined,
@@ -25,7 +25,7 @@ function eventContext(input: Record<string, unknown>): { actor?: string; source?
25
25
  }
26
26
 
27
27
  function requireId(input: Record<string, unknown>): string {
28
- const id = input["id"];
28
+ const id = input.id;
29
29
  if (typeof id !== "string" || id.length === 0) throw new Error("id is required");
30
30
  return id;
31
31
  }
@@ -50,7 +50,10 @@ export function registerArtifactTrashOperations(registry: VehicleRegistry, artif
50
50
  idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
51
51
  limits: LIMITS,
52
52
  });
53
- registry.register(OWNER, bindVehicleOperation(operation, () => async (context) => execute(context.input)));
53
+ registry.register(
54
+ OWNER,
55
+ bindVehicleOperation(operation, () => async (context) => execute(context.input)),
56
+ );
54
57
  };
55
58
 
56
59
  define(
@@ -59,11 +62,12 @@ export function registerArtifactTrashOperations(registry: VehicleRegistry, artif
59
62
  "read",
60
63
  { id: stringProp, tree: { type: "boolean" } as unknown as { type: string }, depth: numberProp, max_nodes: numberProp },
61
64
  ["id"],
62
- (input) => artifacts.get(requireId(input), {
63
- tree: input["tree"] === true,
64
- depth: typeof input["depth"] === "number" ? input["depth"] : undefined,
65
- maxNodes: typeof input["max_nodes"] === "number" ? input["max_nodes"] : undefined,
66
- }),
65
+ (input) =>
66
+ artifacts.get(requireId(input), {
67
+ tree: input.tree === true,
68
+ depth: typeof input.depth === "number" ? input.depth : undefined,
69
+ maxNodes: typeof input.max_nodes === "number" ? input.max_nodes : undefined,
70
+ }),
67
71
  );
68
72
 
69
73
  define(
@@ -72,7 +76,11 @@ export function registerArtifactTrashOperations(registry: VehicleRegistry, artif
72
76
  "local-write",
73
77
  { id: stringProp, reason: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
74
78
  ["id"],
75
- (input) => artifacts.trash(requireId(input), { reason: typeof input["reason"] === "string" ? input["reason"] : undefined, context: eventContext(input) }),
79
+ (input) =>
80
+ artifacts.trash(requireId(input), {
81
+ reason: typeof input.reason === "string" ? input.reason : undefined,
82
+ context: eventContext(input),
83
+ }),
76
84
  );
77
85
 
78
86
  define(
@@ -81,7 +89,11 @@ export function registerArtifactTrashOperations(registry: VehicleRegistry, artif
81
89
  "local-write",
82
90
  { id: stringProp, reason: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
83
91
  ["id"],
84
- (input) => removeArtifactSubtree(artifacts, requireId(input), { reason: typeof input["reason"] === "string" ? input["reason"] : undefined, context: eventContext(input) }),
92
+ (input) =>
93
+ removeArtifactSubtree(artifacts, requireId(input), {
94
+ reason: typeof input.reason === "string" ? input.reason : undefined,
95
+ context: eventContext(input),
96
+ }),
85
97
  );
86
98
 
87
99
  define(
@@ -3,7 +3,7 @@
3
3
  * VehicleRegistry projection (notes-vehicle.ts, rules-vehicle.ts, docs-vehicle.ts,
4
4
  * artifact-trash-vehicle.ts).
5
5
  */
6
- import { defineVehicleSchema, type VehicleSchemaCodec, type VehicleContentBlock } from "@danypops/vehicle-core";
6
+ import { defineVehicleSchema, type VehicleContentBlock, type VehicleSchemaCodec } from "@danypops/vehicle-core";
7
7
  import type { Artifact } from "../domain/artifact.ts";
8
8
  import type { ArtifactStore } from "../ports/artifact-store.ts";
9
9
  import type { TaskExecutionPlan } from "../task-execution.ts";
@@ -14,7 +14,10 @@ import type { TaskExecutionPlan } from "../task-execution.ts";
14
14
  * enforced at runtime -- so a declared `enum` has to be checked here for
15
15
  * real, or it's a documentation gesture, not an honest contract.
16
16
  */
17
- export function looseObjectSchema(properties: Record<string, { type: string; enum?: readonly string[] }>, required: readonly string[] = []): VehicleSchemaCodec<Record<string, unknown>> {
17
+ export function looseObjectSchema(
18
+ properties: Record<string, { type: string; enum?: readonly string[] }>,
19
+ required: readonly string[] = [],
20
+ ): VehicleSchemaCodec<Record<string, unknown>> {
18
21
  return defineVehicleSchema<Record<string, unknown>>({
19
22
  jsonSchema: { type: "object", properties, required: [...required], additionalProperties: false },
20
23
  safeParse(value) {
@@ -61,7 +64,9 @@ export function matchArtifactByName(candidates: readonly Artifact[], name: strin
61
64
  const matches = candidates.filter((artifact) => artifact.title.trim().toLowerCase() === needle);
62
65
  if (matches.length === 0) throw new Error(`no artifact named "${name}" found in this scope`);
63
66
  if (matches.length > 1) {
64
- throw new Error(`${matches.length} artifacts are named "${name}": ${matches.map((a) => `${a.title} (${a.id})`).join(", ")} -- use id to disambiguate`);
67
+ throw new Error(
68
+ `${matches.length} artifacts are named "${name}": ${matches.map((a) => `${a.title} (${a.id})`).join(", ")} -- use id to disambiguate`,
69
+ );
65
70
  }
66
71
  return matches[0]!.id;
67
72
  }
@@ -72,7 +77,11 @@ export function matchArtifactByName(candidates: readonly Artifact[], name: strin
72
77
  * flow only -- the caller supplies its own scoped/widened list calls, since scoping
73
78
  * differs per domain. Omit `fetchWidened` when there is no wider scope to retry.
74
79
  */
75
- export function resolveArtifactIdWidened(name: string, fetchCandidates: () => readonly Artifact[], fetchWidened?: () => readonly Artifact[]): string {
80
+ export function resolveArtifactIdWidened(
81
+ name: string,
82
+ fetchCandidates: () => readonly Artifact[],
83
+ fetchWidened?: () => readonly Artifact[],
84
+ ): string {
76
85
  try {
77
86
  return matchArtifactByName(fetchCandidates(), name);
78
87
  } catch (error) {
@@ -87,7 +96,12 @@ export function labelsById(artifacts: ArtifactStore, ids: readonly string[]): Ma
87
96
  const resolved = uniqueIds.map((id) => artifacts.get(id)).filter((artifact): artifact is Artifact => artifact !== null);
88
97
  const titleCounts = new Map<string, number>();
89
98
  for (const artifact of resolved) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
90
- return new Map(resolved.map((artifact) => [artifact.id, (titleCounts.get(artifact.title) ?? 0) > 1 ? `${artifact.title} (${artifact.id})` : artifact.title]));
99
+ return new Map(
100
+ resolved.map((artifact) => [
101
+ artifact.id,
102
+ (titleCounts.get(artifact.title) ?? 0) > 1 ? `${artifact.title} (${artifact.id})` : artifact.title,
103
+ ]),
104
+ );
91
105
  }
92
106
 
93
107
  export interface WorkflowRunNarrativeInput {
@@ -103,14 +117,21 @@ export interface WorkflowRunNarrativeInput {
103
117
  * execution DAG -- the same shape pi-papyrus's own hand-rolled playbooks tool built
104
118
  * client-side, now built once here where the run result is actually produced.
105
119
  */
106
- export function buildWorkflowRunContent(artifacts: ArtifactStore, headline: string, input: WorkflowRunNarrativeInput, extraLines: readonly string[] = []): VehicleContentBlock {
120
+ export function buildWorkflowRunContent(
121
+ artifacts: ArtifactStore,
122
+ headline: string,
123
+ input: WorkflowRunNarrativeInput,
124
+ extraLines: readonly string[] = [],
125
+ ): VehicleContentBlock {
107
126
  const nodeById = new Map(input.execution.nodes.map((node) => [node.id, node]));
108
127
  const rootLabels = input.rootTaskIds.map((id) => nodeById.get(id)?.title ?? "unknown task");
109
128
  const createdLabels = labelsById(artifacts, [...input.created.docs, ...input.created.rules]);
110
129
  const titleCounts = new Map<string, number>();
111
130
  for (const node of input.execution.nodes) titleCounts.set(node.title, (titleCounts.get(node.title) ?? 0) + 1);
112
131
  const executionLines = input.execution.nodes
113
- .map((node) => ((titleCounts.get(node.title) ?? 0) > 1 ? ` [${node.state}] ${node.title} (${node.id})` : ` [${node.state}] ${node.title}`))
132
+ .map((node) =>
133
+ (titleCounts.get(node.title) ?? 0) > 1 ? ` [${node.state}] ${node.title} (${node.id})` : ` [${node.state}] ${node.title}`,
134
+ )
114
135
  .join("\n");
115
136
  const text = [
116
137
  headline,