@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
@@ -26,7 +26,7 @@ import type { ArtifactStore } from "../ports/artifact-store.ts";
26
26
  import type { SessionIdentity } from "../session-identity-service.ts";
27
27
  import { taskContext } from "../task-context.ts";
28
28
  import { projectTaskExecution } from "../task-execution.ts";
29
- import { Tasks, type TaskStatus } from "../task-service.ts";
29
+ import type { TaskStatus, Tasks } from "../task-service.ts";
30
30
 
31
31
  const MODULE_ID = "tasks";
32
32
 
@@ -87,17 +87,51 @@ const taskFilter = (input: OperationInput) => ({
87
87
  */
88
88
  /** This module's own operation names, the single source of truth src/service.ts's EXPECTED_OPERATION_NAMES spreads in rather than re-listing by hand. */
89
89
  export const TASKS_OPERATION_NAMES = [
90
- "tasks.create", "tasks.update", "tasks.list", "tasks.graph", "tasks.plan", "tasks.show", "tasks.history",
91
- "tasks.scope", "tasks.set_scope", "tasks.assign_project", "tasks.active", "tasks.focused", "tasks.focus",
92
- "tasks.pause", "tasks.unpause", "tasks.clear_focus", "tasks.start", "tasks.submit", "tasks.complete",
93
- "tasks.run_gates", "tasks.set_checklist", "tasks.set_gates", "tasks.context", "tasks.reject", "tasks.retry", "tasks.cancel", "tasks.cancel_subtree",
94
- "tasks.depend", "tasks.undepend", "tasks.contain", "tasks.uncontain", "tasks.reap_stale_focus",
95
- "tasks.claim", "tasks.heartbeat_lease", "tasks.release_lease", "tasks.lease", "tasks.reap_stale_leases", "tasks.event_feed",
90
+ "tasks.create",
91
+ "tasks.update",
92
+ "tasks.list",
93
+ "tasks.graph",
94
+ "tasks.plan",
95
+ "tasks.show",
96
+ "tasks.history",
97
+ "tasks.scope",
98
+ "tasks.set_scope",
99
+ "tasks.assign_project",
100
+ "tasks.active",
101
+ "tasks.focused",
102
+ "tasks.focus",
103
+ "tasks.pause",
104
+ "tasks.unpause",
105
+ "tasks.clear_focus",
106
+ "tasks.start",
107
+ "tasks.submit",
108
+ "tasks.complete",
109
+ "tasks.run_gates",
110
+ "tasks.set_checklist",
111
+ "tasks.set_gates",
112
+ "tasks.context",
113
+ "tasks.reject",
114
+ "tasks.retry",
115
+ "tasks.cancel",
116
+ "tasks.cancel_subtree",
117
+ "tasks.depend",
118
+ "tasks.undepend",
119
+ "tasks.contain",
120
+ "tasks.uncontain",
121
+ "tasks.reap_stale_focus",
122
+ "tasks.claim",
123
+ "tasks.heartbeat_lease",
124
+ "tasks.release_lease",
125
+ "tasks.lease",
126
+ "tasks.reap_stale_leases",
127
+ "tasks.event_feed",
96
128
  ] as const;
97
129
 
98
130
  export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionIdentity: SessionIdentity): OperationDefinition[] {
99
131
  const define = <Input, Output>(name: string, execute: (input: Input) => Output): OperationDefinition<Input, Output> => ({
100
- name, moduleId: MODULE_ID, execute,
132
+ name,
133
+ moduleId: MODULE_ID,
134
+ execute,
101
135
  });
102
136
  // Enforced only for the operations where session_id is BEHAVIOR-affecting (it selects
103
137
  // which Task Focus row is mutated), not for every session_id-carrying operation -- see
@@ -107,82 +141,123 @@ export function tasksOperations(tasks: Tasks, artifacts: ArtifactStore, sessionI
107
141
  sessionIdentity.assertAuthorized(eventContext(input).sessionId, optionalString(input, "session_secret"));
108
142
  };
109
143
  return [
110
- define("tasks.create", (input: OperationInput) => tasks.create({
111
- title: string(input, "title"),
112
- body: optionalString(input, "body"),
113
- status: optionalString(input, "status") as TaskStatus | undefined,
114
- labels: input["labels"] as string[] | undefined,
115
- extra: input["extra"] as Record<string, unknown> | undefined,
116
- gates: input["gates"] as Parameters<Tasks["create"]>[0]["gates"],
117
- checklist: input["checklist"] as Checklist | undefined,
118
- templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
119
- parentId: optionalString(input, "parent_id") ?? optionalString(input, "parentId"),
120
- dependsOn: (input["depends_on"] ?? input["dependsOn"]) as string[] | undefined,
121
- projectRoot: string(input, "project_root"),
122
- projectSource: "cwd",
123
- }, eventContext(input))),
124
- define("tasks.update", (input: OperationInput) => tasks.update(string(input, "id"), {
125
- ...(input["title"] !== undefined ? { title: optionalString(input, "title")! } : {}),
126
- ...(input["body"] !== undefined ? { body: optionalString(input, "body")! } : {}),
127
- ...(input["labels"] !== undefined ? { labels: optionalStringArray(input, "labels")! } : {}),
128
- ...(input["status"] !== undefined ? { status: string(input, "status") as "todo" } : {}),
129
- }, eventContext(input))),
144
+ define("tasks.create", (input: OperationInput) =>
145
+ tasks.create(
146
+ {
147
+ title: string(input, "title"),
148
+ body: optionalString(input, "body"),
149
+ status: optionalString(input, "status") as TaskStatus | undefined,
150
+ labels: input.labels as string[] | undefined,
151
+ extra: input.extra as Record<string, unknown> | undefined,
152
+ gates: input.gates as Parameters<Tasks["create"]>[0]["gates"],
153
+ checklist: input.checklist as Checklist | undefined,
154
+ templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
155
+ parentId: optionalString(input, "parent_id") ?? optionalString(input, "parentId"),
156
+ dependsOn: (input.depends_on ?? input.dependsOn) as string[] | undefined,
157
+ projectRoot: string(input, "project_root"),
158
+ projectSource: "cwd",
159
+ },
160
+ eventContext(input),
161
+ ),
162
+ ),
163
+ define("tasks.update", (input: OperationInput) =>
164
+ tasks.update(
165
+ string(input, "id"),
166
+ {
167
+ ...(input.title !== undefined ? { title: optionalString(input, "title")! } : {}),
168
+ ...(input.body !== undefined ? { body: optionalString(input, "body")! } : {}),
169
+ ...(input.labels !== undefined ? { labels: optionalStringArray(input, "labels")! } : {}),
170
+ ...(input.status !== undefined ? { status: string(input, "status") as "todo" } : {}),
171
+ },
172
+ eventContext(input),
173
+ ),
174
+ ),
130
175
  define("tasks.list", (input: OperationInput) => tasks.list(taskFilter(input))),
131
176
  define("tasks.graph", (input: OperationInput) => tasks.graph(taskFilter(input))),
132
177
  define("tasks.plan", (input: OperationInput) => projectTaskExecution(tasks.graph(taskFilter(input)))),
133
178
  define("tasks.show", (input: OperationInput) => tasks.show(string(input, "id"))),
134
- define("tasks.history", (input: OperationInput) => tasks.history(string(input, "id"), {
135
- limit: optionalNumber(input, "limit"),
136
- cursor: optionalNumber(input, "cursor"),
137
- direction: optionalString(input, "direction") as TaskEventDirection | undefined,
138
- })),
179
+ define("tasks.history", (input: OperationInput) =>
180
+ tasks.history(string(input, "id"), {
181
+ limit: optionalNumber(input, "limit"),
182
+ cursor: optionalNumber(input, "cursor"),
183
+ direction: optionalString(input, "direction") as TaskEventDirection | undefined,
184
+ }),
185
+ ),
139
186
  define("tasks.scope", (input: OperationInput) => tasks.scopeSelection(string(input, "project_root"))),
140
- define("tasks.set_scope", (input: OperationInput) => tasks.setView(
141
- string(input, "project_root"),
142
- string(input, "scope") as TaskViewMode,
143
- optionalString(input, "root_task_id"),
144
- )),
145
- define("tasks.assign_project", (input: OperationInput) => tasks.assignProject(
146
- string(input, "id"),
147
- string(input, "project_root"),
148
- eventContext(input),
149
- )),
187
+ define("tasks.set_scope", (input: OperationInput) =>
188
+ tasks.setView(string(input, "project_root"), string(input, "scope") as TaskViewMode, optionalString(input, "root_task_id")),
189
+ ),
190
+ define("tasks.assign_project", (input: OperationInput) =>
191
+ tasks.assignProject(string(input, "id"), string(input, "project_root"), eventContext(input)),
192
+ ),
150
193
  define("tasks.active", (input: OperationInput) => tasks.active(taskFilter(input))),
151
194
  define("tasks.focused", (input: OperationInput) => tasks.focused(taskFilter(input))),
152
- define("tasks.focus", (input: OperationInput) => { guardFocusMutation(input); return tasks.focus(string(input, "id"), eventContext(input)); }),
153
- define("tasks.pause", (input: OperationInput) => { guardFocusMutation(input); return tasks.pauseFocus(eventContext(input)); }),
154
- define("tasks.unpause", (input: OperationInput) => { guardFocusMutation(input); return tasks.unpauseFocus(eventContext(input)); }),
155
- define("tasks.clear_focus", (input: OperationInput) => { guardFocusMutation(input); return tasks.clearFocus(eventContext(input)); }),
195
+ define("tasks.focus", (input: OperationInput) => {
196
+ guardFocusMutation(input);
197
+ return tasks.focus(string(input, "id"), eventContext(input));
198
+ }),
199
+ define("tasks.pause", (input: OperationInput) => {
200
+ guardFocusMutation(input);
201
+ return tasks.pauseFocus(eventContext(input));
202
+ }),
203
+ define("tasks.unpause", (input: OperationInput) => {
204
+ guardFocusMutation(input);
205
+ return tasks.unpauseFocus(eventContext(input));
206
+ }),
207
+ define("tasks.clear_focus", (input: OperationInput) => {
208
+ guardFocusMutation(input);
209
+ return tasks.clearFocus(eventContext(input));
210
+ }),
156
211
  define("tasks.reap_stale_focus", () => ({ removed: tasks.reapStaleFocus() })),
157
212
  define("tasks.start", (input: OperationInput) => tasks.transition(string(input, "id"), "start", eventContext(input))),
158
213
  define("tasks.submit", (input: OperationInput) => tasks.transition(string(input, "id"), "submit", eventContext(input))),
159
214
  define("tasks.complete", (input: OperationInput) => tasks.completeAsync(string(input, "id"), eventContext(input))),
160
215
  define("tasks.run_gates", (input: OperationInput) => tasks.runGates(string(input, "id"), eventContext(input))),
161
- define("tasks.set_checklist", (input: OperationInput) => tasks.setChecklist(string(input, "id"), input["checklist"] as Checklist)),
162
- define("tasks.set_gates", (input: OperationInput) => tasks.setGates(string(input, "id"), input["gates"] as Parameters<Tasks["setGates"]>[1])),
163
- define("tasks.context", (input: OperationInput) => taskContext(
164
- artifacts,
165
- tasks.active(taskFilter(input))?.id,
166
- new Set(tasks.list(taskFilter(input)).map((task) => task.id)),
167
- optionalString(input, "verbosity") === "summary" ? "summary" : "full",
168
- )),
216
+ define("tasks.set_checklist", (input: OperationInput) => tasks.setChecklist(string(input, "id"), input.checklist as Checklist)),
217
+ define("tasks.set_gates", (input: OperationInput) =>
218
+ tasks.setGates(string(input, "id"), input.gates as Parameters<Tasks["setGates"]>[1]),
219
+ ),
220
+ define("tasks.context", (input: OperationInput) =>
221
+ taskContext(
222
+ artifacts,
223
+ tasks.active(taskFilter(input))?.id,
224
+ new Set(tasks.list(taskFilter(input)).map((task) => task.id)),
225
+ optionalString(input, "verbosity") === "summary" ? "summary" : "full",
226
+ ),
227
+ ),
169
228
  define("tasks.reject", (input: OperationInput) => tasks.transition(string(input, "id"), "reject", eventContext(input))),
170
229
  define("tasks.retry", (input: OperationInput) => tasks.transition(string(input, "id"), "retry", eventContext(input))),
171
230
  define("tasks.cancel", (input: OperationInput) => tasks.transition(string(input, "id"), "cancel", eventContext(input))),
172
231
  define("tasks.cancel_subtree", (input: OperationInput) => tasks.cancelSubtree(string(input, "id"), eventContext(input))),
173
- define("tasks.depend", (input: OperationInput) => tasks.depend(string(input, "id"), string(input, "dependency_id"), eventContext(input))),
174
- define("tasks.undepend", (input: OperationInput) => tasks.undepend(string(input, "id"), string(input, "dependency_id"), eventContext(input))),
175
- define("tasks.contain", (input: OperationInput) => tasks.contain(string(input, "parent_id"), string(input, "child_id"), eventContext(input))),
176
- define("tasks.uncontain", (input: OperationInput) => tasks.uncontain(string(input, "parent_id"), string(input, "child_id"), eventContext(input))),
177
- define("tasks.claim", (input: OperationInput) => tasks.claimLease(string(input, "id"), string(input, "owner"), optionalNumber(input, "ttl_ms"), optionalString(input, "note"))),
178
- define("tasks.heartbeat_lease", (input: OperationInput) => tasks.heartbeatLease(string(input, "id"), string(input, "owner"), string(input, "token"), optionalNumber(input, "ttl_ms"))),
179
- define("tasks.release_lease", (input: OperationInput) => tasks.releaseLease(string(input, "id"), string(input, "owner"), string(input, "token"))),
232
+ define("tasks.depend", (input: OperationInput) =>
233
+ tasks.depend(string(input, "id"), string(input, "dependency_id"), eventContext(input)),
234
+ ),
235
+ define("tasks.undepend", (input: OperationInput) =>
236
+ tasks.undepend(string(input, "id"), string(input, "dependency_id"), eventContext(input)),
237
+ ),
238
+ define("tasks.contain", (input: OperationInput) =>
239
+ tasks.contain(string(input, "parent_id"), string(input, "child_id"), eventContext(input)),
240
+ ),
241
+ define("tasks.uncontain", (input: OperationInput) =>
242
+ tasks.uncontain(string(input, "parent_id"), string(input, "child_id"), eventContext(input)),
243
+ ),
244
+ define("tasks.claim", (input: OperationInput) =>
245
+ tasks.claimLease(string(input, "id"), string(input, "owner"), optionalNumber(input, "ttl_ms"), optionalString(input, "note")),
246
+ ),
247
+ define("tasks.heartbeat_lease", (input: OperationInput) =>
248
+ tasks.heartbeatLease(string(input, "id"), string(input, "owner"), string(input, "token"), optionalNumber(input, "ttl_ms")),
249
+ ),
250
+ define("tasks.release_lease", (input: OperationInput) =>
251
+ tasks.releaseLease(string(input, "id"), string(input, "owner"), string(input, "token")),
252
+ ),
180
253
  define("tasks.lease", (input: OperationInput) => tasks.getLease(string(input, "id")) ?? null),
181
254
  define("tasks.reap_stale_leases", () => ({ removed: tasks.reapStaleLeases() })),
182
- define("tasks.event_feed", (input: OperationInput) => tasks.eventFeed({
183
- cursor: optionalNumber(input, "cursor"),
184
- limit: optionalNumber(input, "limit"),
185
- eventTypes: optionalStringArray(input, "event_types") as TaskEventFeedQuery["eventTypes"],
186
- })),
255
+ define("tasks.event_feed", (input: OperationInput) =>
256
+ tasks.eventFeed({
257
+ cursor: optionalNumber(input, "cursor"),
258
+ limit: optionalNumber(input, "limit"),
259
+ eventTypes: optionalStringArray(input, "event_types") as TaskEventFeedQuery["eventTypes"],
260
+ }),
261
+ ),
187
262
  ];
188
263
  }
@@ -9,13 +9,13 @@ import {
9
9
  } from "./constants.ts";
10
10
  import type { Artifact } from "./domain/artifact.ts";
11
11
  import type { AppendNoteEvent, NoteEventType, NoteHistoryPage, NoteHistoryQuery } from "./domain/note-event.ts";
12
- import { InMemoryNoteEventStore, type NoteEventStore } from "./ports/note-event-store.ts";
13
- import { requireAtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
14
12
  import type { ArtifactStore } from "./ports/artifact-store.ts";
13
+ import { requireAtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
14
+ import { InMemoryNoteEventStore, type NoteEventStore } from "./ports/note-event-store.ts";
15
15
 
16
16
  export const NOTE_SUBTYPE = "note";
17
17
  export const NOTE_DISPOSITIONS = ["completed", "duplicate", "declined", "superseded"] as const;
18
- export type NoteDisposition = typeof NOTE_DISPOSITIONS[number];
18
+ export type NoteDisposition = (typeof NOTE_DISPOSITIONS)[number];
19
19
 
20
20
  export interface NoteProvenance {
21
21
  actor?: string;
@@ -60,7 +60,10 @@ function noteTitle(body: string, requested?: string): string {
60
60
  return firstLine.slice(0, NOTE_TITLE_MAX_CHARACTERS) || "Deferred note";
61
61
  }
62
62
 
63
- function provenance(input: NoteProvenance, defaults: { actor: string; source: string }): Omit<AppendNoteEvent, "noteId" | "type" | "relatedId" | "disposition"> {
63
+ function provenance(
64
+ input: NoteProvenance,
65
+ defaults: { actor: string; source: string },
66
+ ): Omit<AppendNoteEvent, "noteId" | "type" | "relatedId" | "disposition"> {
64
67
  return {
65
68
  actor: optionalBounded(input.actor, "note actor", NOTE_PROVENANCE_MAX_LENGTH) ?? defaults.actor,
66
69
  source: optionalBounded(input.source, "note source", NOTE_PROVENANCE_MAX_LENGTH) ?? defaults.source,
@@ -154,7 +157,8 @@ export class Notes {
154
157
  }
155
158
 
156
159
  archive(id: string, input: ArchiveNoteInput): Artifact {
157
- if (!NOTE_DISPOSITIONS.includes(input.disposition)) throw new Error("note disposition must be completed, duplicate, declined, or superseded");
160
+ if (!NOTE_DISPOSITIONS.includes(input.disposition))
161
+ throw new Error("note disposition must be completed, duplicate, declined, or superseded");
158
162
  const atomic = requireAtomicArtifactStore(this.artifacts);
159
163
  return atomic.atomic(() => {
160
164
  const note = this.requireNote(id);
@@ -183,12 +187,12 @@ export class Notes {
183
187
 
184
188
  private requireNote(id: string): Artifact {
185
189
  const artifact = this.artifacts.get(id);
186
- if (!artifact || artifact.kind !== "doc" || artifact.subtype !== NOTE_SUBTYPE) throw new Error(`note "${id}" not found`);
190
+ if (artifact?.kind !== "doc" || artifact.subtype !== NOTE_SUBTYPE) throw new Error(`note "${id}" not found`);
187
191
  return artifact;
188
192
  }
189
193
 
190
194
  private requireProject(note: Artifact, projectRoot: string): void {
191
195
  const requested = requiredBounded(projectRoot, "project_root", TASK_PROJECT_ROOT_MAX_LENGTH);
192
- if (note.extra["projectRoot"] !== requested) throw new Error(`note "${note.id}" is outside project scope`);
196
+ if (note.extra.projectRoot !== requested) throw new Error(`note "${note.id}" is outside project scope`);
193
197
  }
194
198
  }