@danypops/papyrus 0.11.3 → 0.12.0

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 (46) hide show
  1. package/README.md +16 -2
  2. package/extension/src/active-task-continuation.ts +6 -0
  3. package/extension/src/domain-tools.ts +108 -52
  4. package/extension/src/index.ts +90 -37
  5. package/extension/src/notes.ts +14 -1
  6. package/extension/src/task-focus-events.ts +57 -0
  7. package/extension/src/tasks.ts +51 -15
  8. package/extension/src/tool-rendering/artifact-card.ts +117 -0
  9. package/extension/src/tool-rendering/artifact-list.ts +179 -0
  10. package/extension/src/tool-rendering/index.ts +107 -0
  11. package/extension/src/tool-rendering/render-model.ts +406 -0
  12. package/package.json +4 -2
  13. package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
  14. package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
  15. package/src/adapters/sqlite-artifact-store.ts +20 -11
  16. package/src/adapters/sqlite-discourse-store.ts +325 -0
  17. package/src/adapters/sqlite-graph-projection-store.ts +41 -0
  18. package/src/adapters/sqlite-task-focus-store.ts +34 -15
  19. package/src/authority-registry.ts +115 -0
  20. package/src/cli.ts +904 -124
  21. package/src/constants.ts +38 -5
  22. package/src/conversation-journal-service.ts +87 -0
  23. package/src/db.ts +336 -8
  24. package/src/domain/artifact-event.ts +99 -0
  25. package/src/domain/conversation-journal.ts +168 -0
  26. package/src/domain/discourse-store.ts +142 -0
  27. package/src/domain/graph-projection.ts +74 -0
  28. package/src/domain/task-event.ts +4 -0
  29. package/src/domain-services.ts +133 -38
  30. package/src/graph-projection-service.ts +103 -0
  31. package/src/id-migration.ts +200 -0
  32. package/src/module-registry.ts +53 -0
  33. package/src/modules/docs.ts +77 -0
  34. package/src/modules/graph-projection.ts +82 -0
  35. package/src/modules/notes.ts +76 -0
  36. package/src/modules/rules.ts +81 -0
  37. package/src/modules/skills.ts +113 -0
  38. package/src/modules/tasks.ts +164 -0
  39. package/src/ops.ts +142 -15
  40. package/src/ports/artifact-scope-store.ts +20 -0
  41. package/src/ports/artifact-store.ts +10 -5
  42. package/src/ports/conversation-journal-store.ts +17 -0
  43. package/src/ports/graph-projection-store.ts +15 -0
  44. package/src/ports/task-focus-store.ts +62 -20
  45. package/src/service.ts +218 -223
  46. package/src/task-service.ts +70 -38
@@ -34,6 +34,8 @@ export interface TaskFilter {
34
34
  projectRoot?: string;
35
35
  scope?: TaskViewMode;
36
36
  rootTaskId?: string;
37
+ /** Requesting agent session id — scopes Task Focus reads so concurrent agents see only their own Focus. Defaults to a shared "global" scope when omitted. */
38
+ sessionId?: string;
37
39
  }
38
40
 
39
41
  export type TaskStatus = TaskLifecycleStatus;
@@ -280,7 +282,7 @@ export class Tasks {
280
282
  throw new Error(`task execution graph exceeds ${TASK_EXECUTION_MAX_NODES} nodes`);
281
283
  }
282
284
  const byId = new Map(tasks.map((task) => [task.id, task]));
283
- const focus = this.focusStore.get();
285
+ const focus = this.focusStore.get(filter.sessionId);
284
286
  const focusedId = focus?.taskId;
285
287
  const nodes = new Map(tasks.map((task) => [task.id, {
286
288
  task,
@@ -326,11 +328,11 @@ export class Tasks {
326
328
  }
327
329
 
328
330
  focused(filter?: TaskFilter): TaskFocus | null {
329
- const focus = this.focusStore.get();
331
+ const focus = this.focusStore.get(filter?.sessionId);
330
332
  if (!focus) return null;
331
333
  const task = this.artifacts.get(focus.taskId);
332
334
  if (!task || task.kind !== "task" || task.status === "done" || task.status === "canceled") {
333
- this.focusStore.clear(focus.taskId);
335
+ this.focusStore.clear(focus.taskId, filter?.sessionId);
334
336
  return null;
335
337
  }
336
338
  if (filter?.projectRoot && !this.list(filter).some((candidate) => candidate.id === task.id)) return null;
@@ -346,7 +348,7 @@ export class Tasks {
346
348
  return this.events.atomic(() => {
347
349
  const task = this.require(id);
348
350
  if (task.status === "done" || task.status === "canceled") throw new Error(`cannot focus task from ${task.status}`);
349
- this.focusStore.set(id);
351
+ this.focusStore.set(id, context.sessionId);
350
352
  this.appendEvent({ taskId: id, type: "focus_set" }, context);
351
353
  return task;
352
354
  });
@@ -354,9 +356,9 @@ export class Tasks {
354
356
 
355
357
  pauseFocus(context: TaskEventContext = {}): TaskFocus {
356
358
  return this.events.atomic(() => {
357
- const focus = this.focused();
359
+ const focus = this.focused({ sessionId: context.sessionId });
358
360
  if (!focus) throw new Error("no focused task");
359
- const state = this.focusStore.pause(focus.artifact.id, context.reason);
361
+ const state = this.focusStore.pause(focus.artifact.id, context.reason, context.sessionId);
360
362
  this.appendEvent({ taskId: focus.artifact.id, type: "focus_paused" }, context);
361
363
  return { artifact: focus.artifact, status: state.status, updatedAt: state.updatedAt, ...(state.pauseReason ? { pauseReason: state.pauseReason } : {}) };
362
364
  });
@@ -364,9 +366,9 @@ export class Tasks {
364
366
 
365
367
  unpauseFocus(context: TaskEventContext = {}): TaskFocus {
366
368
  return this.events.atomic(() => {
367
- const focus = this.focused();
369
+ const focus = this.focused({ sessionId: context.sessionId });
368
370
  if (!focus) throw new Error("no focused task");
369
- const state = this.focusStore.unpause(focus.artifact.id);
371
+ const state = this.focusStore.unpause(focus.artifact.id, context.sessionId);
370
372
  this.appendEvent({ taskId: focus.artifact.id, type: "focus_unpaused" }, context);
371
373
  return { artifact: focus.artifact, status: state.status, updatedAt: state.updatedAt };
372
374
  });
@@ -374,9 +376,9 @@ export class Tasks {
374
376
 
375
377
  clearFocus(context: TaskEventContext = {}): { cleared: boolean } {
376
378
  return this.events.atomic(() => {
377
- const focus = this.focusStore.get();
379
+ const focus = this.focusStore.get(context.sessionId);
378
380
  if (focus) this.appendEvent({ taskId: focus.taskId, type: "focus_cleared" }, context);
379
- this.focusStore.clear();
381
+ this.focusStore.clear(undefined, context.sessionId);
380
382
  return { cleared: focus !== undefined };
381
383
  });
382
384
  }
@@ -389,14 +391,14 @@ export class Tasks {
389
391
  if (action === "start") {
390
392
  const blocking = this.dependencyIds(id).filter((dependencyId) => this.require(dependencyId).status !== "done");
391
393
  if (blocking.length > 0) throw new Error(`task "${id}" is blocked by dependencies: ${blocking.join(", ")}`);
392
- this.focusStore.set(id);
394
+ this.focusStore.set(id, context.sessionId);
393
395
  }
394
396
  const updated = this.artifacts.setStatus(id, transition.to)!;
395
397
  const eventType = { start: "started", submit: "submitted", reject: "review_rejected", retry: "retried", cancel: "canceled" }[action] as AppendTaskEvent["type"];
396
398
  this.appendEvent({ taskId: id, type: eventType, fromStatus: task.status as TaskStatus, toStatus: transition.to }, context);
397
399
  if (action === "start" || action === "retry") this.propagateProgressToAncestors(id, context);
398
- if (action === "retry") this.focusStore.set(id);
399
- if (action === "cancel") this.focusStore.clear(id);
400
+ if (action === "retry") this.focusStore.set(id, context.sessionId);
401
+ if (action === "cancel") this.focusStore.clearEverywhere(id);
400
402
  return updated;
401
403
  });
402
404
  }
@@ -437,30 +439,60 @@ export class Tasks {
437
439
  return this.artifacts.setExtra(id, { ...task.extra, checklist: validateChecklist(checklist) })!;
438
440
  }
439
441
 
440
- depend(id: string, dependencyId: string): Artifact {
441
- this.require(id);
442
- this.require(dependencyId);
443
- const graph = this.graph();
444
- assertDependencyEdgeAllowed(graph, id, dependencyId);
445
- const node = graph.nodes.find((entry) => entry.task.id === id)!;
446
- if (node.dependencyIds.includes(dependencyId)) return this.show(id);
447
- if (node.dependencyIds.length >= TASK_EXECUTION_MAX_DEGREE) {
448
- throw new Error(`task "${id}" cannot exceed ${TASK_EXECUTION_MAX_DEGREE} prerequisites`);
449
- }
450
- const successorCount = graph.nodes.filter((entry) => entry.dependencyIds.includes(dependencyId)).length;
451
- if (successorCount >= TASK_EXECUTION_MAX_DEGREE) {
452
- throw new Error(`task "${dependencyId}" cannot exceed ${TASK_EXECUTION_MAX_DEGREE} successors`);
453
- }
454
- this.artifacts.link({ from: id, relation: "depends_on", to: dependencyId });
455
- return this.show(id);
442
+ depend(id: string, dependencyId: string, context: TaskEventContext = {}): Artifact {
443
+ return this.events.atomic(() => {
444
+ this.require(id);
445
+ this.require(dependencyId);
446
+ const graph = this.graph();
447
+ assertDependencyEdgeAllowed(graph, id, dependencyId);
448
+ const node = graph.nodes.find((entry) => entry.task.id === id)!;
449
+ if (node.dependencyIds.includes(dependencyId)) return this.show(id);
450
+ if (node.dependencyIds.length >= TASK_EXECUTION_MAX_DEGREE) {
451
+ throw new Error(`task "${id}" cannot exceed ${TASK_EXECUTION_MAX_DEGREE} prerequisites`);
452
+ }
453
+ const successorCount = graph.nodes.filter((entry) => entry.dependencyIds.includes(dependencyId)).length;
454
+ if (successorCount >= TASK_EXECUTION_MAX_DEGREE) {
455
+ throw new Error(`task "${dependencyId}" cannot exceed ${TASK_EXECUTION_MAX_DEGREE} successors`);
456
+ }
457
+ this.artifacts.link({ from: id, relation: "depends_on", to: dependencyId }, context);
458
+ this.appendEvent({ taskId: id, type: "dependency_added", reason: context.reason }, context);
459
+ return this.show(id);
460
+ });
456
461
  }
457
462
 
458
- contain(parentId: string, childId: string): Artifact {
459
- this.require(parentId);
460
- this.require(childId);
461
- this.artifacts.link({ from: parentId, relation: "contains", to: childId });
462
- this.artifacts.link({ from: childId, relation: "part_of", to: parentId });
463
- return this.show(parentId);
463
+ /** Idempotent: undepending an already-absent dependency is a no-op. Never starts, completes, or focuses work — only removes the edge. */
464
+ undepend(id: string, dependencyId: string, context: TaskEventContext = {}): Artifact {
465
+ return this.events.atomic(() => {
466
+ this.require(id);
467
+ this.require(dependencyId);
468
+ const removed = this.artifacts.unlink({ from: id, relation: "depends_on", to: dependencyId }, context);
469
+ if (removed) this.appendEvent({ taskId: id, type: "dependency_removed", reason: context.reason }, context);
470
+ return this.show(id);
471
+ });
472
+ }
473
+
474
+ contain(parentId: string, childId: string, context: TaskEventContext = {}): Artifact {
475
+ return this.events.atomic(() => {
476
+ this.require(parentId);
477
+ this.require(childId);
478
+ const alreadyContained = this.relationships(parentId).some((edge) => edge.relation === "contains" && edge.from === parentId && edge.to === childId);
479
+ this.artifacts.link({ from: parentId, relation: "contains", to: childId }, context);
480
+ this.artifacts.link({ from: childId, relation: "part_of", to: parentId }, context);
481
+ if (!alreadyContained) this.appendEvent({ taskId: parentId, type: "containment_added", reason: context.reason }, context);
482
+ return this.show(parentId);
483
+ });
484
+ }
485
+
486
+ /** Idempotent: removing an already-absent containment is a no-op. Both contains/part_of edges are removed atomically. */
487
+ uncontain(parentId: string, childId: string, context: TaskEventContext = {}): Artifact {
488
+ return this.events.atomic(() => {
489
+ this.require(parentId);
490
+ this.require(childId);
491
+ const removedContains = this.artifacts.unlink({ from: parentId, relation: "contains", to: childId }, context);
492
+ this.artifacts.unlink({ from: childId, relation: "part_of", to: parentId }, context);
493
+ if (removedContains) this.appendEvent({ taskId: parentId, type: "containment_removed", reason: context.reason }, context);
494
+ return this.show(parentId);
495
+ });
464
496
  }
465
497
 
466
498
  private descendantIds(rootTaskId: string, projectTaskIds: string[]): Set<string> {
@@ -575,7 +607,7 @@ export class Tasks {
575
607
  attemptId,
576
608
  evidence: { gates, checklist, result: "rejected" },
577
609
  }, context);
578
- return { artifact, gates, checklist, completed: false, focused: this.active(), blocked: [] };
610
+ return { artifact, gates, checklist, completed: false, focused: this.active({ sessionId: context.sessionId }), blocked: [] };
579
611
  });
580
612
  }
581
613
  return this.events.atomic(() => this.finish(id, attemptId, gates, checklist, context, options));
@@ -597,7 +629,7 @@ export class Tasks {
597
629
  attemptId,
598
630
  evidence: { gates, checklist, result: "completed" },
599
631
  }, context);
600
- this.focusStore.clear(id);
632
+ this.focusStore.clearEverywhere(id);
601
633
  const blocked: TaskBlockage[] = [];
602
634
  let focused: Artifact | null = null;
603
635
  for (const successorId of [...successorIds].sort()) {
@@ -610,7 +642,7 @@ export class Tasks {
610
642
  continue;
611
643
  }
612
644
  if (options.focusSuccessor !== false && !focused) {
613
- this.focusStore.set(successor.id);
645
+ this.focusStore.set(successor.id, context.sessionId);
614
646
  focused = successor;
615
647
  }
616
648
  }