@danypops/papyrus 0.41.0 → 0.42.1
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 +8 -11
- package/package.json +2 -2
- package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
- package/src/adapters/sqlite-artifact-store.ts +7 -5
- package/src/adapters/sqlite-discussion-round-store.ts +26 -17
- package/src/adapters/sqlite-gate-runner.ts +1 -1
- package/src/adapters/sqlite-graph-projection-store.ts +14 -10
- package/src/adapters/sqlite-log-store.ts +36 -17
- package/src/adapters/sqlite-note-event-store.ts +20 -16
- package/src/adapters/sqlite-session-identity-store.ts +13 -7
- package/src/adapters/sqlite-task-event-store.ts +29 -21
- package/src/adapters/sqlite-task-focus-store.ts +36 -10
- package/src/adapters/sqlite-task-lease-store.ts +12 -6
- package/src/adapters/sqlite-task-scope-store.ts +25 -14
- package/src/artifact-relationship-view.ts +4 -4
- package/src/artifact-subtree.ts +4 -2
- package/src/authority-registry.ts +2 -1
- package/src/cli.ts +794 -354
- package/src/client.ts +6 -3
- package/src/constants.ts +34 -56
- package/src/daemon-state.ts +4 -12
- package/src/daemon.ts +31 -9
- package/src/db.ts +153 -105
- package/src/discussion-service.ts +109 -44
- package/src/domain/artifact-event.ts +18 -5
- package/src/domain/artifact.ts +3 -1
- package/src/domain/blueprint-definition.ts +268 -0
- package/src/domain/checklist.ts +20 -17
- package/src/domain/discussion.ts +37 -18
- package/src/domain/gate.ts +7 -7
- package/src/domain/log-entry.ts +1 -1
- package/src/domain/note-event.ts +20 -7
- package/src/domain/task-event.ts +17 -7
- package/src/domain-services.ts +362 -288
- package/src/graph-projection-service.ts +34 -8
- package/src/id-migration.ts +17 -4
- package/src/index.ts +16 -11
- package/src/log-service.ts +6 -5
- package/src/log.ts +19 -0
- package/src/modules/discuss.ts +63 -28
- package/src/modules/docs.ts +74 -17
- package/src/modules/graph-projection.ts +20 -9
- package/src/modules/logs.ts +34 -22
- package/src/modules/notes.ts +66 -28
- package/src/modules/playbooks.ts +93 -32
- package/src/modules/rules.ts +57 -15
- package/src/modules/session-identity.ts +6 -2
- package/src/modules/tasks.ts +142 -67
- package/src/note-service.ts +11 -7
- package/src/ops.ts +134 -69
- package/src/playbook-definition.ts +124 -39
- package/src/playbook-execution.ts +18 -25
- package/src/ports/artifact-scope-store.ts +1 -1
- package/src/ports/note-event-store.ts +6 -4
- package/src/ports/task-event-store.ts +9 -6
- package/src/ports/task-focus-store.ts +17 -4
- package/src/ports/task-lease-store.ts +9 -4
- package/src/ports/task-scope-store.ts +3 -1
- package/src/service.ts +148 -110
- package/src/session-identity-service.ts +10 -2
- package/src/task-context.ts +28 -16
- package/src/task-execution.ts +4 -12
- package/src/task-graph-view.ts +12 -12
- package/src/task-relationship-view.ts +1 -3
- package/src/task-service.ts +168 -73
- package/src/vehicle/artifact-trash-vehicle.ts +26 -14
- package/src/vehicle/artifact-vehicle-shared.ts +32 -13
- package/src/vehicle/docs-vehicle.ts +50 -18
- package/src/vehicle/notes-vehicle.ts +26 -8
- package/src/vehicle/papyrus-vehicle.ts +16 -8
- package/src/vehicle/playbooks-vehicle.ts +88 -19
- package/src/vehicle/rules-vehicle.ts +58 -21
- package/src/vehicle/tasks-vehicle.ts +366 -54
- package/src/version.ts +1 -1
- package/src/workflow-execution.ts +198 -109
- package/src/domain/skill-definition.ts +0 -270
- package/src/modules/skills.ts +0 -158
- package/src/vehicle/skills-vehicle.ts +0 -194
package/src/service.ts
CHANGED
|
@@ -1,68 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { VehicleRegistry } from "@danypops/vehicle-server";
|
|
2
|
+
import { createVehicleHttpApp } from "@danypops/vehicle-server/http";
|
|
3
|
+
import type { Logger } from "@danypops/vehicle-server/logging";
|
|
4
|
+
import { SQLiteArtifactScopeStore } from "./adapters/sqlite-artifact-scope-store.ts";
|
|
5
5
|
import { SQLiteArtifactStore } from "./adapters/sqlite-artifact-store.ts";
|
|
6
|
+
import { SQLiteDiscussionRoundStore } from "./adapters/sqlite-discussion-round-store.ts";
|
|
6
7
|
import { SQLiteGateRunner } from "./adapters/sqlite-gate-runner.ts";
|
|
7
|
-
import { SQLiteArtifactScopeStore } from "./adapters/sqlite-artifact-scope-store.ts";
|
|
8
8
|
import { SQLiteGraphProjectionStore } from "./adapters/sqlite-graph-projection-store.ts";
|
|
9
|
+
import { SQLiteLogStore } from "./adapters/sqlite-log-store.ts";
|
|
10
|
+
import { SQLiteNoteEventStore } from "./adapters/sqlite-note-event-store.ts";
|
|
11
|
+
import { SQLiteSessionIdentityStore } from "./adapters/sqlite-session-identity-store.ts";
|
|
12
|
+
import { SQLiteTaskEventStore } from "./adapters/sqlite-task-event-store.ts";
|
|
9
13
|
import { SQLiteTaskFocusStore } from "./adapters/sqlite-task-focus-store.ts";
|
|
10
14
|
import { SQLiteTaskLeaseStore } from "./adapters/sqlite-task-lease-store.ts";
|
|
11
|
-
import { SQLiteTaskEventStore } from "./adapters/sqlite-task-event-store.ts";
|
|
12
|
-
import { SQLiteNoteEventStore } from "./adapters/sqlite-note-event-store.ts";
|
|
13
15
|
import { SQLiteTaskScopeStore } from "./adapters/sqlite-task-scope-store.ts";
|
|
14
|
-
import {
|
|
16
|
+
import { removeArtifactSubtree } from "./artifact-subtree.ts";
|
|
17
|
+
import { type AuthorityClaim, AuthorityRegistry, AuthorizedArtifactWriter } from "./authority-registry.ts";
|
|
18
|
+
import { SERVICE_MAX_BODY_BYTES, SQLITE_SCHEMA_VERSION } from "./constants.ts";
|
|
19
|
+
import { migrateDb, openDb, schemaVersion } from "./db.ts";
|
|
20
|
+
import { Discussions } from "./discussion-service.ts";
|
|
15
21
|
import type { CreateArtifactInput } from "./domain/artifact.ts";
|
|
16
|
-
import { AuthorityRegistry, AuthorizedArtifactWriter, type AuthorityClaim } from "./authority-registry.ts";
|
|
17
22
|
import type { TaskEventContext } from "./domain/task-event.ts";
|
|
18
23
|
import type { TaskViewMode } from "./domain/task-scope.ts";
|
|
19
|
-
import
|
|
24
|
+
import { listInjectableRules } from "./domain-services.ts";
|
|
25
|
+
import { Logs } from "./log-service.ts";
|
|
26
|
+
import { OperationRegistry } from "./module-registry.ts";
|
|
27
|
+
import { DISCUSS_OPERATION_NAMES, discussOperations } from "./modules/discuss.ts";
|
|
28
|
+
import { DOCS_OPERATION_NAMES, docsOperations } from "./modules/docs.ts";
|
|
29
|
+
import { GRAPH_PROJECTION_OPERATION_NAMES, graphProjectionOperations } from "./modules/graph-projection.ts";
|
|
30
|
+
import { LOGS_OPERATION_NAMES, logsOperations } from "./modules/logs.ts";
|
|
31
|
+
import { NOTES_OPERATION_NAMES, notesOperations } from "./modules/notes.ts";
|
|
32
|
+
import { PLAYBOOKS_OPERATION_NAMES, playbooksOperations } from "./modules/playbooks.ts";
|
|
33
|
+
import { RULES_OPERATION_NAMES, rulesOperations } from "./modules/rules.ts";
|
|
34
|
+
import { SESSION_IDENTITY_OPERATION_NAMES, sessionIdentityOperations } from "./modules/session-identity.ts";
|
|
35
|
+
import { TASKS_OPERATION_NAMES, tasksOperations } from "./modules/tasks.ts";
|
|
36
|
+
import { NOTE_SUBTYPE, Notes } from "./note-service.ts";
|
|
20
37
|
import type { ArtifactEventReader } from "./ports/artifact-event-reader.ts";
|
|
38
|
+
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
21
39
|
import type { ArtifactTrashStore } from "./ports/artifact-trash-store.ts";
|
|
22
40
|
import type { GateRunner } from "./ports/gate-runner.ts";
|
|
23
41
|
import type { TaskEventStore } from "./ports/task-event-store.ts";
|
|
24
42
|
import type { TaskScopeStore } from "./ports/task-scope-store.ts";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
listInjectableRules,
|
|
28
|
-
} from "./domain-services.ts";
|
|
29
|
-
import { Notes, NOTE_SUBTYPE } from "./note-service.ts";
|
|
43
|
+
import { InvalidSessionSecretError, SessionIdentity } from "./session-identity-service.ts";
|
|
44
|
+
import { type TaskStatus, Tasks } from "./task-service.ts";
|
|
30
45
|
import { createPapyrusVehicleRegistry } from "./vehicle/papyrus-vehicle.ts";
|
|
31
|
-
import
|
|
32
|
-
import { createVehicleHttpApp } from "@danypops/vehicle-server/http";
|
|
33
|
-
import { Logs } from "./log-service.ts";
|
|
34
|
-
import { SQLiteLogStore } from "./adapters/sqlite-log-store.ts";
|
|
35
|
-
import { SessionIdentity, InvalidSessionSecretError } from "./session-identity-service.ts";
|
|
36
|
-
import { OperationRegistry } from "./module-registry.ts";
|
|
37
|
-
import { docsOperations, DOCS_OPERATION_NAMES } from "./modules/docs.ts";
|
|
38
|
-
import { graphProjectionOperations, GRAPH_PROJECTION_OPERATION_NAMES } from "./modules/graph-projection.ts";
|
|
39
|
-
import { logsOperations, LOGS_OPERATION_NAMES } from "./modules/logs.ts";
|
|
40
|
-
import { notesOperations, NOTES_OPERATION_NAMES } from "./modules/notes.ts";
|
|
41
|
-
import { rulesOperations, RULES_OPERATION_NAMES } from "./modules/rules.ts";
|
|
42
|
-
import { instantiateSkillOrTemplate, skillsOperations, SKILLS_OPERATION_NAMES } from "./modules/skills.ts";
|
|
43
|
-
import { playbooksOperations, PLAYBOOKS_OPERATION_NAMES } from "./modules/playbooks.ts";
|
|
44
|
-
import { sessionIdentityOperations, SESSION_IDENTITY_OPERATION_NAMES } from "./modules/session-identity.ts";
|
|
45
|
-
import { discussOperations, DISCUSS_OPERATION_NAMES } from "./modules/discuss.ts";
|
|
46
|
-
import { tasksOperations, TASKS_OPERATION_NAMES } from "./modules/tasks.ts";
|
|
47
|
-
import { Discussions } from "./discussion-service.ts";
|
|
48
|
-
import { SQLiteDiscussionRoundStore } from "./adapters/sqlite-discussion-round-store.ts";
|
|
46
|
+
import { VERSION } from "./version.ts";
|
|
49
47
|
|
|
50
48
|
/**
|
|
51
49
|
* Operations with no registered module: the generic, cross-cutting kernel surface
|
|
52
50
|
* (artifact create/query/show, graph link/unlink/tree/status/history, gates run --
|
|
53
51
|
* no domain owns creation/linking/traversal for every kind, the same way system.migrate
|
|
54
|
-
* has no owning module) and
|
|
55
|
-
* needs tasks.active()
|
|
56
|
-
* src/modules/rules.ts and src/modules/skills.ts's module comments. Discourse's own
|
|
52
|
+
* has no owning module) and one permanent composition-root exception (rules.injectable
|
|
53
|
+
* needs tasks.active()) -- see src/modules/rules.ts's own module comment. Discourse's own
|
|
57
54
|
* Papyrus-embedded storage (discourse.store) was removed entirely -- zero real callers
|
|
58
55
|
* were ever confirmed against it; Discourse's real home is the standalone
|
|
59
56
|
* @danypops/discourse package plus host adapters.
|
|
60
57
|
*/
|
|
61
58
|
const COMPOSITION_ROOT_OPERATION_NAMES = [
|
|
62
|
-
"system.migrate",
|
|
63
|
-
"artifact.
|
|
64
|
-
"
|
|
65
|
-
"
|
|
59
|
+
"system.migrate",
|
|
60
|
+
"artifact.create",
|
|
61
|
+
"artifact.query",
|
|
62
|
+
"artifact.show",
|
|
63
|
+
"artifact.remove",
|
|
64
|
+
"artifact.remove_subtree",
|
|
65
|
+
"artifact.restore",
|
|
66
|
+
"artifact.trash_status",
|
|
67
|
+
"artifact.trash_list",
|
|
68
|
+
"graph.link",
|
|
69
|
+
"graph.unlink",
|
|
70
|
+
"graph.tree",
|
|
71
|
+
"graph.status",
|
|
72
|
+
"graph.history",
|
|
73
|
+
"gates.run",
|
|
74
|
+
"rules.injectable",
|
|
66
75
|
] as const;
|
|
67
76
|
|
|
68
77
|
/**
|
|
@@ -80,7 +89,6 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
80
89
|
...DOCS_OPERATION_NAMES,
|
|
81
90
|
...NOTES_OPERATION_NAMES,
|
|
82
91
|
...RULES_OPERATION_NAMES,
|
|
83
|
-
...SKILLS_OPERATION_NAMES,
|
|
84
92
|
...PLAYBOOKS_OPERATION_NAMES,
|
|
85
93
|
...GRAPH_PROJECTION_OPERATION_NAMES,
|
|
86
94
|
...LOGS_OPERATION_NAMES,
|
|
@@ -88,7 +96,7 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
88
96
|
...DISCUSS_OPERATION_NAMES,
|
|
89
97
|
] as const;
|
|
90
98
|
|
|
91
|
-
export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
|
|
99
|
+
export type OperationName = (typeof EXPECTED_OPERATION_NAMES)[number];
|
|
92
100
|
type OperationInput = Record<string, unknown>;
|
|
93
101
|
type OperationHandler = (input: OperationInput) => unknown;
|
|
94
102
|
|
|
@@ -110,7 +118,7 @@ function optionalString(input: OperationInput, key: string): string | undefined
|
|
|
110
118
|
return value;
|
|
111
119
|
}
|
|
112
120
|
|
|
113
|
-
function
|
|
121
|
+
function _optionalStringArray(input: OperationInput, key: string): string[] | undefined {
|
|
114
122
|
const value = input[key];
|
|
115
123
|
if (value === undefined) return undefined;
|
|
116
124
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
|
|
@@ -131,9 +139,9 @@ function normalizeCreateInput(input: OperationInput): CreateArtifactInput {
|
|
|
131
139
|
|
|
132
140
|
function templateSubtype(artifacts: ArtifactStore, templateId: string | undefined): string | undefined {
|
|
133
141
|
if (!templateId) return undefined;
|
|
134
|
-
const defaults = artifacts.get(templateId)?.extra
|
|
142
|
+
const defaults = artifacts.get(templateId)?.extra.defaults;
|
|
135
143
|
if (typeof defaults !== "object" || defaults === null || Array.isArray(defaults)) return undefined;
|
|
136
|
-
const subtype = (defaults as Record<string, unknown>)
|
|
144
|
+
const subtype = (defaults as Record<string, unknown>).subtype;
|
|
137
145
|
return typeof subtype === "string" ? subtype : undefined;
|
|
138
146
|
}
|
|
139
147
|
|
|
@@ -170,11 +178,11 @@ const tasksAuthorityClaim: AuthorityClaim = {
|
|
|
170
178
|
|
|
171
179
|
/**
|
|
172
180
|
* The same status-bypass protection Tasks and Notes already have, extended to every other kind
|
|
173
|
-
* with its own validated transition set (Doc's draft/active/archived, Rule/
|
|
181
|
+
* with its own validated transition set (Doc's draft/active/archived, Rule/Playbook's
|
|
174
182
|
* active/deprecated) -- graph.status previously let a caller jump straight to any status string,
|
|
175
183
|
* skipping e.g. Doc's draft-must-go-through-active-before-archived rule entirely.
|
|
176
184
|
*/
|
|
177
|
-
function lifecycleAuthorityClaim(owner: "docs" | "rules" | "
|
|
185
|
+
function lifecycleAuthorityClaim(owner: "docs" | "rules" | "playbooks", kind: string): AuthorityClaim {
|
|
178
186
|
return {
|
|
179
187
|
owner,
|
|
180
188
|
matchesArtifact: (candidateKind, subtype) => candidateKind === kind && !(kind === "doc" && subtype === NOTE_SUBTYPE),
|
|
@@ -190,7 +198,6 @@ export function createAuthorityRegistry(): AuthorityRegistry {
|
|
|
190
198
|
tasksAuthorityClaim,
|
|
191
199
|
lifecycleAuthorityClaim("docs", "doc"),
|
|
192
200
|
lifecycleAuthorityClaim("rules", "rule"),
|
|
193
|
-
lifecycleAuthorityClaim("skills", "skill"),
|
|
194
201
|
lifecycleAuthorityClaim("playbooks", "playbook"),
|
|
195
202
|
]);
|
|
196
203
|
return authority;
|
|
@@ -229,9 +236,9 @@ function handlers(
|
|
|
229
236
|
artifacts: ArtifactStore & ArtifactTrashStore & ArtifactEventReader,
|
|
230
237
|
gates: GateRunner,
|
|
231
238
|
tasks: Tasks,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
_notes: Notes,
|
|
240
|
+
_events: TaskEventStore,
|
|
241
|
+
_scopes: TaskScopeStore,
|
|
235
242
|
migrate: () => unknown,
|
|
236
243
|
moduleRegistry: OperationRegistry,
|
|
237
244
|
authority: AuthorityRegistry,
|
|
@@ -241,7 +248,10 @@ function handlers(
|
|
|
241
248
|
// these six entries stay in this completeness-checked table only as a thin forward so
|
|
242
249
|
// `Record<OperationName, OperationHandler>` still guarantees every operation has an entry
|
|
243
250
|
// at compile time. The actual notes.* logic now lives in the module, not here.
|
|
244
|
-
const forwardToModule =
|
|
251
|
+
const forwardToModule =
|
|
252
|
+
(name: OperationName): OperationHandler =>
|
|
253
|
+
(input) =>
|
|
254
|
+
moduleRegistry.get(name)!.execute(input);
|
|
245
255
|
const eventContext = (input: OperationInput): TaskEventContext => ({
|
|
246
256
|
actor: optionalString(input, "actor"),
|
|
247
257
|
source: optionalString(input, "source"),
|
|
@@ -268,30 +278,41 @@ function handlers(
|
|
|
268
278
|
"system.migrate": () => migrate(),
|
|
269
279
|
"artifact.create": (input) => {
|
|
270
280
|
const normalized = normalizeCreateInput(input);
|
|
271
|
-
authority.requireArtifactAllowed(
|
|
281
|
+
authority.requireArtifactAllowed(
|
|
282
|
+
normalized.kind,
|
|
283
|
+
normalized.subtype ?? templateSubtype(artifacts, normalized.templateId),
|
|
284
|
+
"create",
|
|
285
|
+
GENERIC_CALLER,
|
|
286
|
+
);
|
|
272
287
|
authority.requireArtifactAllowed(normalized.kind, normalized.subtype, "create", GENERIC_CALLER);
|
|
273
288
|
if (normalized.kind !== "task") return artifacts.create(normalized);
|
|
274
|
-
return tasks.create(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
289
|
+
return tasks.create(
|
|
290
|
+
{
|
|
291
|
+
id: normalized.id,
|
|
292
|
+
title: string(input, "title"),
|
|
293
|
+
body: normalized.body,
|
|
294
|
+
subtype: normalized.subtype,
|
|
295
|
+
status: normalized.status as TaskStatus | undefined,
|
|
296
|
+
labels: normalized.labels,
|
|
297
|
+
extra: normalized.extra,
|
|
298
|
+
templateId: normalized.templateId,
|
|
299
|
+
projectRoot: string(input, "project_root"),
|
|
300
|
+
projectSource: "cwd",
|
|
301
|
+
},
|
|
302
|
+
eventContextFor(input, "artifact-api"),
|
|
303
|
+
);
|
|
286
304
|
},
|
|
287
305
|
"artifact.query": (input) => artifacts.query(input),
|
|
288
|
-
"artifact.show": (input) =>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
"artifact.
|
|
306
|
+
"artifact.show": (input) =>
|
|
307
|
+
artifacts.get(string(input, "id"), {
|
|
308
|
+
tree: input.tree === true,
|
|
309
|
+
depth: optionalNumber(input, "depth"),
|
|
310
|
+
maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
|
|
311
|
+
}),
|
|
312
|
+
"artifact.remove": (input) =>
|
|
313
|
+
artifacts.trash(string(input, "id"), { reason: optionalString(input, "reason"), context: eventContext(input) }),
|
|
314
|
+
"artifact.remove_subtree": (input) =>
|
|
315
|
+
removeArtifactSubtree(artifacts, string(input, "id"), { reason: optionalString(input, "reason"), context: eventContext(input) }),
|
|
295
316
|
"artifact.restore": (input) => artifacts.restore(string(input, "id"), eventContext(input)),
|
|
296
317
|
"artifact.trash_status": (input) => artifacts.trashStatus(string(input, "id")),
|
|
297
318
|
"artifact.trash_list": () => artifacts.listTrash(),
|
|
@@ -314,7 +335,11 @@ function handlers(
|
|
|
314
335
|
genericWriter.checkLink({ from, relation, to });
|
|
315
336
|
let removed: boolean;
|
|
316
337
|
if (relation === "depends_on" && artifacts.get(from)?.kind === "task" && artifacts.get(to)?.kind === "task") {
|
|
317
|
-
const before =
|
|
338
|
+
const before =
|
|
339
|
+
tasks
|
|
340
|
+
.graph()
|
|
341
|
+
.nodes.find((node) => node.task.id === from)
|
|
342
|
+
?.dependencyIds.includes(to) ?? false;
|
|
318
343
|
tasks.undepend(from, to, eventContext(input));
|
|
319
344
|
removed = before;
|
|
320
345
|
} else {
|
|
@@ -322,29 +347,29 @@ function handlers(
|
|
|
322
347
|
}
|
|
323
348
|
return { removed };
|
|
324
349
|
},
|
|
325
|
-
"graph.tree": (input) =>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
"graph.tree": (input) =>
|
|
351
|
+
artifacts.get(string(input, "id"), {
|
|
352
|
+
tree: true,
|
|
353
|
+
depth: optionalNumber(input, "depth"),
|
|
354
|
+
maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
|
|
355
|
+
}),
|
|
330
356
|
"graph.status": (input) => genericWriter.setStatus(string(input, "id"), string(input, "status"), eventContext(input)),
|
|
331
|
-
"graph.history": (input) =>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
357
|
+
"graph.history": (input) =>
|
|
358
|
+
artifacts.events({
|
|
359
|
+
artifactId: optionalString(input, "id"),
|
|
360
|
+
actor: optionalString(input, "actor"),
|
|
361
|
+
sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
|
|
362
|
+
since: optionalString(input, "since"),
|
|
363
|
+
limit: optionalNumber(input, "limit"),
|
|
364
|
+
cursor: optionalNumber(input, "cursor"),
|
|
365
|
+
direction: optionalString(input, "direction") as "asc" | "desc" | undefined,
|
|
366
|
+
}),
|
|
340
367
|
"gates.run": (input) => {
|
|
341
368
|
const id = string(input, "id");
|
|
342
|
-
return artifacts.get(id)?.kind === "task"
|
|
343
|
-
? tasks.runGates(id, eventContextFor(input, "gates-api"))
|
|
344
|
-
: gates.runAsync(id);
|
|
369
|
+
return artifacts.get(id)?.kind === "task" ? tasks.runGates(id, eventContextFor(input, "gates-api")) : gates.runAsync(id);
|
|
345
370
|
},
|
|
346
|
-
"rules.injectable": (input) =>
|
|
347
|
-
.map(({ id, title, body, extra }) => ({ id, title, body, extra })),
|
|
371
|
+
"rules.injectable": (input) =>
|
|
372
|
+
listInjectableRules(artifacts, tasks.active(taskFilter(input))?.id).map(({ id, title, body, extra }) => ({ id, title, body, extra })),
|
|
348
373
|
"tasks.create": forwardToModule("tasks.create"),
|
|
349
374
|
"tasks.update": forwardToModule("tasks.update"),
|
|
350
375
|
"tasks.list": forwardToModule("tasks.list"),
|
|
@@ -408,16 +433,6 @@ function handlers(
|
|
|
408
433
|
"rules.gate": forwardToModule("rules.gate"),
|
|
409
434
|
"rules.assign_project": forwardToModule("rules.assign_project"),
|
|
410
435
|
"rules.update": forwardToModule("rules.update"),
|
|
411
|
-
"skills.create": forwardToModule("skills.create"),
|
|
412
|
-
"skills.create_template": forwardToModule("skills.create_template"),
|
|
413
|
-
"skills.list": forwardToModule("skills.list"),
|
|
414
|
-
"skills.show": forwardToModule("skills.show"),
|
|
415
|
-
"skills.invoke": forwardToModule("skills.invoke"),
|
|
416
|
-
"skills.run": forwardToModule("skills.run"),
|
|
417
|
-
"skills.enable": forwardToModule("skills.enable"),
|
|
418
|
-
"skills.disable": forwardToModule("skills.disable"),
|
|
419
|
-
"skills.assign_project": forwardToModule("skills.assign_project"),
|
|
420
|
-
"skills.update": forwardToModule("skills.update"),
|
|
421
436
|
"playbooks.create": forwardToModule("playbooks.create"),
|
|
422
437
|
"playbooks.list": forwardToModule("playbooks.list"),
|
|
423
438
|
"playbooks.show": forwardToModule("playbooks.show"),
|
|
@@ -431,7 +446,6 @@ function handlers(
|
|
|
431
446
|
"playbooks.uncontain": forwardToModule("playbooks.uncontain"),
|
|
432
447
|
"playbooks.depend": forwardToModule("playbooks.depend"),
|
|
433
448
|
"playbooks.undepend": forwardToModule("playbooks.undepend"),
|
|
434
|
-
"skills.instantiate": (input) => instantiateSkillOrTemplate({ artifacts, tasks, authority }, input, eventContextFor(input, "template-instantiation")),
|
|
435
449
|
"graph_projection.apply": forwardToModule("graph_projection.apply"),
|
|
436
450
|
"graph_projection.checkpoint": forwardToModule("graph_projection.checkpoint"),
|
|
437
451
|
"logs.append": forwardToModule("logs.append"),
|
|
@@ -468,7 +482,16 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
468
482
|
const sessionIdentity = new SessionIdentity(new SQLiteSessionIdentityStore(db));
|
|
469
483
|
const discussions = new Discussions(artifacts, new SQLiteDiscussionRoundStore(db));
|
|
470
484
|
const authority = createAuthorityRegistry();
|
|
471
|
-
const vehicle = createPapyrusVehicleRegistry({
|
|
485
|
+
const vehicle = createPapyrusVehicleRegistry({
|
|
486
|
+
artifacts,
|
|
487
|
+
scopes: artifactScopes,
|
|
488
|
+
authority,
|
|
489
|
+
notes,
|
|
490
|
+
events,
|
|
491
|
+
taskScopes: scopes,
|
|
492
|
+
tasks,
|
|
493
|
+
sessionIdentity,
|
|
494
|
+
});
|
|
472
495
|
const moduleRegistry = new OperationRegistry();
|
|
473
496
|
moduleRegistry.registerAll(notesOperations(notes));
|
|
474
497
|
moduleRegistry.registerAll(logsOperations(logs));
|
|
@@ -477,7 +500,6 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
477
500
|
moduleRegistry.registerAll(tasksOperations(tasks, artifacts, sessionIdentity));
|
|
478
501
|
moduleRegistry.registerAll(docsOperations(artifacts, artifactScopes, authority));
|
|
479
502
|
moduleRegistry.registerAll(rulesOperations(artifacts, artifactScopes));
|
|
480
|
-
moduleRegistry.registerAll(skillsOperations({ artifacts, events, scopes, artifactScopes, authority }));
|
|
481
503
|
moduleRegistry.registerAll(playbooksOperations({ artifacts, events, scopes, artifactScopes, tasks, sessionIdentity }));
|
|
482
504
|
moduleRegistry.registerAll(graphProjectionOperations(artifacts, projections, authority));
|
|
483
505
|
const registry = handlers(artifacts, gates, tasks, notes, events, scopes, () => migrateDb(db), moduleRegistry, authority);
|
|
@@ -497,8 +519,12 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
497
519
|
}
|
|
498
520
|
return handler(input);
|
|
499
521
|
},
|
|
500
|
-
checkpoint: () => {
|
|
501
|
-
|
|
522
|
+
checkpoint: () => {
|
|
523
|
+
db.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
|
524
|
+
},
|
|
525
|
+
optimize: () => {
|
|
526
|
+
db.exec("PRAGMA optimize");
|
|
527
|
+
},
|
|
502
528
|
reapStaleFocus: () => tasks.reapStaleFocus(),
|
|
503
529
|
purgeDueTrash: () => artifacts.purgeDueTrash(),
|
|
504
530
|
close: () => {
|
|
@@ -533,7 +559,10 @@ async function readOperationBody(request: Request): Promise<{ op?: unknown; inpu
|
|
|
533
559
|
}
|
|
534
560
|
const bytes = new Uint8Array(size);
|
|
535
561
|
let offset = 0;
|
|
536
|
-
for (const chunk of chunks) {
|
|
562
|
+
for (const chunk of chunks) {
|
|
563
|
+
bytes.set(chunk, offset);
|
|
564
|
+
offset += chunk.byteLength;
|
|
565
|
+
}
|
|
537
566
|
return JSON.parse(new TextDecoder().decode(bytes)) as { op?: unknown; input?: unknown };
|
|
538
567
|
}
|
|
539
568
|
|
|
@@ -547,9 +576,11 @@ export function createApp(deps: {
|
|
|
547
576
|
* (daemon.ts) wires this to a PushChannel; tests and other embedders can ignore it.
|
|
548
577
|
*/
|
|
549
578
|
onOperationExecuted?: (operation: string, input: OperationInput) => void;
|
|
579
|
+
/** Defaults to a no-op (createVehicleHttpApp's own default) -- daemon.ts wires vehicleLogger() so a failed invocation is actually logged, not silently discarded. */
|
|
580
|
+
logger?: Logger;
|
|
550
581
|
}): { fetch(request: Request): Promise<Response> } {
|
|
551
582
|
// Same Bearer token, daemon, and port as the rest of this API -- see ./vehicle/papyrus-vehicle.ts.
|
|
552
|
-
const vehicleApp = createVehicleHttpApp({ registry: deps.service.vehicle, token: deps.token });
|
|
583
|
+
const vehicleApp = createVehicleHttpApp({ registry: deps.service.vehicle, token: deps.token, logger: deps.logger });
|
|
553
584
|
return {
|
|
554
585
|
async fetch(request: Request): Promise<Response> {
|
|
555
586
|
if (request.headers.get("authorization") !== `Bearer ${deps.token}`) {
|
|
@@ -575,7 +606,14 @@ export function createApp(deps: {
|
|
|
575
606
|
deps.onOperationExecuted?.(body.op, input as OperationInput);
|
|
576
607
|
return json({ result });
|
|
577
608
|
} catch (error) {
|
|
578
|
-
const status =
|
|
609
|
+
const status =
|
|
610
|
+
error instanceof PayloadTooLargeError
|
|
611
|
+
? 413
|
|
612
|
+
: error instanceof UnknownOperationError
|
|
613
|
+
? 404
|
|
614
|
+
: error instanceof InvalidSessionSecretError
|
|
615
|
+
? 403
|
|
616
|
+
: 400;
|
|
579
617
|
return json({ error: error instanceof Error ? error.message : String(error) }, { status });
|
|
580
618
|
}
|
|
581
619
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isSessionRegistered,
|
|
3
|
+
registerSessionIdentity,
|
|
4
|
+
releaseSessionIdentity,
|
|
5
|
+
verifySessionSecret,
|
|
6
|
+
} from "@danypops/vehicle-server/session-identity";
|
|
2
7
|
import { assertValidSessionId } from "./domain/session-identity.ts";
|
|
3
8
|
import type { SessionIdentityStore } from "./ports/session-identity-store.ts";
|
|
4
9
|
|
|
@@ -50,6 +55,9 @@ export class SessionIdentity {
|
|
|
50
55
|
assertAuthorized(sessionId: string | undefined, secret: string | undefined): void {
|
|
51
56
|
if (sessionId === undefined) return;
|
|
52
57
|
if (!this.isRegistered(sessionId)) return;
|
|
53
|
-
if (!this.verify(sessionId, secret))
|
|
58
|
+
if (!this.verify(sessionId, secret))
|
|
59
|
+
throw new InvalidSessionSecretError(
|
|
60
|
+
`session "${sessionId}" is registered; a valid session_secret is required to mutate its Task Focus`,
|
|
61
|
+
);
|
|
54
62
|
}
|
|
55
63
|
}
|
package/src/task-context.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
+
import { TASK_CONTEXT_CURRENT_LIMIT, TASK_CONTEXT_REJECTED_LIMIT, TASK_RECONCILIATION_INSTRUCTION } from "./constants.ts";
|
|
1
2
|
import type { Artifact } from "./domain/artifact.ts";
|
|
2
3
|
import { DISCUSSION_SUBTYPE, readDiscussionExtra } from "./domain/discussion.ts";
|
|
3
4
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
4
|
-
import {
|
|
5
|
-
TASK_CONTEXT_CURRENT_LIMIT,
|
|
6
|
-
TASK_CONTEXT_REJECTED_LIMIT,
|
|
7
|
-
TASK_RECONCILIATION_INSTRUCTION,
|
|
8
|
-
} from "./constants.ts";
|
|
9
5
|
|
|
10
6
|
interface Gate {
|
|
11
7
|
type?: unknown;
|
|
@@ -14,8 +10,8 @@ interface Gate {
|
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
function gatesFrom(task: Artifact): Gate[] {
|
|
17
|
-
const gates = task.extra
|
|
18
|
-
return Array.isArray(gates) ? gates as Gate[] : [];
|
|
13
|
+
const gates = task.extra.gates;
|
|
14
|
+
return Array.isArray(gates) ? (gates as Gate[]) : [];
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
function renderGate(gate: Gate): string {
|
|
@@ -45,14 +41,22 @@ function inScope(taskId: string, activeTaskId: string | undefined, taskIds: Set<
|
|
|
45
41
|
return taskIds === undefined || taskIds.has(taskId) || taskId === activeTaskId;
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
function deferredBlockingDiscussions(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
function deferredBlockingDiscussions(
|
|
45
|
+
artifacts: ArtifactStore,
|
|
46
|
+
activeTaskId: string | undefined,
|
|
47
|
+
taskIds: Set<string> | undefined,
|
|
48
|
+
): string[] {
|
|
49
|
+
const discussions = artifacts.query({ kind: "task", subtype: DISCUSSION_SUBTYPE }).filter((discussion) => {
|
|
50
|
+
try {
|
|
51
|
+
return readDiscussionExtra(discussion.extra).state === "deferred";
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
53
56
|
if (discussions.length === 0) return [];
|
|
54
57
|
|
|
55
|
-
const blocks = artifacts
|
|
58
|
+
const blocks = artifacts
|
|
59
|
+
.relationships({ artifactIds: discussions.map((discussion) => discussion.id) })
|
|
56
60
|
.filter((edge) => edge.relation === "blocks");
|
|
57
61
|
const lines: string[] = [];
|
|
58
62
|
for (const discussion of discussions) {
|
|
@@ -76,8 +80,14 @@ export type TaskContextVerbosity = "summary" | "full";
|
|
|
76
80
|
* exists and that the full plan is one explicit call away -- avoiding repeating the same
|
|
77
81
|
* unchanged prose every single turn for a task that can persist across dozens of turns.
|
|
78
82
|
*/
|
|
79
|
-
export function taskContext(
|
|
80
|
-
|
|
83
|
+
export function taskContext(
|
|
84
|
+
artifacts: ArtifactStore,
|
|
85
|
+
activeTaskId?: string,
|
|
86
|
+
taskIds?: Set<string>,
|
|
87
|
+
verbosity: TaskContextVerbosity = "full",
|
|
88
|
+
): string | null {
|
|
89
|
+
const tasks = artifacts
|
|
90
|
+
.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE })
|
|
81
91
|
.filter((task) => taskIds === undefined || taskIds.has(task.id))
|
|
82
92
|
.sort((left, right) => left.updated_at.localeCompare(right.updated_at));
|
|
83
93
|
const open = tasks.filter((task) => task.status !== "done" && task.status !== "canceled");
|
|
@@ -86,7 +96,9 @@ export function taskContext(artifacts: ArtifactStore, activeTaskId?: string, tas
|
|
|
86
96
|
|
|
87
97
|
const done = tasks.length - open.length;
|
|
88
98
|
const active = activeTaskId ? open.find((task) => task.id === activeTaskId) : undefined;
|
|
89
|
-
const current = active
|
|
99
|
+
const current = active
|
|
100
|
+
? [active]
|
|
101
|
+
: open.filter((task) => task.status === "in-progress" || task.status === "review").slice(0, TASK_CONTEXT_CURRENT_LIMIT);
|
|
90
102
|
const next = open.find((task) => task.status === "todo");
|
|
91
103
|
const rejected = open.filter((task) => task.status === "rejected").slice(0, TASK_CONTEXT_REJECTED_LIMIT);
|
|
92
104
|
const lines = tasks.length > 0 ? [`Progress: ${done}/${tasks.length} done`] : [];
|
package/src/task-execution.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import { TASK_EXECUTION_MAX_DEGREE, TASK_EXECUTION_MAX_EDGES, TASK_EXECUTION_MAX_NODES } from "./constants.ts";
|
|
2
2
|
import type { TaskGraph } from "./task-service.ts";
|
|
3
3
|
|
|
4
|
-
export type TaskExecutionState =
|
|
5
|
-
| "todo"
|
|
6
|
-
| "in-progress"
|
|
7
|
-
| "review"
|
|
8
|
-
| "rejected"
|
|
9
|
-
| "done"
|
|
10
|
-
| "canceled"
|
|
11
|
-
| "ready"
|
|
12
|
-
| "blocked"
|
|
13
|
-
| "invalid";
|
|
4
|
+
export type TaskExecutionState = "todo" | "in-progress" | "review" | "rejected" | "done" | "canceled" | "ready" | "blocked" | "invalid";
|
|
14
5
|
|
|
15
6
|
export interface TaskExecutionNode {
|
|
16
7
|
id: string;
|
|
@@ -56,8 +47,9 @@ function assertBounds(graph: TaskGraph): void {
|
|
|
56
47
|
/** Build deterministic topological layers ordered by creation time and task ID. */
|
|
57
48
|
export function projectTaskExecution(graph: TaskGraph): TaskExecutionPlan {
|
|
58
49
|
assertBounds(graph);
|
|
59
|
-
const orderedNodes = [...graph.nodes].sort(
|
|
60
|
-
left.task.created_at.localeCompare(right.task.created_at) || left.task.id.localeCompare(right.task.id)
|
|
50
|
+
const orderedNodes = [...graph.nodes].sort(
|
|
51
|
+
(left, right) => left.task.created_at.localeCompare(right.task.created_at) || left.task.id.localeCompare(right.task.id),
|
|
52
|
+
);
|
|
61
53
|
const byId = new Map(orderedNodes.map((node) => [node.task.id, node]));
|
|
62
54
|
const order = new Map(orderedNodes.map((node, index) => [node.task.id, index]));
|
|
63
55
|
const successors = new Map(orderedNodes.map((node) => [node.task.id, [] as string[]]));
|
package/src/task-graph-view.ts
CHANGED
|
@@ -36,18 +36,18 @@ export function projectTaskGraph(graph: TaskGraph, view: TaskGraphView): Display
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const connected =
|
|
40
|
-
? new Set(graph.nodes.map((node) => node.task.id))
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
const connected =
|
|
40
|
+
view === "execution" ? new Set(graph.nodes.map((node) => node.task.id)) : new Set(edges.flatMap((edge) => [edge.from, edge.to]));
|
|
41
|
+
const nodes =
|
|
42
|
+
view === "execution"
|
|
43
|
+
? projectTaskExecution(graph).nodes.map((node) => ({
|
|
44
|
+
id: node.id,
|
|
45
|
+
label: `${node.active ? "▶ " : ""}${EXECUTION_GLYPHS[node.state]} ${node.title} · ${node.layer === null ? "no layer" : `layer ${node.layer + 1}`} · ${node.state}`,
|
|
46
|
+
status: node.state,
|
|
47
|
+
}))
|
|
48
|
+
: graph.nodes
|
|
49
|
+
.filter((node) => connected.has(node.task.id))
|
|
50
|
+
.map((node) => ({ id: node.task.id, label: node.task.title, status: node.task.status }));
|
|
51
51
|
return {
|
|
52
52
|
direction: "TD",
|
|
53
53
|
nodes,
|
|
@@ -32,9 +32,7 @@ export function projectTaskRelationships(task: Artifact, graph?: TaskGraph): Dis
|
|
|
32
32
|
}
|
|
33
33
|
const nodes: DisplayGraphNode[] = [...nodeIds].map((id) => {
|
|
34
34
|
const artifact = taskNodes.get(id);
|
|
35
|
-
return artifact
|
|
36
|
-
? { id, label: artifact.title, status: artifact.status }
|
|
37
|
-
: { id, label: fallbackLabel(id) };
|
|
35
|
+
return artifact ? { id, label: artifact.title, status: artifact.status } : { id, label: fallbackLabel(id) };
|
|
38
36
|
});
|
|
39
37
|
return { direction: "LR", nodes, edges };
|
|
40
38
|
}
|