@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/README.md
CHANGED
|
@@ -62,11 +62,20 @@ Each kind has an enforced status vocabulary. Every edge endpoint must exist, and
|
|
|
62
62
|
|
|
63
63
|
### Hierarchy and traversal
|
|
64
64
|
|
|
65
|
-
Use `contains` and `part_of` for explicit parent/child structure; use `depends_on` for execution ordering. Dependency edges form an executable DAG: self-dependencies and cycles are rejected, fan-in waits for every prerequisite, and fan-out
|
|
65
|
+
Use `contains` and `part_of` for explicit parent/child structure; use `depends_on` for execution ordering. Dependency edges form an executable DAG: self-dependencies and cycles are rejected, fan-in waits for every prerequisite, and fan-out can expose several ready successors while active focus remains singular. Graph reads are cycle-safe and bounded by `depth` and `max_nodes` (defaults: depth 4, 100 nodes; hard ceilings: depth 20, 1,000 nodes). Executable task plans are additionally bounded to 1,000 tasks and 10,000 relationships.
|
|
66
66
|
|
|
67
67
|
### Skills and compatibility templates
|
|
68
68
|
|
|
69
|
-
A Papyrus Skill is distinct from a conventional prompt-only skill: its input API and
|
|
69
|
+
A Papyrus Skill is distinct from a conventional prompt-only skill: its input API and blueprints define a connected Task/Rule/Doc workflow. `skills.run` validates and normalizes all arguments, safely renders placeholders in memory, validates the complete graph, then persists artifacts and edges in one transaction. Task dependencies, containment, gates, checklists, and context survive rendering. Run Rules are injected only while active focus belongs to that run. Docs retain invocation context and provenance; missing evidence references remain unknown and no gate runs during instantiation.
|
|
70
|
+
|
|
71
|
+
A run result has a stable schema: Skill ID, run ID, normalized arguments, created IDs grouped by kind, ready root task IDs, and the bounded execution plan. Explicit run IDs produce deterministic artifact IDs (`<run-id>-<blueprint-ref>`); collisions roll back the entire run.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
papyrus skills run <skill-id> \
|
|
75
|
+
--arguments-json '{"project":"Papyrus"}' \
|
|
76
|
+
--run-id papyrus-001 \
|
|
77
|
+
--json
|
|
78
|
+
```
|
|
70
79
|
|
|
71
80
|
The existing `artifact-template` skill subtype remains a compatibility mechanism for one-artifact templates with metadata `{targetKind, defaults, required}`. Instantiate it through `papyrus_create` with `template_id`; defaults merge recursively, explicit arrays replace defaults, required paths such as `extra.owner` are validated, and target-kind mismatches are rejected.
|
|
72
81
|
|
|
@@ -84,7 +93,7 @@ Agent-facing domain tools own lifecycle invariants and sit above this store API:
|
|
|
84
93
|
- **`tasks`** — create/list/show/plan, manage the singleton active focus, replace evidence-bearing checklists, hierarchy/dependencies, lifecycle transitions, non-blocking gates, and review completion that focuses one deterministic ready successor without claiming effort
|
|
85
94
|
- **`docs`** — create/list/show, activate/archive/reopen, and document-safe graph links
|
|
86
95
|
- **`rules`** — create/list/show/preview, enable/disable, and attach governance gates to tasks
|
|
87
|
-
- **`skills`** — create/list/show/invoke, enable/disable, create templates, and instantiate
|
|
96
|
+
- **`skills`** — create/list/show/invoke/run, enable/disable, create compatibility templates, and atomically instantiate parameterized workflow runs
|
|
88
97
|
|
|
89
98
|
Every tool operation is registered in the daemon’s `/api/v1/ops` registry; parity is verified in tests. The task consumer uses the `tasks.graph` operation, which returns task nodes with explicit parent, child, and dependency IDs rather than leaking SQLite rows or asking the UI to reconstruct relationships.
|
|
90
99
|
|
|
@@ -92,7 +101,7 @@ Internally, application services depend on the `ArtifactStore` and `GateRunner`
|
|
|
92
101
|
|
|
93
102
|
## Interactive frontends
|
|
94
103
|
|
|
95
|
-
- `/tasks` — task lifecycle, gates, dependencies, and nested metadata
|
|
104
|
+
- `/tasks` — task lifecycle, append-only history, gates, dependencies, and nested metadata
|
|
96
105
|
- `/docs` — searchable documents, lifecycle, details, and graph links
|
|
97
106
|
- `/rules` — severity/condition rows, exact injection preview, enable/disable, and task gating
|
|
98
107
|
- `/skills` — trigger/tools rows, invocation into the editor, and artifact templates
|
|
@@ -105,6 +114,7 @@ Run `/tasks` for the interactive task panel:
|
|
|
105
114
|
|
|
106
115
|
- `/` filters; arrow keys navigate; Enter opens task actions
|
|
107
116
|
- `g` opens the programmatic Unicode graph; Tab switches dependency/composition views and arrow keys pan
|
|
117
|
+
- routed graph layouts are bounded to 48 nodes/96 edges; larger graphs use a deterministic, box-drawn line fallback, and renderer failures are contained inside the viewport rather than escaping Pi
|
|
108
118
|
- advance the `todo → in-progress → review → done` lifecycle; failed review becomes `rejected`, retry returns to `in-progress`, and `canceled` is terminal
|
|
109
119
|
- use **active** only as the independent singleton focus that auto-drive continues; focusing a task never changes its lifecycle
|
|
110
120
|
- starting nested effort moves todo ancestors to in-progress; submitting enters review; completing review checks both typed checklist proofs and executable gates
|
|
@@ -112,13 +122,15 @@ Run `/tasks` for the interactive task panel:
|
|
|
112
122
|
- successors are never auto-completed; fan-in, fan-out, diamonds, and disconnected DAGs remain explicit
|
|
113
123
|
- inspect deterministic execution layers, readiness, a box-drawn nested hierarchy, composition, dependencies, evidence-bearing checklists, and verification gates
|
|
114
124
|
- lifecycle colors are semantic and redundant with text/glyphs: To-Do grey, in-progress yellow, review blue, rejected orange, done green, and canceled red; `▶` marks active focus
|
|
115
|
-
- Show details keeps Checklist and Validation gates separate from incidental Metadata, then renders relationships as a Unicode box-drawing graph footer; `↑/↓` scrolls and `←/→` pans wide graphs
|
|
125
|
+
- Show details keeps Checklist and Validation gates separate from incidental Metadata, renders bounded post-migration lifecycle history with actor/source/reason and gate evidence, then renders relationships as a Unicode box-drawing graph footer; `↑/↓` scrolls and `←/→` pans wide graphs
|
|
116
126
|
- the compact persistent widget shows bounded open work in containment order and always retains the active focus
|
|
117
127
|
|
|
118
128
|
Authenticated CLI parity covers the changed lifecycle and focus operations:
|
|
119
129
|
|
|
120
130
|
```bash
|
|
131
|
+
papyrus tasks graph --json
|
|
121
132
|
papyrus tasks active --json
|
|
133
|
+
papyrus tasks history <id> --json
|
|
122
134
|
papyrus tasks focus <id> --json
|
|
123
135
|
papyrus tasks start <id> --json
|
|
124
136
|
papyrus tasks submit <id> --json
|
|
@@ -160,10 +172,10 @@ packed install npm:@danypops/papyrus
|
|
|
160
172
|
~/.pi/agent/npm/node_modules/.bin/papyrus service install
|
|
161
173
|
```
|
|
162
174
|
|
|
163
|
-
Existing databases are never migrated on daemon boot. After upgrading
|
|
175
|
+
Existing databases are never migrated on daemon boot. After upgrading to append-only task history, run the authenticated CLI migration explicitly. A v1 database receives the lifecycle prerequisite and history schema in one transaction; existing tasks receive no fabricated events:
|
|
164
176
|
|
|
165
177
|
```bash
|
|
166
|
-
~/.pi/agent/npm/node_modules/.bin/papyrus migrate task-
|
|
178
|
+
~/.pi/agent/npm/node_modules/.bin/papyrus migrate task-history
|
|
167
179
|
```
|
|
168
180
|
|
|
169
181
|
Until that command succeeds, health reports `migrationRequired` and normal domain operations are rejected with actionable guidance. Migration is not exposed as a Pi tool or MCP action. New empty databases bootstrap directly at the current schema.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { renderMermaidASCII } from "beautiful-mermaid";
|
|
2
2
|
import {
|
|
3
3
|
GRAPH_RENDER_BOX_PADDING,
|
|
4
|
+
GRAPH_RENDER_MAX_FALLBACK_LINES,
|
|
5
|
+
GRAPH_RENDER_MAX_ROUTED_EDGES,
|
|
6
|
+
GRAPH_RENDER_MAX_ROUTED_NODES,
|
|
4
7
|
GRAPH_RENDER_PADDING_X,
|
|
5
8
|
GRAPH_RENDER_PADDING_Y,
|
|
6
9
|
} from "../../src/constants.ts";
|
|
@@ -30,9 +33,29 @@ export function mermaidSource(graph: DisplayGraph): string {
|
|
|
30
33
|
return lines.join("\n");
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
function boundedLineFallback(graph: DisplayGraph): RenderedGraph {
|
|
37
|
+
const candidates = [
|
|
38
|
+
"┌─ Task graph ─",
|
|
39
|
+
`│ ${graph.nodes.length} nodes · ${graph.edges.length} edges · routed layout skipped above ${GRAPH_RENDER_MAX_ROUTED_NODES} nodes`,
|
|
40
|
+
"├─ Nodes",
|
|
41
|
+
...graph.nodes.map((node) => `│ ${node.label}`),
|
|
42
|
+
"├─ Edges",
|
|
43
|
+
...graph.edges.map((edge) => `│ ${edge.from} ─${edge.label ? `${edge.label}─` : ""}→ ${edge.to}`),
|
|
44
|
+
];
|
|
45
|
+
const contentLimit = Math.max(1, GRAPH_RENDER_MAX_FALLBACK_LINES - 1);
|
|
46
|
+
const lines = candidates.slice(0, contentLimit);
|
|
47
|
+
const omitted = candidates.length - lines.length;
|
|
48
|
+
if (omitted > 0) lines[lines.length - 1] = `│ … ${omitted + 1} lines omitted`;
|
|
49
|
+
lines.push("└─");
|
|
50
|
+
return { lines };
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
export class BeautifulMermaidRenderer implements GraphRenderer {
|
|
34
54
|
render(graph: DisplayGraph): RenderedGraph {
|
|
35
55
|
if (graph.nodes.length === 0) return { lines: [] };
|
|
56
|
+
if (graph.nodes.length > GRAPH_RENDER_MAX_ROUTED_NODES || graph.edges.length > GRAPH_RENDER_MAX_ROUTED_EDGES) {
|
|
57
|
+
return boundedLineFallback(graph);
|
|
58
|
+
}
|
|
36
59
|
const output = renderMermaidASCII(mermaidSource(graph), {
|
|
37
60
|
useAscii: false,
|
|
38
61
|
paddingX: GRAPH_RENDER_PADDING_X,
|
|
@@ -4,7 +4,9 @@ import type { Artifact } from "../../src/domain/artifact.ts";
|
|
|
4
4
|
import { PROOF_TYPES } from "../../src/domain/checklist.ts";
|
|
5
5
|
import type { GateResult } from "../../src/domain/gate.ts";
|
|
6
6
|
import type { TaskExecutionPlan } from "../../src/task-execution.ts";
|
|
7
|
-
import type {
|
|
7
|
+
import type { TaskHistoryPage } from "../../src/domain/task-event.ts";
|
|
8
|
+
import type { TaskCompletion, TaskGraph } from "../../src/task-service.ts";
|
|
9
|
+
import type { SkillWorkflowRunResult } from "../../src/skill-execution.ts";
|
|
8
10
|
import { callService } from "./service-client.ts";
|
|
9
11
|
|
|
10
12
|
function text(message: string, details: Record<string, unknown> = {}) {
|
|
@@ -29,7 +31,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
29
31
|
pi.registerTool({
|
|
30
32
|
name: "tasks",
|
|
31
33
|
label: "Tasks",
|
|
32
|
-
description: "Task domain tool. ACTIONS: create, list, show, plan, active, focus, start, submit, complete, reject, retry, cancel, run_gates, set_checklist, depend, contain. Lifecycle is todo → in-progress → review → done, with review failure → rejected and retry → in-progress; canceled is terminal. Active focus is independent and identifies the one task auto-drive continues. Completion runs gates and checklist-proof review, then focuses one deterministic ready successor without claiming effort. Dependency cycles are rejected. Prefer this over low-level papyrus_* tools for task work.",
|
|
34
|
+
description: "Task domain tool. ACTIONS: create, list, show, history, graph, plan, active, focus, start, submit, complete, reject, retry, cancel, run_gates, set_checklist, depend, contain. Lifecycle is todo → in-progress → review → done, with review failure → rejected and retry → in-progress; canceled is terminal. Active focus is independent and identifies the one task auto-drive continues. Completion runs gates and checklist-proof review, then focuses one deterministic ready successor without claiming effort. Dependency cycles are rejected. Prefer this over low-level papyrus_* tools for task work.",
|
|
33
35
|
parameters: Type.Object({
|
|
34
36
|
action: Type.String(),
|
|
35
37
|
id: Type.Optional(Type.String()),
|
|
@@ -38,6 +40,10 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
38
40
|
status: Type.Optional(Type.String()),
|
|
39
41
|
text: Type.Optional(Type.String()),
|
|
40
42
|
limit: Type.Optional(Type.Number()),
|
|
43
|
+
cursor: Type.Optional(Type.Number()),
|
|
44
|
+
direction: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")])),
|
|
45
|
+
reason: Type.Optional(Type.String()),
|
|
46
|
+
session_id: Type.Optional(Type.String()),
|
|
41
47
|
labels: Type.Optional(Type.Array(Type.String())),
|
|
42
48
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
43
49
|
gates: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Unknown()))),
|
|
@@ -51,8 +57,9 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
51
57
|
async execute(_id, params) {
|
|
52
58
|
try {
|
|
53
59
|
const action = params.action;
|
|
60
|
+
const request = { ...params, actor: "agent", source: "pi-tool" };
|
|
54
61
|
if (action === "create") {
|
|
55
|
-
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create",
|
|
62
|
+
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.create", request);
|
|
56
63
|
return text(`Created task ${artifactLine(artifact)}`, { artifact });
|
|
57
64
|
}
|
|
58
65
|
if (action === "list") {
|
|
@@ -63,10 +70,21 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
63
70
|
const artifact = await callService<Record<string, unknown>, Artifact>("tasks.show", params);
|
|
64
71
|
return text(`${artifactLine(artifact)}\n\n${artifact.body}`, { artifact });
|
|
65
72
|
}
|
|
73
|
+
if (action === "history") {
|
|
74
|
+
const page = await callService<Record<string, unknown>, TaskHistoryPage>("tasks.history", request);
|
|
75
|
+
const lines = page.events.map((event) => `${event.occurredAt} ${event.type} ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"} · ${event.actor}/${event.source}${event.reason ? ` · ${event.reason}` : ""}`);
|
|
76
|
+
return text(lines.join("\n") || "No recorded history for this task.", { page });
|
|
77
|
+
}
|
|
66
78
|
if (action === "active") {
|
|
67
79
|
const artifact = await callService<Record<string, unknown>, Artifact | null>("tasks.active", params);
|
|
68
80
|
return text(artifact ? `Active: ${artifactLine(artifact)}` : "No active task.", { artifact });
|
|
69
81
|
}
|
|
82
|
+
if (action === "graph") {
|
|
83
|
+
const graph = await callService<Record<string, unknown>, TaskGraph>("tasks.graph", params);
|
|
84
|
+
const dependencies = graph.nodes.reduce((count, node) => count + node.dependencyIds.length, 0);
|
|
85
|
+
const containment = graph.nodes.reduce((count, node) => count + node.childIds.length, 0);
|
|
86
|
+
return text(`Task graph: ${graph.nodes.length} nodes, ${graph.rootIds.length} roots, ${dependencies} dependencies, ${containment} containment edges.`, { graph });
|
|
87
|
+
}
|
|
70
88
|
if (action === "plan") {
|
|
71
89
|
const plan = await callService<Record<string, unknown>, TaskExecutionPlan>("tasks.plan", params);
|
|
72
90
|
const byId = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
@@ -85,7 +103,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
85
103
|
return text(`Updated checklist: ${artifactLine(artifact)}`, { artifact });
|
|
86
104
|
}
|
|
87
105
|
if (action === "complete") {
|
|
88
|
-
const result = await callService<Record<string, unknown>, TaskCompletion>("tasks.complete",
|
|
106
|
+
const result = await callService<Record<string, unknown>, TaskCompletion>("tasks.complete", request);
|
|
89
107
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n");
|
|
90
108
|
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}${item.reason ? ` — ${item.reason}` : ""}`).join("\n");
|
|
91
109
|
const focused = result.focused ? `\nActive: ${artifactLine(result.focused)}` : "";
|
|
@@ -95,7 +113,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
95
113
|
return text(`${result.completed ? "Completed" : "Rejected"}: ${artifactLine(result.artifact)}${focused}${blocked}${checklist ? `\n${checklist}` : ""}${gates ? `\n${gates}` : ""}`, { ...result });
|
|
96
114
|
}
|
|
97
115
|
if (action === "run_gates") {
|
|
98
|
-
const gates = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates",
|
|
116
|
+
const gates = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", request);
|
|
99
117
|
return text(gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n") || "No gates configured.", { gates });
|
|
100
118
|
}
|
|
101
119
|
const operations = {
|
|
@@ -110,7 +128,7 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
110
128
|
} as const;
|
|
111
129
|
const operation = operations[action as keyof typeof operations];
|
|
112
130
|
if (!operation) return text(`Unknown tasks action: ${action}`);
|
|
113
|
-
const artifact = await callService<Record<string, unknown>, Artifact>(operation,
|
|
131
|
+
const artifact = await callService<Record<string, unknown>, Artifact>(operation, request);
|
|
114
132
|
return text(artifactLine(artifact), { artifact });
|
|
115
133
|
} catch (error) {
|
|
116
134
|
return text(`tasks failed: ${error instanceof Error ? error.message : error}`);
|
|
@@ -203,11 +221,13 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
203
221
|
pi.registerTool({
|
|
204
222
|
name: "skills",
|
|
205
223
|
label: "Skills",
|
|
206
|
-
description: "Papyrus Skill workflow and compatibility-template domain tool. Papyrus Skills are parameterized Task/Rule/Doc bundles, distinct from prompt-only skills. ACTIONS: create, create_template, list, show, invoke, enable, disable, instantiate.",
|
|
224
|
+
description: "Papyrus Skill workflow and compatibility-template domain tool. Papyrus Skills are parameterized Task/Rule/Doc bundles, distinct from prompt-only skills. ACTIONS: create, create_template, list, show, invoke, run, enable, disable, instantiate. run validates arguments and atomically creates one scoped workflow run.",
|
|
207
225
|
parameters: Type.Object({
|
|
208
226
|
action: Type.String(), id: Type.Optional(Type.String()), title: Type.Optional(Type.String()),
|
|
209
227
|
body: Type.Optional(Type.String()), trigger: Type.Optional(Type.String()), steps: Type.Optional(Type.Array(Type.String())),
|
|
210
|
-
tools: Type.Optional(Type.Array(Type.String())),
|
|
228
|
+
tools: Type.Optional(Type.Array(Type.String())), definition: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
229
|
+
arguments: Type.Optional(Type.Record(Type.String(), Type.Unknown())), run_id: Type.Optional(Type.String()),
|
|
230
|
+
labels: Type.Optional(Type.Array(Type.String())),
|
|
211
231
|
extra: Type.Optional(Type.Record(Type.String(), Type.Unknown())), status: Type.Optional(Type.String()),
|
|
212
232
|
text: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), template_id: Type.Optional(Type.String()),
|
|
213
233
|
target_kind: Type.Optional(Type.String()), defaults: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
@@ -229,6 +249,17 @@ export function registerDomainTools(pi: ExtensionAPI): void {
|
|
|
229
249
|
const invocation = await callService<Record<string, unknown>, string>("skills.invoke", params);
|
|
230
250
|
return text(invocation, { invocation });
|
|
231
251
|
}
|
|
252
|
+
if (action === "run") {
|
|
253
|
+
const run = await callService<Record<string, unknown>, SkillWorkflowRunResult>("skills.run", params);
|
|
254
|
+
const execution = run.execution.nodes.map((node) => ` [${node.state}] ${node.id} ${node.title}`).join("\n");
|
|
255
|
+
return text([
|
|
256
|
+
`Created Skill run ${run.runId}: ${run.created.tasks.length} tasks, ${run.created.rules.length} rules, ${run.created.docs.length} docs.`,
|
|
257
|
+
`Ready roots: ${run.rootTaskIds.join(", ") || "none"}.`,
|
|
258
|
+
`Context docs: ${run.created.docs.join(", ") || "none"}.`,
|
|
259
|
+
`Scoped rules: ${run.created.rules.join(", ") || "none"}.`,
|
|
260
|
+
...(execution ? ["Execution:", execution] : []),
|
|
261
|
+
].join("\n"), { run });
|
|
262
|
+
}
|
|
232
263
|
const operations = { show: "skills.show", enable: "skills.enable", disable: "skills.disable", instantiate: "skills.instantiate" } as const;
|
|
233
264
|
const operation = operations[action as keyof typeof operations];
|
|
234
265
|
if (!operation) return text(`Unknown skills action: ${action}`);
|
package/extension/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { callService } from "./service-client.ts";
|
|
|
21
21
|
import { registerDomainTools } from "./domain-tools.ts";
|
|
22
22
|
import type { TaskGraph, TaskStatus } from "../../src/task-service.ts";
|
|
23
23
|
import { ActiveTaskContinuation, type ActiveTaskMarker } from "./active-task-continuation.ts";
|
|
24
|
-
import { buildTaskWidgetProjection } from "./task-widget.ts";
|
|
24
|
+
import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task-widget.ts";
|
|
25
25
|
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
26
26
|
|
|
27
27
|
function text(t: string, details: Record<string, unknown> = {}) {
|
|
@@ -34,6 +34,21 @@ function text(t: string, details: Record<string, unknown> = {}) {
|
|
|
34
34
|
|
|
35
35
|
const WIDGET_KEY = "pi-papyrus";
|
|
36
36
|
|
|
37
|
+
export function renderTaskWidgetLines(theme: Theme, projection: TaskWidgetProjection, width: number): string[] {
|
|
38
|
+
if (projection.openTotal === 0) return [];
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
for (let index = 0; index < projection.rows.length; index++) {
|
|
41
|
+
const row = projection.rows[index]!;
|
|
42
|
+
const laterSibling = projection.rows.slice(index + 1).some((candidate) => candidate.depth === row.depth);
|
|
43
|
+
const hierarchy = taskTreeConnector({ depth: row.depth, hasChildren: row.hasOpenChildren, hasLaterSibling: laterSibling });
|
|
44
|
+
const focus = row.active ? theme.fg("accent", "▶") : " ";
|
|
45
|
+
const presentation = TASK_STATUS_PRESENTATION[row.task.status as TaskStatus];
|
|
46
|
+
const glyph = presentation ? theme.fg(presentation.color, presentation.glyph) : theme.fg("muted", "?");
|
|
47
|
+
lines.push(truncateToWidth(`${focus} ${hierarchy} ${glyph} ${row.task.title}`, width, "…"));
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
class TaskOverlay {
|
|
38
53
|
private uiCtx: ExtensionUIContext | undefined;
|
|
39
54
|
private registered = false;
|
|
@@ -93,31 +108,7 @@ class TaskOverlay {
|
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
private renderLines(theme: Theme, width: number): string[] {
|
|
96
|
-
|
|
97
|
-
if (projection.total === 0) return [];
|
|
98
|
-
|
|
99
|
-
if (projection.openTotal === 0) {
|
|
100
|
-
return [truncateToWidth(theme.bold("Tasks · no open tasks · /tasks"), width, "…")];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const active = projection.rows.find((row) => row.active);
|
|
104
|
-
const lines = [
|
|
105
|
-
truncateToWidth(
|
|
106
|
-
theme.bold(`Tasks · ${active ? theme.fg("accent", "▶ active") : "no active focus"} · ${projection.openTotal} open`),
|
|
107
|
-
width,
|
|
108
|
-
"…",
|
|
109
|
-
),
|
|
110
|
-
];
|
|
111
|
-
for (let index = 0; index < projection.rows.length; index++) {
|
|
112
|
-
const row = projection.rows[index]!;
|
|
113
|
-
const laterSibling = projection.rows.slice(index + 1).some((candidate) => candidate.depth === row.depth);
|
|
114
|
-
const hierarchy = taskTreeConnector({ depth: row.depth, hasChildren: row.hasOpenChildren, hasLaterSibling: laterSibling });
|
|
115
|
-
const focus = row.active ? theme.fg("accent", "▶") : " ";
|
|
116
|
-
const presentation = TASK_STATUS_PRESENTATION[row.task.status as TaskStatus];
|
|
117
|
-
const glyph = presentation ? theme.fg(presentation.color, presentation.glyph) : theme.fg("muted", "?");
|
|
118
|
-
lines.push(truncateToWidth(`${focus} ${hierarchy} ${glyph} ${row.task.title}`, width, "…"));
|
|
119
|
-
}
|
|
120
|
-
return lines;
|
|
111
|
+
return renderTaskWidgetLines(theme, buildTaskWidgetProjection(this.snapshot), width);
|
|
121
112
|
}
|
|
122
113
|
|
|
123
114
|
dispose(): void {
|
package/extension/src/skills.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
3
|
+
import type { SkillWorkflowRunResult } from "../../src/skill-execution.ts";
|
|
4
|
+
import type { TaskGraph } from "../../src/task-service.ts";
|
|
3
5
|
import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
|
|
4
6
|
import { callService } from "./service-client.ts";
|
|
7
|
+
import { showTaskGraph } from "./task-graph.ts";
|
|
5
8
|
|
|
6
9
|
const SKILL_GLYPHS: Record<string, string> = { active: "●", deprecated: "○" };
|
|
7
10
|
|
|
@@ -14,6 +17,15 @@ export function skillRowMeta(skill: Artifact): string {
|
|
|
14
17
|
const target = typeof skill.extra["targetKind"] === "string" ? skill.extra["targetKind"] : "artifact";
|
|
15
18
|
return `template → ${target}`;
|
|
16
19
|
}
|
|
20
|
+
if (skill.subtype === "workflow") {
|
|
21
|
+
const definition = skill.extra["definition"] as Record<string, unknown> | undefined;
|
|
22
|
+
const inputs = definition?.["inputs"] && typeof definition["inputs"] === "object"
|
|
23
|
+
? Object.keys(definition["inputs"] as Record<string, unknown>).length
|
|
24
|
+
: 0;
|
|
25
|
+
const blueprints = definition?.["blueprints"] as Record<string, unknown> | undefined;
|
|
26
|
+
const tasks = Array.isArray(blueprints?.["tasks"]) ? blueprints["tasks"].length : 0;
|
|
27
|
+
return `workflow · ${inputs} inputs · ${tasks} tasks`;
|
|
28
|
+
}
|
|
17
29
|
const trigger = typeof skill.extra["trigger"] === "string" ? `when ${skill.extra["trigger"]}` : "manual";
|
|
18
30
|
const tools = strings(skill.extra["tools"]);
|
|
19
31
|
return [trigger, tools.join(", ")].filter(Boolean).join(" · ");
|
|
@@ -23,6 +35,12 @@ export function skillInvocationPrompt(skill: Artifact): string {
|
|
|
23
35
|
if (skill.subtype === "artifact-template") {
|
|
24
36
|
return [`Create an artifact using Papyrus template \"${skill.title}\".`, `template_id: ${skill.id}`, "Ask for or infer the title and all required template fields, then call papyrus_create."].join("\n");
|
|
25
37
|
}
|
|
38
|
+
if (skill.subtype === "workflow") {
|
|
39
|
+
return [
|
|
40
|
+
`Run Papyrus workflow Skill \"${skill.title}\" (${skill.id}).`,
|
|
41
|
+
"Collect its required arguments, then call the skills domain tool with action=run.",
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
26
44
|
const trigger = typeof skill.extra["trigger"] === "string" ? skill.extra["trigger"] : "manual invocation";
|
|
27
45
|
const steps = strings(skill.extra["steps"]);
|
|
28
46
|
const tools = strings(skill.extra["tools"]);
|
|
@@ -35,6 +53,20 @@ export function skillInvocationPrompt(skill: Artifact): string {
|
|
|
35
53
|
].join("\n");
|
|
36
54
|
}
|
|
37
55
|
|
|
56
|
+
export function skillRunTaskGraph(run: SkillWorkflowRunResult, taskArtifacts: Artifact[]): TaskGraph {
|
|
57
|
+
const executionById = new Map(run.execution.nodes.map((node) => [node.id, node]));
|
|
58
|
+
return {
|
|
59
|
+
nodes: taskArtifacts.map((task) => ({
|
|
60
|
+
task,
|
|
61
|
+
active: executionById.get(task.id)?.active === true,
|
|
62
|
+
parentIds: [],
|
|
63
|
+
childIds: [],
|
|
64
|
+
dependencyIds: executionById.get(task.id)?.prerequisiteIds ?? [],
|
|
65
|
+
})),
|
|
66
|
+
rootIds: run.rootTaskIds,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
38
70
|
export async function showSkills(ctx: ExtensionCommandContext): Promise<void> {
|
|
39
71
|
await showArtifactBrowser(ctx, {
|
|
40
72
|
kind: "skill",
|
|
@@ -43,10 +75,37 @@ export async function showSkills(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
43
75
|
statusOrder: ["active", "deprecated"],
|
|
44
76
|
glyphs: SKILL_GLYPHS,
|
|
45
77
|
rowMeta: skillRowMeta,
|
|
46
|
-
actions: (skill) => [
|
|
78
|
+
actions: (skill) => [
|
|
79
|
+
"Show details",
|
|
80
|
+
skill.subtype === "artifact-template" ? "Use template" : skill.subtype === "workflow" ? "Run workflow" : "Invoke skill",
|
|
81
|
+
skill.status === "active" ? "Disable" : "Enable",
|
|
82
|
+
],
|
|
47
83
|
handleAction: async (choice, skill, commandCtx) => {
|
|
48
84
|
if (choice === "Show details") await showArtifactDetails(commandCtx, skill.id, "skills.show");
|
|
49
|
-
else if (choice === "
|
|
85
|
+
else if (choice === "Run workflow") {
|
|
86
|
+
const source = await commandCtx.ui.input("Workflow arguments JSON:", "{}");
|
|
87
|
+
if (source === undefined) return;
|
|
88
|
+
try {
|
|
89
|
+
const arguments_ = JSON.parse(source) as unknown;
|
|
90
|
+
if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
|
|
91
|
+
throw new Error("arguments must be a JSON object");
|
|
92
|
+
}
|
|
93
|
+
const run = await callService<Record<string, unknown>, SkillWorkflowRunResult>("skills.run", {
|
|
94
|
+
id: skill.id,
|
|
95
|
+
arguments: arguments_ as Record<string, unknown>,
|
|
96
|
+
});
|
|
97
|
+
commandCtx.ui.notify([
|
|
98
|
+
`Created ${run.runId} · ${run.created.tasks.length} tasks · ${run.rootTaskIds.length} ready roots`,
|
|
99
|
+
`Context docs: ${run.created.docs.join(", ") || "none"}`,
|
|
100
|
+
`Scoped rules: ${run.created.rules.join(", ") || "none"}`,
|
|
101
|
+
].join("\n"), "info");
|
|
102
|
+
const taskArtifacts = await Promise.all(run.execution.nodes.map((node) =>
|
|
103
|
+
callService<Record<string, unknown>, Artifact>("tasks.show", { id: node.id })));
|
|
104
|
+
await showTaskGraph(commandCtx, skillRunTaskGraph(run, taskArtifacts));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
commandCtx.ui.notify(`Workflow run failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
107
|
+
}
|
|
108
|
+
} else if (choice === "Invoke skill" || choice === "Use template") {
|
|
50
109
|
const invocation = await callService<Record<string, unknown>, string>("skills.invoke", { id: skill.id });
|
|
51
110
|
commandCtx.ui.setEditorText(invocation);
|
|
52
111
|
commandCtx.ui.notify("Invocation placed in the editor", "info");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
2
|
+
import type { TaskEvent } from "../../src/domain/task-event.ts";
|
|
2
3
|
import { checklistEntries, type ProofReference } from "../../src/domain/checklist.ts";
|
|
3
4
|
import { formatMetadata } from "./artifact-format.ts";
|
|
4
5
|
|
|
@@ -48,7 +49,28 @@ function gateLines(value: unknown): string[] {
|
|
|
48
49
|
return lines;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
function historyLines(history: TaskEvent[]): string[] {
|
|
53
|
+
if (history.length === 0) return ["History:", " (no post-migration events recorded)"];
|
|
54
|
+
const lines = ["History:"];
|
|
55
|
+
for (const event of history) {
|
|
56
|
+
const transition = event.fromStatus || event.toStatus ? ` · ${event.fromStatus ?? "∅"} → ${event.toStatus ?? "∅"}` : "";
|
|
57
|
+
const reason = event.reason ? ` · ${event.reason}` : "";
|
|
58
|
+
lines.push(` ${event.occurredAt} · ${event.type}${transition} · ${event.actor}/${event.source}${reason}`);
|
|
59
|
+
if (event.evidence?.result) lines.push(` result: ${event.evidence.result}`);
|
|
60
|
+
if (Array.isArray(event.evidence?.gates)) {
|
|
61
|
+
for (const value of event.evidence.gates) {
|
|
62
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
63
|
+
const result = value as Record<string, unknown>;
|
|
64
|
+
const gate = typeof result["gate"] === "object" && result["gate"] !== null ? result["gate"] as Record<string, unknown> : {};
|
|
65
|
+
const passed = result["passed"] === true;
|
|
66
|
+
lines.push(` ${passed ? "✓" : "✗"} ${String(gate["type"] ?? "gate")} · ${String(gate["target"] ?? "unknown")}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function taskDetailsText(task: Artifact, relationshipGraphLines: string[] = [], history: TaskEvent[] = []): string {
|
|
52
74
|
let output = `${TASK_STATUS_GLYPHS[task.status] ?? "?"} ${task.title}\n${task.id} [task|${task.status}]`;
|
|
53
75
|
if (task.labels.length > 0) output += `\nLabels: ${task.labels.join(", ")}`;
|
|
54
76
|
output += `\n\n${task.body || "(no body)"}`;
|
|
@@ -60,6 +82,7 @@ export function taskDetailsText(task: Artifact, relationshipGraphLines: string[]
|
|
|
60
82
|
if (Object.keys(metadata).length > 0) {
|
|
61
83
|
output += `\n\nMetadata:\n${formatMetadata(metadata).map((line) => ` ${line}`).join("\n")}`;
|
|
62
84
|
}
|
|
85
|
+
output += `\n\n${historyLines(history).join("\n")}`;
|
|
63
86
|
if (task.edges?.length) {
|
|
64
87
|
const graph = relationshipGraphLines.length > 0 ? relationshipGraphLines.join("\n") : " (graph unavailable)";
|
|
65
88
|
output += `\n\nRelationships:\n Dependencies point prerequisite → dependent.\n${graph}`;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TASK_DETAIL_RESERVED_ROWS,
|
|
8
8
|
} from "../../src/constants.ts";
|
|
9
9
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
10
|
+
import type { TaskEvent } from "../../src/domain/task-event.ts";
|
|
10
11
|
import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
|
|
11
12
|
import { projectTaskRelationships } from "../../src/task-relationship-view.ts";
|
|
12
13
|
import type { TaskGraph } from "../../src/task-service.ts";
|
|
@@ -31,13 +32,14 @@ class TaskDetailViewport {
|
|
|
31
32
|
private readonly theme: Theme,
|
|
32
33
|
task: Artifact,
|
|
33
34
|
private readonly graphLines: string[],
|
|
35
|
+
history: TaskEvent[],
|
|
34
36
|
private readonly close: () => void,
|
|
35
37
|
) {
|
|
36
38
|
this.visibleLines = Math.max(
|
|
37
39
|
TASK_DETAIL_MIN_VISIBLE_LINES,
|
|
38
40
|
Math.min(TASK_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - TASK_DETAIL_RESERVED_ROWS),
|
|
39
41
|
);
|
|
40
|
-
this.narrative = taskDetailsText({ ...task, edges: undefined });
|
|
42
|
+
this.narrative = taskDetailsText({ ...task, edges: undefined }, [], history);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
invalidate(): void { this.renderedWidth = 0; }
|
|
@@ -99,13 +101,14 @@ export async function showTaskDetails(
|
|
|
99
101
|
task: Artifact,
|
|
100
102
|
graph?: TaskGraph,
|
|
101
103
|
renderer: GraphRenderer = new BeautifulMermaidRenderer(),
|
|
104
|
+
history: TaskEvent[] = [],
|
|
102
105
|
): Promise<void> {
|
|
103
106
|
const relationshipGraph = renderer.render(projectTaskRelationships(task, graph)).lines;
|
|
104
|
-
const content = taskDetailsText(task, relationshipGraph);
|
|
107
|
+
const content = taskDetailsText(task, relationshipGraph, history);
|
|
105
108
|
if (ctx.mode !== "tui") {
|
|
106
109
|
ctx.ui.notify(content, "info");
|
|
107
110
|
return;
|
|
108
111
|
}
|
|
109
112
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
|
|
110
|
-
new TaskDetailViewport(tui, theme, task, relationshipGraph, done));
|
|
113
|
+
new TaskDetailViewport(tui, theme, task, relationshipGraph, history, done));
|
|
111
114
|
}
|
|
@@ -86,8 +86,17 @@ export class TaskGraphViewport {
|
|
|
86
86
|
|
|
87
87
|
private rebuild(): void {
|
|
88
88
|
const view = GRAPH_VIEWS[this.viewIndex]!;
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
try {
|
|
90
|
+
this.graphLines = this.renderer.render(projectTaskGraph(this.graph, view)).lines;
|
|
91
|
+
if (this.graphLines.length === 0) this.graphLines = [`No task ${view} relationships`];
|
|
92
|
+
} catch {
|
|
93
|
+
this.graphLines = [
|
|
94
|
+
"┌─ Task graph ─",
|
|
95
|
+
`│ Graph rendering failed for ${view} view.`,
|
|
96
|
+
"│ Press Tab for another view or Esc to close.",
|
|
97
|
+
"└─",
|
|
98
|
+
];
|
|
99
|
+
}
|
|
91
100
|
this.offsetY = Math.min(this.offsetY, Math.max(0, this.graphLines.length - this.viewportHeight));
|
|
92
101
|
}
|
|
93
102
|
}
|
package/extension/src/tasks.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { taskDetailsText } from "./task-detail-format.ts";
|
|
|
14
14
|
export { showTaskDetails } from "./task-detail-view.ts";
|
|
15
15
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
16
16
|
import type { GateResult } from "../../src/domain/gate.ts";
|
|
17
|
+
import type { TaskHistoryPage } from "../../src/domain/task-event.ts";
|
|
17
18
|
import { projectTaskExecution } from "../../src/task-execution.ts";
|
|
18
19
|
import type { TaskCompletion, TaskGraph, TaskStatus } from "../../src/task-service.ts";
|
|
19
20
|
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
@@ -70,7 +71,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
70
71
|
if (create === "Create a task") {
|
|
71
72
|
const title = await ctx.ui.input("Task title:", "");
|
|
72
73
|
if (title) {
|
|
73
|
-
await callService("tasks.create", { title });
|
|
74
|
+
await callService("tasks.create", { title, actor: "user", source: "tasks-tui" });
|
|
74
75
|
graph = await loadTaskGraph();
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -97,7 +98,8 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
97
98
|
if (choice === "Show details") {
|
|
98
99
|
const art = await callService<Record<string, unknown>, Artifact | null>("tasks.show", { id: action.row.id });
|
|
99
100
|
if (!art) { ctx.ui.notify("Not found", "error"); continue; }
|
|
100
|
-
await
|
|
101
|
+
const history = await callService<Record<string, unknown>, TaskHistoryPage>("tasks.history", { id: art.id, direction: "desc" });
|
|
102
|
+
await showTaskDetails(ctx, art, graph, undefined, [...history.events].reverse());
|
|
101
103
|
} else if (choice === "Make active") {
|
|
102
104
|
try {
|
|
103
105
|
await callService<Record<string, unknown>, Artifact>("tasks.focus", { id: action.row.id });
|
|
@@ -107,7 +109,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
107
109
|
}
|
|
108
110
|
} else if (choice === "Run gates") {
|
|
109
111
|
try {
|
|
110
|
-
const results = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id: action.row.id });
|
|
112
|
+
const results = await callService<Record<string, unknown>, GateResult[]>("tasks.run_gates", { id: action.row.id, actor: "user", source: "tasks-tui" });
|
|
111
113
|
ctx.ui.notify(`Gates:\n${results.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target} — ${gate.output}`).join("\n")}`, "info");
|
|
112
114
|
} catch (error) {
|
|
113
115
|
ctx.ui.notify(`Gates failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
@@ -126,7 +128,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
126
128
|
? "tasks.cancel"
|
|
127
129
|
: "tasks.complete";
|
|
128
130
|
if (operation === "tasks.complete") {
|
|
129
|
-
const result = await callService<Record<string, unknown>, TaskCompletion>(operation, { id: action.row.id });
|
|
131
|
+
const result = await callService<Record<string, unknown>, TaskCompletion>(operation, { id: action.row.id, actor: "user", source: "tasks-tui" });
|
|
130
132
|
action.row.status = result.artifact.status;
|
|
131
133
|
const gates = result.gates.map((gate) => `${gate.passed ? "✓" : "✗"} ${gate.gate.type}: ${gate.gate.target}`).join("\n");
|
|
132
134
|
const checklist = result.checklist.map((item) => `${item.accepted ? "✓" : "✗"} proof: ${item.item}`).join("\n");
|
|
@@ -141,7 +143,7 @@ export async function showTasks(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
141
143
|
result.completed ? "info" : "warning",
|
|
142
144
|
);
|
|
143
145
|
} else {
|
|
144
|
-
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: action.row.id });
|
|
146
|
+
const updated = await callService<Record<string, unknown>, Artifact>(operation, { id: action.row.id, actor: "user", source: "tasks-tui" });
|
|
145
147
|
action.row.status = updated.status;
|
|
146
148
|
ctx.ui.notify(`${updated.id} → [${updated.status}]`, "info");
|
|
147
149
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Db } from "../db.ts";
|
|
2
|
-
import
|
|
2
|
+
import { inTransaction } from "../db.ts";
|
|
3
|
+
import type { AtomicArtifactStore } from "../ports/atomic-artifact-store.ts";
|
|
3
4
|
import type {
|
|
4
5
|
Artifact,
|
|
5
6
|
ArtifactEdge,
|
|
@@ -11,9 +12,13 @@ import type {
|
|
|
11
12
|
} from "../domain/artifact.ts";
|
|
12
13
|
import { createArtifact, getArtifact, linkArtifacts, queryArtifacts, updateExtra, updateStatus } from "../ops.ts";
|
|
13
14
|
|
|
14
|
-
export class SQLiteArtifactStore implements
|
|
15
|
+
export class SQLiteArtifactStore implements AtomicArtifactStore {
|
|
15
16
|
constructor(private readonly db: Db) {}
|
|
16
17
|
|
|
18
|
+
atomic<T>(operation: () => T): T {
|
|
19
|
+
return inTransaction(this.db, operation);
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
create(input: CreateArtifactInput): Artifact {
|
|
18
23
|
return createArtifact(this.db, input);
|
|
19
24
|
}
|