@danypops/papyrus 0.3.0 → 0.5.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.
- package/README.md +19 -7
- package/extension/src/beautiful-mermaid-renderer.ts +23 -0
- package/extension/src/domain-tools.ts +39 -8
- package/extension/src/index.ts +17 -26
- package/extension/src/skills.ts +61 -2
- package/extension/src/task-detail-format.ts +24 -1
- package/extension/src/task-detail-view.ts +6 -3
- package/extension/src/task-graph.ts +11 -2
- package/extension/src/tasks.ts +7 -5
- package/package.json +1 -1
- package/src/adapters/sqlite-artifact-store.ts +7 -2
- package/src/adapters/sqlite-task-event-store.ts +92 -0
- package/src/cli.ts +82 -8
- package/src/constants.ts +18 -1
- package/src/db.ts +102 -29
- package/src/domain/skill-definition.ts +15 -11
- package/src/domain/task-event.ts +102 -0
- package/src/domain-services.ts +31 -0
- package/src/ports/atomic-artifact-store.ts +13 -0
- package/src/ports/task-event-store.ts +43 -0
- package/src/service.ts +66 -15
- package/src/skill-execution.ts +220 -0
- package/src/task-service.ts +120 -53
package/src/task-service.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { TASK_EXECUTION_MAX_DEGREE, TASK_EXECUTION_MAX_EDGES, TASK_EXECUTION_MAX
|
|
|
2
2
|
import type { Artifact } from "./domain/artifact.ts";
|
|
3
3
|
import { checklistEntries, validateChecklist, type Checklist, type ProofReference } from "./domain/checklist.ts";
|
|
4
4
|
import type { Gate, GateResult } from "./domain/gate.ts";
|
|
5
|
+
import type { AppendTaskEvent, TaskEventContext, TaskHistoryPage, TaskHistoryQuery, TaskLifecycleStatus } from "./domain/task-event.ts";
|
|
5
6
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
6
7
|
import type { GateRunner } from "./ports/gate-runner.ts";
|
|
7
8
|
import { InMemoryTaskFocusStore, type TaskFocusStore } from "./ports/task-focus-store.ts";
|
|
9
|
+
import { InMemoryTaskEventStore, type TaskEventStore } from "./ports/task-event-store.ts";
|
|
8
10
|
import { assertDependencyEdgeAllowed } from "./task-execution.ts";
|
|
9
11
|
|
|
10
12
|
export interface TaskFilter {
|
|
@@ -13,11 +15,13 @@ export interface TaskFilter {
|
|
|
13
15
|
limit?: number;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
export type TaskStatus =
|
|
18
|
+
export type TaskStatus = TaskLifecycleStatus;
|
|
17
19
|
|
|
18
20
|
export interface CreateTaskInput {
|
|
21
|
+
id?: string;
|
|
19
22
|
title: string;
|
|
20
23
|
body?: string;
|
|
24
|
+
subtype?: string;
|
|
21
25
|
status?: TaskStatus;
|
|
22
26
|
labels?: string[];
|
|
23
27
|
extra?: Record<string, unknown>;
|
|
@@ -77,6 +81,7 @@ export class Tasks {
|
|
|
77
81
|
private readonly artifacts: ArtifactStore,
|
|
78
82
|
private readonly gates: GateRunner,
|
|
79
83
|
private readonly focusStore: TaskFocusStore = new InMemoryTaskFocusStore(),
|
|
84
|
+
private readonly events: TaskEventStore = new InMemoryTaskEventStore(),
|
|
80
85
|
) {}
|
|
81
86
|
|
|
82
87
|
private require(id: string): Artifact {
|
|
@@ -86,27 +91,32 @@ export class Tasks {
|
|
|
86
91
|
return artifact;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
create(input: CreateTaskInput): Artifact {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
94
|
+
create(input: CreateTaskInput, context: TaskEventContext = {}): Artifact {
|
|
95
|
+
return this.events.atomic(() => {
|
|
96
|
+
if ((input.dependsOn?.length ?? 0) > TASK_EXECUTION_MAX_DEGREE) {
|
|
97
|
+
throw new Error(`task cannot exceed ${TASK_EXECUTION_MAX_DEGREE} prerequisites`);
|
|
98
|
+
}
|
|
99
|
+
if (input.parentId) this.require(input.parentId);
|
|
100
|
+
for (const dependency of input.dependsOn ?? []) this.require(dependency);
|
|
101
|
+
const extra: Record<string, unknown> = { ...(input.extra ?? {}) };
|
|
102
|
+
if (input.gates !== undefined) extra["gates"] = input.gates;
|
|
103
|
+
if (input.checklist !== undefined) extra["checklist"] = validateChecklist(input.checklist);
|
|
104
|
+
const task = this.artifacts.create({
|
|
105
|
+
id: input.id,
|
|
106
|
+
kind: "task",
|
|
107
|
+
title: input.title,
|
|
108
|
+
body: input.body,
|
|
109
|
+
subtype: input.subtype,
|
|
110
|
+
status: input.status,
|
|
111
|
+
labels: input.labels,
|
|
112
|
+
extra,
|
|
113
|
+
templateId: input.templateId,
|
|
114
|
+
});
|
|
115
|
+
if (input.parentId) this.contain(input.parentId, task.id);
|
|
116
|
+
for (const dependency of input.dependsOn ?? []) this.depend(task.id, dependency);
|
|
117
|
+
this.appendEvent({ taskId: task.id, type: "created", toStatus: task.status as TaskStatus }, context);
|
|
118
|
+
return this.show(task.id);
|
|
106
119
|
});
|
|
107
|
-
if (input.parentId) this.contain(input.parentId, task.id);
|
|
108
|
-
for (const dependency of input.dependsOn ?? []) this.depend(task.id, dependency);
|
|
109
|
-
return this.show(task.id);
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
list(filter: TaskFilter = {}): Artifact[] {
|
|
@@ -185,48 +195,55 @@ export class Tasks {
|
|
|
185
195
|
return task;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
transition(id: string, action: TaskTransition): Artifact {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
transition(id: string, action: TaskTransition, context: TaskEventContext = {}): Artifact {
|
|
199
|
+
return this.events.atomic(() => {
|
|
200
|
+
const task = this.require(id);
|
|
201
|
+
const transition = TASK_TRANSITIONS[action];
|
|
202
|
+
if (!transition.from.includes(task.status as TaskStatus)) throw new Error(`cannot ${action} task from ${task.status}`);
|
|
203
|
+
if (action === "start") {
|
|
204
|
+
const blocking = this.dependencyIds(id).filter((dependencyId) => this.require(dependencyId).status !== "done");
|
|
205
|
+
if (blocking.length > 0) throw new Error(`task "${id}" is blocked by dependencies: ${blocking.join(", ")}`);
|
|
206
|
+
this.focusStore.set(id);
|
|
207
|
+
}
|
|
208
|
+
const updated = this.artifacts.setStatus(id, transition.to)!;
|
|
209
|
+
const eventType = { start: "started", submit: "submitted", reject: "review_rejected", retry: "retried", cancel: "canceled" }[action] as AppendTaskEvent["type"];
|
|
210
|
+
this.appendEvent({ taskId: id, type: eventType, fromStatus: task.status as TaskStatus, toStatus: transition.to }, context);
|
|
211
|
+
if (action === "start" || action === "retry") this.propagateProgressToAncestors(id, context);
|
|
212
|
+
if (action === "retry") this.focusStore.set(id);
|
|
213
|
+
if (action === "cancel") this.focusStore.clear(id);
|
|
214
|
+
return updated;
|
|
215
|
+
});
|
|
202
216
|
}
|
|
203
217
|
|
|
204
|
-
complete(id: string): TaskCompletion {
|
|
218
|
+
complete(id: string, context: TaskEventContext = {}): TaskCompletion {
|
|
205
219
|
const task = this.requireReview(id);
|
|
220
|
+
const attemptId = crypto.randomUUID();
|
|
221
|
+
this.events.atomic(() => this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context));
|
|
206
222
|
const checklist = this.reviewChecklist(task);
|
|
207
223
|
const results = this.gates.run(id);
|
|
208
|
-
|
|
209
|
-
const artifact = this.artifacts.setStatus(id, "rejected")!;
|
|
210
|
-
return { artifact, gates: results, checklist, completed: false, focused: this.active(), blocked: [] };
|
|
211
|
-
}
|
|
212
|
-
return this.finish(id, results, checklist);
|
|
224
|
+
return this.resolveCompletion(id, attemptId, results, checklist, context);
|
|
213
225
|
}
|
|
214
226
|
|
|
215
|
-
async completeAsync(id: string): Promise<TaskCompletion> {
|
|
227
|
+
async completeAsync(id: string, context: TaskEventContext = {}): Promise<TaskCompletion> {
|
|
216
228
|
const task = this.requireReview(id);
|
|
229
|
+
const attemptId = crypto.randomUUID();
|
|
230
|
+
this.events.atomic(() => this.appendEvent({ taskId: id, type: "completion_attempted", fromStatus: "review", toStatus: "review", attemptId }, context));
|
|
217
231
|
const checklist = this.reviewChecklist(task);
|
|
218
232
|
const results = await this.gates.runAsync(id);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
233
|
+
this.requireReview(id);
|
|
234
|
+
return this.resolveCompletion(id, attemptId, results, checklist, context);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async runGates(id: string, context: TaskEventContext = {}): Promise<GateResult[]> {
|
|
238
|
+
this.require(id);
|
|
239
|
+
const results = await this.gates.runAsync(id);
|
|
240
|
+
this.events.atomic(() => this.appendEvent({ taskId: id, type: "gates_evaluated", evidence: { gates: results, result: results.every((gate) => gate.passed) ? "passed" : "failed" } }, context));
|
|
241
|
+
return results;
|
|
225
242
|
}
|
|
226
243
|
|
|
227
|
-
|
|
244
|
+
history(id: string, query: TaskHistoryQuery = {}): TaskHistoryPage {
|
|
228
245
|
this.require(id);
|
|
229
|
-
return this.
|
|
246
|
+
return this.events.history(id, query);
|
|
230
247
|
}
|
|
231
248
|
|
|
232
249
|
setChecklist(id: string, checklist: Checklist): Artifact {
|
|
@@ -282,7 +299,7 @@ export class Tasks {
|
|
|
282
299
|
.filter((parentId, index, ids) => ids.indexOf(parentId) === index);
|
|
283
300
|
}
|
|
284
301
|
|
|
285
|
-
private propagateProgressToAncestors(id: string): void {
|
|
302
|
+
private propagateProgressToAncestors(id: string, context: TaskEventContext): void {
|
|
286
303
|
const pending = this.parentIds(id);
|
|
287
304
|
const visited = new Set<string>();
|
|
288
305
|
while (pending.length > 0) {
|
|
@@ -291,7 +308,14 @@ export class Tasks {
|
|
|
291
308
|
if (visited.size >= TASK_EXECUTION_MAX_NODES) throw new Error("task ancestry exceeds execution node bound");
|
|
292
309
|
visited.add(parentId);
|
|
293
310
|
const parent = this.require(parentId);
|
|
294
|
-
if (parent.status === "todo")
|
|
311
|
+
if (parent.status === "todo") {
|
|
312
|
+
this.artifacts.setStatus(parentId, "in-progress");
|
|
313
|
+
this.appendEvent({ taskId: parentId, type: "started", fromStatus: "todo", toStatus: "in-progress" }, {
|
|
314
|
+
...context,
|
|
315
|
+
source: "task-ancestry",
|
|
316
|
+
reason: `nested task ${id} entered progress`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
295
319
|
pending.push(...this.parentIds(parentId));
|
|
296
320
|
}
|
|
297
321
|
}
|
|
@@ -315,7 +339,32 @@ export class Tasks {
|
|
|
315
339
|
return ids;
|
|
316
340
|
}
|
|
317
341
|
|
|
318
|
-
private
|
|
342
|
+
private resolveCompletion(
|
|
343
|
+
id: string,
|
|
344
|
+
attemptId: string,
|
|
345
|
+
gates: GateResult[],
|
|
346
|
+
checklist: ChecklistReview[],
|
|
347
|
+
context: TaskEventContext,
|
|
348
|
+
): TaskCompletion {
|
|
349
|
+
const failed = gates.some((gate) => !gate.passed) || checklist.some((item) => !item.accepted);
|
|
350
|
+
if (failed) {
|
|
351
|
+
return this.events.atomic(() => {
|
|
352
|
+
const artifact = this.artifacts.setStatus(id, "rejected")!;
|
|
353
|
+
this.appendEvent({
|
|
354
|
+
taskId: id,
|
|
355
|
+
type: "review_rejected",
|
|
356
|
+
fromStatus: "review",
|
|
357
|
+
toStatus: "rejected",
|
|
358
|
+
attemptId,
|
|
359
|
+
evidence: { gates, checklist, result: "rejected" },
|
|
360
|
+
}, context);
|
|
361
|
+
return { artifact, gates, checklist, completed: false, focused: this.active(), blocked: [] };
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return this.events.atomic(() => this.finish(id, attemptId, gates, checklist, context));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private finish(id: string, attemptId: string, gates: GateResult[], checklist: ChecklistReview[], context: TaskEventContext): TaskCompletion {
|
|
319
368
|
const successorIds = this.relationships(id)
|
|
320
369
|
.filter((edge) => edge.relation === "depends_on" && edge.to === id)
|
|
321
370
|
.map((edge) => edge.from);
|
|
@@ -323,6 +372,14 @@ export class Tasks {
|
|
|
323
372
|
throw new Error(`task "${id}" exceeds ${TASK_EXECUTION_MAX_DEGREE} successors`);
|
|
324
373
|
}
|
|
325
374
|
const artifact = this.artifacts.setStatus(id, "done")!;
|
|
375
|
+
this.appendEvent({
|
|
376
|
+
taskId: id,
|
|
377
|
+
type: "completed",
|
|
378
|
+
fromStatus: "review",
|
|
379
|
+
toStatus: "done",
|
|
380
|
+
attemptId,
|
|
381
|
+
evidence: { gates, checklist, result: "completed" },
|
|
382
|
+
}, context);
|
|
326
383
|
this.focusStore.clear(id);
|
|
327
384
|
const blocked: TaskBlockage[] = [];
|
|
328
385
|
let focused: Artifact | null = null;
|
|
@@ -343,6 +400,16 @@ export class Tasks {
|
|
|
343
400
|
return { artifact, gates, checklist, completed: true, focused, blocked };
|
|
344
401
|
}
|
|
345
402
|
|
|
403
|
+
private appendEvent(event: Omit<AppendTaskEvent, "actor" | "source">, context: TaskEventContext): void {
|
|
404
|
+
this.events.append({
|
|
405
|
+
...event,
|
|
406
|
+
actor: context.actor ?? "system",
|
|
407
|
+
source: context.source ?? "task-domain",
|
|
408
|
+
...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),
|
|
409
|
+
...(context.reason === undefined ? {} : { reason: context.reason }),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
346
413
|
private requireReview(id: string): Artifact {
|
|
347
414
|
const task = this.require(id);
|
|
348
415
|
if (task.status !== "review") throw new Error(`cannot complete task from ${task.status}`);
|