@danypops/papyrus 0.6.0 → 0.8.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 +26 -32
- package/extension/src/active-task-continuation.ts +9 -0
- package/extension/src/domain-tools.ts +35 -12
- package/extension/src/index.ts +39 -14
- package/extension/src/skills.ts +1 -0
- package/extension/src/task-widget.ts +4 -2
- package/extension/src/tasks.ts +62 -29
- package/package.json +1 -1
- package/src/adapters/sqlite-artifact-store.ts +6 -1
- package/src/adapters/sqlite-task-focus-store.ts +27 -14
- package/src/adapters/sqlite-task-scope-store.ts +59 -0
- package/src/cli.ts +100 -47
- package/src/constants.ts +9 -14
- package/src/daemon.ts +2 -18
- package/src/db.ts +49 -2
- package/src/domain/artifact.ts +6 -0
- package/src/domain/task-event.ts +6 -2
- package/src/domain/task-scope.ts +39 -0
- package/src/ops.ts +17 -1
- package/src/ports/artifact-store.ts +2 -0
- package/src/ports/task-focus-store.ts +30 -8
- package/src/ports/task-scope-store.ts +40 -0
- package/src/service.ts +79 -27
- package/src/skill-execution.ts +16 -10
- package/src/task-context.ts +4 -2
- package/src/task-service.ts +197 -30
- package/src/task-automation.ts +0 -188
package/src/ops.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import { exec } from "node:child_process";
|
|
7
7
|
import type { Db } from "./db.ts";
|
|
8
8
|
import { inTransaction } from "./db.ts";
|
|
9
|
-
import type { Artifact, CreateArtifactInput } from "./domain/artifact.ts";
|
|
9
|
+
import type { Artifact, CreateArtifactInput, UpdateArtifactInput } from "./domain/artifact.ts";
|
|
10
10
|
import type { Gate, GateResult, GateRunOptions } from "./domain/gate.ts";
|
|
11
11
|
export type { Artifact } from "./domain/artifact.ts";
|
|
12
12
|
export type { Gate, GateResult } from "./domain/gate.ts";
|
|
@@ -204,6 +204,22 @@ export function linkArtifacts(db: Db, fromId: string, relation: string, toId: st
|
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
export function updateArtifactContent(db: Db, id: string, input: UpdateArtifactInput): Artifact | null {
|
|
208
|
+
const artifact = getArtifact(db, id);
|
|
209
|
+
if (!artifact) return null;
|
|
210
|
+
const now = new Date().toISOString();
|
|
211
|
+
inTransaction(db, () => {
|
|
212
|
+
db.prepare("UPDATE artifacts SET title = ?, body = ?, labels = ?, updated_at = ? WHERE id = ?").run(
|
|
213
|
+
input.title ?? artifact.title,
|
|
214
|
+
input.body ?? artifact.body,
|
|
215
|
+
JSON.stringify(input.labels ?? artifact.labels),
|
|
216
|
+
now,
|
|
217
|
+
id,
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
return getArtifact(db, id);
|
|
221
|
+
}
|
|
222
|
+
|
|
207
223
|
export function updateStatus(db: Db, id: string, status: string): Artifact | null {
|
|
208
224
|
const art = getArtifact(db, id);
|
|
209
225
|
if (!art) return null;
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ArtifactQuery,
|
|
7
7
|
CreateArtifactInput,
|
|
8
8
|
RelationshipQuery,
|
|
9
|
+
UpdateArtifactInput,
|
|
9
10
|
} from "../domain/artifact.ts";
|
|
10
11
|
|
|
11
12
|
export interface ArtifactStore {
|
|
@@ -15,5 +16,6 @@ export interface ArtifactStore {
|
|
|
15
16
|
link(link: ArtifactLink): void;
|
|
16
17
|
setStatus(id: string, status: string): Artifact | null;
|
|
17
18
|
setExtra(id: string, extra: Record<string, unknown>): Artifact | null;
|
|
19
|
+
updateContent(id: string, input: UpdateArtifactInput): Artifact | null;
|
|
18
20
|
relationships(filter?: RelationshipQuery): ArtifactEdge[];
|
|
19
21
|
}
|
|
@@ -1,21 +1,43 @@
|
|
|
1
|
+
export type TaskFocusStatus = "active" | "paused";
|
|
2
|
+
|
|
3
|
+
export interface TaskFocusState {
|
|
4
|
+
taskId: string;
|
|
5
|
+
status: TaskFocusStatus;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
pauseReason?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
export interface TaskFocusStore {
|
|
2
|
-
get():
|
|
3
|
-
set(taskId: string):
|
|
11
|
+
get(): TaskFocusState | undefined;
|
|
12
|
+
set(taskId: string): TaskFocusState;
|
|
13
|
+
pause(taskId: string, reason?: string): TaskFocusState;
|
|
14
|
+
unpause(taskId: string): TaskFocusState;
|
|
4
15
|
clear(taskId?: string): void;
|
|
5
16
|
}
|
|
6
17
|
|
|
7
18
|
export class InMemoryTaskFocusStore implements TaskFocusStore {
|
|
8
|
-
private
|
|
19
|
+
private state: TaskFocusState | undefined;
|
|
20
|
+
|
|
21
|
+
get(): TaskFocusState | undefined { return this.state; }
|
|
22
|
+
|
|
23
|
+
set(taskId: string): TaskFocusState {
|
|
24
|
+
this.state = { taskId, status: "active", updatedAt: new Date().toISOString() };
|
|
25
|
+
return this.state;
|
|
26
|
+
}
|
|
9
27
|
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
pause(taskId: string, reason?: string): TaskFocusState {
|
|
29
|
+
if (this.state?.taskId !== taskId) throw new Error(`task "${taskId}" is not focused`);
|
|
30
|
+
this.state = { ...this.state, status: "paused", updatedAt: new Date().toISOString(), ...(reason ? { pauseReason: reason } : {}) };
|
|
31
|
+
return this.state;
|
|
12
32
|
}
|
|
13
33
|
|
|
14
|
-
|
|
15
|
-
this.taskId
|
|
34
|
+
unpause(taskId: string): TaskFocusState {
|
|
35
|
+
if (this.state?.taskId !== taskId) throw new Error(`task "${taskId}" is not focused`);
|
|
36
|
+
this.state = { taskId, status: "active", updatedAt: new Date().toISOString() };
|
|
37
|
+
return this.state;
|
|
16
38
|
}
|
|
17
39
|
|
|
18
40
|
clear(taskId?: string): void {
|
|
19
|
-
if (taskId === undefined || taskId ===
|
|
41
|
+
if (taskId === undefined || this.state?.taskId === taskId) this.state = undefined;
|
|
20
42
|
}
|
|
21
43
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TaskProjectScope, TaskScopeSource, TaskViewMode, TaskViewPreference } from "../domain/task-scope.ts";
|
|
2
|
+
|
|
3
|
+
export interface TaskScopeStore {
|
|
4
|
+
assign(taskId: string, projectRoot: string | undefined, source: TaskScopeSource): TaskProjectScope;
|
|
5
|
+
get(taskId: string): TaskProjectScope | undefined;
|
|
6
|
+
taskIds(projectRoot: string | undefined, limit: number): string[];
|
|
7
|
+
view(projectRoot: string): TaskViewPreference;
|
|
8
|
+
setView(projectRoot: string, mode: TaskViewMode, rootTaskId?: string): TaskViewPreference;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class InMemoryTaskScopeStore implements TaskScopeStore {
|
|
12
|
+
private readonly scopes = new Map<string, TaskProjectScope>();
|
|
13
|
+
private readonly views = new Map<string, TaskViewPreference>();
|
|
14
|
+
|
|
15
|
+
assign(taskId: string, projectRoot: string | undefined, source: TaskScopeSource): TaskProjectScope {
|
|
16
|
+
const scope = { taskId, ...(projectRoot === undefined ? {} : { projectRoot }), source };
|
|
17
|
+
this.scopes.set(taskId, scope);
|
|
18
|
+
return scope;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get(taskId: string): TaskProjectScope | undefined { return this.scopes.get(taskId); }
|
|
22
|
+
|
|
23
|
+
taskIds(projectRoot: string | undefined, limit: number): string[] {
|
|
24
|
+
return [...this.scopes.values()]
|
|
25
|
+
.filter((scope) => scope.projectRoot === projectRoot)
|
|
26
|
+
.map((scope) => scope.taskId)
|
|
27
|
+
.sort()
|
|
28
|
+
.slice(0, limit);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
view(projectRoot: string): TaskViewPreference {
|
|
32
|
+
return this.views.get(projectRoot) ?? { projectRoot, mode: "project" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setView(projectRoot: string, mode: TaskViewMode, rootTaskId?: string): TaskViewPreference {
|
|
36
|
+
const view = { projectRoot, mode, ...(rootTaskId === undefined ? {} : { rootTaskId }) };
|
|
37
|
+
this.views.set(projectRoot, view);
|
|
38
|
+
return view;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -5,15 +5,17 @@ import { SQLiteArtifactStore } from "./adapters/sqlite-artifact-store.ts";
|
|
|
5
5
|
import { SQLiteGateRunner } from "./adapters/sqlite-gate-runner.ts";
|
|
6
6
|
import { SQLiteTaskFocusStore } from "./adapters/sqlite-task-focus-store.ts";
|
|
7
7
|
import { SQLiteTaskEventStore } from "./adapters/sqlite-task-event-store.ts";
|
|
8
|
+
import { SQLiteTaskScopeStore } from "./adapters/sqlite-task-scope-store.ts";
|
|
8
9
|
import type { CreateArtifactInput } from "./domain/artifact.ts";
|
|
9
10
|
import type { Checklist } from "./domain/checklist.ts";
|
|
10
11
|
import type { TaskEventContext, TaskEventDirection } from "./domain/task-event.ts";
|
|
12
|
+
import type { TaskViewMode } from "./domain/task-scope.ts";
|
|
11
13
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
12
14
|
import type { GateRunner } from "./ports/gate-runner.ts";
|
|
13
15
|
import type { TaskEventStore } from "./ports/task-event-store.ts";
|
|
16
|
+
import type { TaskScopeStore } from "./ports/task-scope-store.ts";
|
|
14
17
|
import { projectTaskExecution } from "./task-execution.ts";
|
|
15
18
|
import { Tasks, type TaskStatus } from "./task-service.ts";
|
|
16
|
-
import { TaskAutomationReconciler, taskAutomationSettings, type TaskAutomationSettings } from "./task-automation.ts";
|
|
17
19
|
import {
|
|
18
20
|
createArtifactTemplate,
|
|
19
21
|
createDocument,
|
|
@@ -41,8 +43,6 @@ import { instantiateSkillWorkflow } from "./skill-execution.ts";
|
|
|
41
43
|
|
|
42
44
|
export const EXPECTED_OPERATION_NAMES = [
|
|
43
45
|
"system.migrate",
|
|
44
|
-
"automation.status",
|
|
45
|
-
"automation.reconcile",
|
|
46
46
|
"artifact.create",
|
|
47
47
|
"artifact.query",
|
|
48
48
|
"artifact.show",
|
|
@@ -52,19 +52,26 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
52
52
|
"gates.run",
|
|
53
53
|
"rules.injectable",
|
|
54
54
|
"tasks.create",
|
|
55
|
+
"tasks.update",
|
|
55
56
|
"tasks.list",
|
|
56
57
|
"tasks.graph",
|
|
57
58
|
"tasks.plan",
|
|
58
59
|
"tasks.show",
|
|
59
60
|
"tasks.history",
|
|
61
|
+
"tasks.scope",
|
|
62
|
+
"tasks.set_scope",
|
|
63
|
+
"tasks.assign_project",
|
|
60
64
|
"tasks.active",
|
|
65
|
+
"tasks.focused",
|
|
61
66
|
"tasks.focus",
|
|
67
|
+
"tasks.pause",
|
|
68
|
+
"tasks.unpause",
|
|
69
|
+
"tasks.clear_focus",
|
|
62
70
|
"tasks.start",
|
|
63
71
|
"tasks.submit",
|
|
64
72
|
"tasks.complete",
|
|
65
73
|
"tasks.run_gates",
|
|
66
74
|
"tasks.set_checklist",
|
|
67
|
-
"tasks.set_automation",
|
|
68
75
|
"tasks.context",
|
|
69
76
|
"tasks.reject",
|
|
70
77
|
"tasks.retry",
|
|
@@ -117,6 +124,13 @@ function optionalString(input: OperationInput, key: string): string | undefined
|
|
|
117
124
|
return value;
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
function optionalStringArray(input: OperationInput, key: string): string[] | undefined {
|
|
128
|
+
const value = input[key];
|
|
129
|
+
if (value === undefined) return undefined;
|
|
130
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
|
|
131
|
+
return value as string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
function optionalNumber(input: OperationInput, key: string): number | undefined {
|
|
121
135
|
const value = input[key];
|
|
122
136
|
if (value === undefined) return undefined;
|
|
@@ -148,8 +162,8 @@ function handlers(
|
|
|
148
162
|
artifacts: ArtifactStore,
|
|
149
163
|
gates: GateRunner,
|
|
150
164
|
tasks: Tasks,
|
|
151
|
-
automation: TaskAutomationReconciler,
|
|
152
165
|
events: TaskEventStore,
|
|
166
|
+
scopes: TaskScopeStore,
|
|
153
167
|
migrate: () => unknown,
|
|
154
168
|
): Record<OperationName, OperationHandler> {
|
|
155
169
|
const eventContext = (input: OperationInput): TaskEventContext => ({
|
|
@@ -162,15 +176,19 @@ function handlers(
|
|
|
162
176
|
const context = eventContext(input);
|
|
163
177
|
return { ...context, source: context.source ?? source };
|
|
164
178
|
};
|
|
165
|
-
const
|
|
179
|
+
const artifactFilter = (input: OperationInput) => ({
|
|
166
180
|
status: optionalString(input, "status"),
|
|
167
181
|
text: optionalString(input, "text"),
|
|
168
182
|
limit: optionalNumber(input, "limit"),
|
|
169
183
|
});
|
|
184
|
+
const taskFilter = (input: OperationInput) => ({
|
|
185
|
+
...artifactFilter(input),
|
|
186
|
+
projectRoot: string(input, "project_root"),
|
|
187
|
+
scope: optionalString(input, "scope") as TaskViewMode | undefined,
|
|
188
|
+
rootTaskId: optionalString(input, "root_task_id"),
|
|
189
|
+
});
|
|
170
190
|
return {
|
|
171
191
|
"system.migrate": () => migrate(),
|
|
172
|
-
"automation.status": () => automation.status(),
|
|
173
|
-
"automation.reconcile": () => automation.reconcile(),
|
|
174
192
|
"artifact.create": (input) => {
|
|
175
193
|
const normalized = normalizeCreateInput(input);
|
|
176
194
|
if (normalized.kind !== "task") return artifacts.create(normalized);
|
|
@@ -183,6 +201,8 @@ function handlers(
|
|
|
183
201
|
labels: normalized.labels,
|
|
184
202
|
extra: normalized.extra,
|
|
185
203
|
templateId: normalized.templateId,
|
|
204
|
+
projectRoot: string(input, "project_root"),
|
|
205
|
+
projectSource: "cwd",
|
|
186
206
|
}, eventContextFor(input, "artifact-api"));
|
|
187
207
|
},
|
|
188
208
|
"artifact.query": (input) => artifacts.query(input),
|
|
@@ -218,7 +238,7 @@ function handlers(
|
|
|
218
238
|
? tasks.runGates(id, eventContextFor(input, "gates-api"))
|
|
219
239
|
: gates.runAsync(id);
|
|
220
240
|
},
|
|
221
|
-
"rules.injectable": () => listInjectableRules(artifacts, tasks.active()?.id)
|
|
241
|
+
"rules.injectable": (input) => listInjectableRules(artifacts, tasks.active(taskFilter(input))?.id)
|
|
222
242
|
.map(({ id, title, body, extra }) => ({ id, title, body, extra })),
|
|
223
243
|
"tasks.create": (input) => tasks.create({
|
|
224
244
|
title: string(input, "title"),
|
|
@@ -231,6 +251,13 @@ function handlers(
|
|
|
231
251
|
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
232
252
|
parentId: optionalString(input, "parent_id") ?? optionalString(input, "parentId"),
|
|
233
253
|
dependsOn: (input["depends_on"] ?? input["dependsOn"]) as string[] | undefined,
|
|
254
|
+
projectRoot: string(input, "project_root"),
|
|
255
|
+
projectSource: "cwd",
|
|
256
|
+
}, eventContext(input)),
|
|
257
|
+
"tasks.update": (input) => tasks.update(string(input, "id"), {
|
|
258
|
+
...(input["title"] !== undefined ? { title: optionalString(input, "title")! } : {}),
|
|
259
|
+
...(input["body"] !== undefined ? { body: optionalString(input, "body")! } : {}),
|
|
260
|
+
...(input["labels"] !== undefined ? { labels: optionalStringArray(input, "labels")! } : {}),
|
|
234
261
|
}, eventContext(input)),
|
|
235
262
|
"tasks.list": (input) => tasks.list(taskFilter(input)),
|
|
236
263
|
"tasks.graph": (input) => tasks.graph(taskFilter(input)),
|
|
@@ -241,18 +268,29 @@ function handlers(
|
|
|
241
268
|
cursor: optionalNumber(input, "cursor"),
|
|
242
269
|
direction: optionalString(input, "direction") as TaskEventDirection | undefined,
|
|
243
270
|
}),
|
|
244
|
-
"tasks.
|
|
245
|
-
"tasks.
|
|
271
|
+
"tasks.scope": (input) => tasks.scopeSelection(string(input, "project_root")),
|
|
272
|
+
"tasks.set_scope": (input) => tasks.setView(
|
|
273
|
+
string(input, "project_root"),
|
|
274
|
+
string(input, "scope") as TaskViewMode,
|
|
275
|
+
optionalString(input, "root_task_id"),
|
|
276
|
+
),
|
|
277
|
+
"tasks.assign_project": (input) => tasks.assignProject(
|
|
278
|
+
string(input, "id"),
|
|
279
|
+
string(input, "project_root"),
|
|
280
|
+
eventContext(input),
|
|
281
|
+
),
|
|
282
|
+
"tasks.active": (input) => tasks.active(taskFilter(input)),
|
|
283
|
+
"tasks.focused": (input) => tasks.focused(taskFilter(input)),
|
|
284
|
+
"tasks.focus": (input) => tasks.focus(string(input, "id"), eventContext(input)),
|
|
285
|
+
"tasks.pause": (input) => tasks.pauseFocus(eventContext(input)),
|
|
286
|
+
"tasks.unpause": (input) => tasks.unpauseFocus(eventContext(input)),
|
|
287
|
+
"tasks.clear_focus": (input) => tasks.clearFocus(eventContext(input)),
|
|
246
288
|
"tasks.start": (input) => tasks.transition(string(input, "id"), "start", eventContext(input)),
|
|
247
289
|
"tasks.submit": (input) => tasks.transition(string(input, "id"), "submit", eventContext(input)),
|
|
248
290
|
"tasks.complete": (input) => tasks.completeAsync(string(input, "id"), eventContext(input)),
|
|
249
291
|
"tasks.run_gates": (input) => tasks.runGates(string(input, "id"), eventContext(input)),
|
|
250
292
|
"tasks.set_checklist": (input) => tasks.setChecklist(string(input, "id"), input["checklist"] as Checklist),
|
|
251
|
-
"tasks.
|
|
252
|
-
if (typeof input["enabled"] !== "boolean") throw new Error("enabled must be a boolean");
|
|
253
|
-
return tasks.setAutomation(string(input, "id"), input["enabled"], eventContext(input));
|
|
254
|
-
},
|
|
255
|
-
"tasks.context": () => taskContext(artifacts, tasks.active()?.id),
|
|
293
|
+
"tasks.context": (input) => taskContext(artifacts, tasks.active()?.id, new Set(tasks.list(taskFilter(input)).map((task) => task.id))),
|
|
256
294
|
"tasks.reject": (input) => tasks.transition(string(input, "id"), "reject", eventContext(input)),
|
|
257
295
|
"tasks.retry": (input) => tasks.transition(string(input, "id"), "retry", eventContext(input)),
|
|
258
296
|
"tasks.cancel": (input) => tasks.transition(string(input, "id"), "cancel", eventContext(input)),
|
|
@@ -263,7 +301,7 @@ function handlers(
|
|
|
263
301
|
labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
|
|
264
302
|
templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
|
|
265
303
|
}),
|
|
266
|
-
"docs.list": (input) => listDocuments(artifacts,
|
|
304
|
+
"docs.list": (input) => listDocuments(artifacts, artifactFilter(input)),
|
|
267
305
|
"docs.show": (input) => showDocument(artifacts, string(input, "id")),
|
|
268
306
|
"docs.activate": (input) => transitionDocument(artifacts, string(input, "id"), "activate"),
|
|
269
307
|
"docs.archive": (input) => transitionDocument(artifacts, string(input, "id"), "archive"),
|
|
@@ -275,7 +313,7 @@ function handlers(
|
|
|
275
313
|
severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
|
|
276
314
|
labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
|
|
277
315
|
}),
|
|
278
|
-
"rules.list": (input) => listRules(artifacts,
|
|
316
|
+
"rules.list": (input) => listRules(artifacts, artifactFilter(input)),
|
|
279
317
|
"rules.show": (input) => showRule(artifacts, string(input, "id")),
|
|
280
318
|
"rules.preview": (input) => previewRule(artifacts, string(input, "id")),
|
|
281
319
|
"rules.enable": (input) => transitionRule(artifacts, string(input, "id"), "enable"),
|
|
@@ -291,28 +329,42 @@ function handlers(
|
|
|
291
329
|
title: string(input, "title"), targetKind: string(input, "target_kind"), defaults: input["defaults"] as Record<string, unknown> | undefined,
|
|
292
330
|
required: input["required"] as string[] | undefined, body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
|
|
293
331
|
}),
|
|
294
|
-
"skills.list": (input) => listSkills(artifacts,
|
|
332
|
+
"skills.list": (input) => listSkills(artifacts, artifactFilter(input)),
|
|
295
333
|
"skills.show": (input) => showSkill(artifacts, string(input, "id")),
|
|
296
334
|
"skills.invoke": (input) => skillInvocation(artifacts, string(input, "id")),
|
|
297
335
|
"skills.run": (input) => instantiateSkillWorkflow(artifacts, string(input, "id"), {
|
|
298
336
|
runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
|
|
299
337
|
arguments: input["arguments"] as Record<string, unknown> | undefined,
|
|
300
|
-
}, { events, context: eventContextFor(input, "skill-run") }),
|
|
338
|
+
}, { events, scopes, projectRoot: string(input, "project_root"), context: eventContextFor(input, "skill-run") }),
|
|
301
339
|
"skills.enable": (input) => transitionSkill(artifacts, string(input, "id"), "enable"),
|
|
302
340
|
"skills.disable": (input) => transitionSkill(artifacts, string(input, "id"), "disable"),
|
|
303
|
-
"skills.instantiate": (input) =>
|
|
341
|
+
"skills.instantiate": (input) => {
|
|
342
|
+
const templateId = string(input, "template_id");
|
|
343
|
+
const template = artifacts.get(templateId);
|
|
344
|
+
if (template?.extra["targetKind"] !== "task") return instantiateTemplate(artifacts, templateId, normalizeCreateInput(input));
|
|
345
|
+
return tasks.create({
|
|
346
|
+
title: optionalString(input, "title") as string,
|
|
347
|
+
body: optionalString(input, "body"),
|
|
348
|
+
status: optionalString(input, "status") as TaskStatus | undefined,
|
|
349
|
+
labels: input["labels"] as string[] | undefined,
|
|
350
|
+
extra: input["extra"] as Record<string, unknown> | undefined,
|
|
351
|
+
templateId,
|
|
352
|
+
projectRoot: string(input, "project_root"),
|
|
353
|
+
projectSource: "cwd",
|
|
354
|
+
}, eventContextFor(input, "template-instantiation"));
|
|
355
|
+
},
|
|
304
356
|
};
|
|
305
357
|
}
|
|
306
358
|
|
|
307
|
-
export function createPapyrusService(path: string
|
|
359
|
+
export function createPapyrusService(path: string): PapyrusService {
|
|
308
360
|
const db = openDb(path);
|
|
309
361
|
const artifacts = new SQLiteArtifactStore(db);
|
|
310
362
|
const gates = new SQLiteGateRunner(db);
|
|
311
363
|
const focus = new SQLiteTaskFocusStore(db);
|
|
312
364
|
const events = new SQLiteTaskEventStore(db);
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const registry = handlers(artifacts, gates, tasks,
|
|
365
|
+
const scopes = new SQLiteTaskScopeStore(db);
|
|
366
|
+
const tasks = new Tasks(artifacts, gates, focus, events, scopes);
|
|
367
|
+
const registry = handlers(artifacts, gates, tasks, events, scopes, () => migrateDb(db));
|
|
316
368
|
const state = (): SchemaState => {
|
|
317
369
|
const current = schemaVersion(db);
|
|
318
370
|
return { current, required: SQLITE_SCHEMA_VERSION, migrationRequired: current !== SQLITE_SCHEMA_VERSION };
|
|
@@ -323,8 +375,8 @@ export function createPapyrusService(path: string, options: { automation?: TaskA
|
|
|
323
375
|
async execute(operation, input = {}) {
|
|
324
376
|
const handler = registry[operation as OperationName];
|
|
325
377
|
if (!handler) throw new UnknownOperationError(`unknown operation "${operation}"`);
|
|
326
|
-
if (operation !== "system.migrate" &&
|
|
327
|
-
throw new MigrationRequiredError("database migration required; run `papyrus migrate task-
|
|
378
|
+
if (operation !== "system.migrate" && state().migrationRequired) {
|
|
379
|
+
throw new MigrationRequiredError("database migration required; run `papyrus migrate task-focus`");
|
|
328
380
|
}
|
|
329
381
|
return handler(input);
|
|
330
382
|
},
|
package/src/skill-execution.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
12
12
|
import type { TaskEventContext } from "./domain/task-event.ts";
|
|
13
13
|
import type { TaskEventStore } from "./ports/task-event-store.ts";
|
|
14
|
+
import type { TaskScopeStore } from "./ports/task-scope-store.ts";
|
|
15
|
+
import { normalizeProjectRoot } from "./domain/task-scope.ts";
|
|
14
16
|
import { requireAtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
|
|
15
17
|
import { projectTaskExecution, type TaskExecutionPlan } from "./task-execution.ts";
|
|
16
18
|
import type { TaskGraph, TaskNode, TaskStatus } from "./task-service.ts";
|
|
@@ -117,9 +119,10 @@ export function instantiateSkillWorkflow(
|
|
|
117
119
|
artifacts: ArtifactStore,
|
|
118
120
|
skillId: string,
|
|
119
121
|
input: InstantiateSkillWorkflowInput = {},
|
|
120
|
-
history?: { events: TaskEventStore; context?: TaskEventContext },
|
|
122
|
+
history?: { events: TaskEventStore; scopes: TaskScopeStore; projectRoot: string; context?: TaskEventContext },
|
|
121
123
|
): SkillWorkflowRunResult {
|
|
122
124
|
const { definition } = requireWorkflowSkill(artifacts, skillId);
|
|
125
|
+
const projectRoot = history ? normalizeProjectRoot(history.projectRoot) : undefined;
|
|
123
126
|
const arguments_ = resolveSkillArguments(definition, input.arguments);
|
|
124
127
|
const rendered = renderDefinition(definition, arguments_);
|
|
125
128
|
const runId = normalizeRunId(skillId, input.runId);
|
|
@@ -175,15 +178,18 @@ export function instantiateSkillWorkflow(
|
|
|
175
178
|
labels: withRunLabel(blueprint.labels, runId),
|
|
176
179
|
extra: { ...(blueprint.extra ?? {}), skillRun: { id: runId, skillId, ref: blueprint.ref } },
|
|
177
180
|
});
|
|
178
|
-
if (history)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
if (history) {
|
|
182
|
+
history.scopes.assign(task.id, projectRoot, "cwd");
|
|
183
|
+
history.events.append({
|
|
184
|
+
taskId: task.id,
|
|
185
|
+
type: "created",
|
|
186
|
+
actor: history.context?.actor ?? "system",
|
|
187
|
+
source: history.context?.source ?? "skill-run",
|
|
188
|
+
toStatus: task.status as TaskStatus,
|
|
189
|
+
...(history.context?.sessionId === undefined ? {} : { sessionId: history.context.sessionId }),
|
|
190
|
+
...(history.context?.reason === undefined ? {} : { reason: history.context.reason }),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
187
193
|
return task;
|
|
188
194
|
});
|
|
189
195
|
|
package/src/task-context.ts
CHANGED
|
@@ -34,8 +34,10 @@ function renderCurrent(task: Artifact): string[] {
|
|
|
34
34
|
];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function taskContext(artifacts: ArtifactStore, activeTaskId?: string): string | null {
|
|
38
|
-
const tasks = artifacts.query({ kind: "task" })
|
|
37
|
+
export function taskContext(artifacts: ArtifactStore, activeTaskId?: string, taskIds?: Set<string>): string | null {
|
|
38
|
+
const tasks = artifacts.query({ kind: "task" })
|
|
39
|
+
.filter((task) => taskIds === undefined || taskIds.has(task.id))
|
|
40
|
+
.sort((left, right) => left.updated_at.localeCompare(right.updated_at));
|
|
39
41
|
const open = tasks.filter((task) => task.status !== "done" && task.status !== "canceled");
|
|
40
42
|
if (open.length === 0) return null;
|
|
41
43
|
|