@danypops/papyrus 0.42.0 → 0.42.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +3 -3
- package/src/artifact-subtree.ts +4 -2
- package/src/authority-registry.ts +2 -1
- package/src/cli.ts +785 -179
- package/src/client.ts +46 -20
- package/src/constants.ts +15 -4
- package/src/daemon-state.ts +4 -12
- package/src/daemon.ts +31 -9
- package/src/db.ts +119 -98
- package/src/discussion-service.ts +109 -44
- package/src/domain/artifact-event.ts +17 -4
- package/src/domain/artifact.ts +3 -1
- package/src/domain/blueprint-definition.ts +26 -31
- 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 +241 -110
- 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 +33 -21
- package/src/modules/notes.ts +66 -28
- package/src/modules/playbooks.ts +88 -29
- 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 +131 -68
- package/src/playbook-definition.ts +56 -17
- package/src/playbook-execution.ts +13 -3
- 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 +143 -89
- 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 +167 -72
- package/src/vehicle/artifact-trash-vehicle.ts +25 -13
- package/src/vehicle/artifact-vehicle-shared.ts +28 -7
- package/src/vehicle/docs-vehicle.ts +48 -16
- package/src/vehicle/notes-vehicle.ts +24 -6
- package/src/vehicle/papyrus-vehicle.ts +14 -3
- package/src/vehicle/playbooks-vehicle.ts +87 -18
- 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 +71 -52
|
@@ -37,7 +37,9 @@ export interface TaskFocusStore {
|
|
|
37
37
|
export class InMemoryTaskFocusStore implements TaskFocusStore {
|
|
38
38
|
private readonly state = new Map<string, TaskFocusState>();
|
|
39
39
|
|
|
40
|
-
get(scope?: string): TaskFocusState | undefined {
|
|
40
|
+
get(scope?: string): TaskFocusState | undefined {
|
|
41
|
+
return this.state.get(normalizeFocusScope(scope));
|
|
42
|
+
}
|
|
41
43
|
|
|
42
44
|
set(taskId: string, scope?: string): TaskFocusState {
|
|
43
45
|
const key = normalizeFocusScope(scope);
|
|
@@ -51,7 +53,12 @@ export class InMemoryTaskFocusStore implements TaskFocusStore {
|
|
|
51
53
|
const key = normalizeFocusScope(scope);
|
|
52
54
|
const current = this.state.get(key);
|
|
53
55
|
if (current?.taskId !== taskId) throw new Error(`task "${taskId}" is not focused`);
|
|
54
|
-
const focus: TaskFocusState = {
|
|
56
|
+
const focus: TaskFocusState = {
|
|
57
|
+
...current,
|
|
58
|
+
status: "paused",
|
|
59
|
+
updatedAt: new Date().toISOString(),
|
|
60
|
+
...(reason ? { pauseReason: reason } : {}),
|
|
61
|
+
};
|
|
55
62
|
this.state.set(key, focus);
|
|
56
63
|
return focus;
|
|
57
64
|
}
|
|
@@ -79,7 +86,10 @@ export class InMemoryTaskFocusStore implements TaskFocusStore {
|
|
|
79
86
|
reapStale(olderThanIso: string): number {
|
|
80
87
|
let removed = 0;
|
|
81
88
|
for (const [key, focus] of this.state) {
|
|
82
|
-
if (focus.updatedAt < olderThanIso) {
|
|
89
|
+
if (focus.updatedAt < olderThanIso) {
|
|
90
|
+
this.state.delete(key);
|
|
91
|
+
removed++;
|
|
92
|
+
}
|
|
83
93
|
}
|
|
84
94
|
return removed;
|
|
85
95
|
}
|
|
@@ -88,7 +98,10 @@ export class InMemoryTaskFocusStore implements TaskFocusStore {
|
|
|
88
98
|
let oldestKey: string | undefined;
|
|
89
99
|
let oldestAt: string | undefined;
|
|
90
100
|
for (const [key, focus] of this.state) {
|
|
91
|
-
if (oldestAt === undefined || focus.updatedAt < oldestAt) {
|
|
101
|
+
if (oldestAt === undefined || focus.updatedAt < oldestAt) {
|
|
102
|
+
oldestKey = key;
|
|
103
|
+
oldestAt = focus.updatedAt;
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
if (oldestKey !== undefined) this.state.delete(oldestKey);
|
|
94
107
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TASK_LEASE_DEFAULT_TTL_MS } from "../constants.ts";
|
|
2
|
-
import { isLeaseExpired, validateLeaseNote, validateLeaseOwner, validateLeaseTtlMs
|
|
2
|
+
import { isLeaseExpired, type TaskLease, validateLeaseNote, validateLeaseOwner, validateLeaseTtlMs } from "../domain/task-lease.ts";
|
|
3
3
|
|
|
4
4
|
export interface TaskLeaseStore {
|
|
5
5
|
/**
|
|
@@ -51,7 +51,8 @@ export class InMemoryTaskLeaseStore implements TaskLeaseStore {
|
|
|
51
51
|
const nowIso = new Date().toISOString();
|
|
52
52
|
const current = this.leases.get(taskId);
|
|
53
53
|
if (!current || isLeaseExpired(current, nowIso)) throw new Error(`task "${taskId}" has no live lease to renew`);
|
|
54
|
-
if (current.owner !== owner || current.token !== token)
|
|
54
|
+
if (current.owner !== owner || current.token !== token)
|
|
55
|
+
throw new Error(`lease for task "${taskId}" is held by a different owner/token`);
|
|
55
56
|
const renewed: TaskLease = { ...current, leaseExpiresAt: new Date(Date.now() + ttlMs).toISOString(), heartbeatAt: nowIso };
|
|
56
57
|
this.leases.set(taskId, renewed);
|
|
57
58
|
return renewed;
|
|
@@ -61,7 +62,8 @@ export class InMemoryTaskLeaseStore implements TaskLeaseStore {
|
|
|
61
62
|
const nowIso = new Date().toISOString();
|
|
62
63
|
const current = this.leases.get(taskId);
|
|
63
64
|
if (!current || isLeaseExpired(current, nowIso)) return { released: false };
|
|
64
|
-
if (current.owner !== owner || current.token !== token)
|
|
65
|
+
if (current.owner !== owner || current.token !== token)
|
|
66
|
+
throw new Error(`lease for task "${taskId}" is held by a different owner/token`);
|
|
65
67
|
this.leases.delete(taskId);
|
|
66
68
|
return { released: true };
|
|
67
69
|
}
|
|
@@ -75,7 +77,10 @@ export class InMemoryTaskLeaseStore implements TaskLeaseStore {
|
|
|
75
77
|
reapExpired(olderThanIso: string): number {
|
|
76
78
|
let removed = 0;
|
|
77
79
|
for (const [taskId, lease] of this.leases) {
|
|
78
|
-
if (lease.leaseExpiresAt < olderThanIso) {
|
|
80
|
+
if (lease.leaseExpiresAt < olderThanIso) {
|
|
81
|
+
this.leases.delete(taskId);
|
|
82
|
+
removed++;
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
85
|
return removed;
|
|
81
86
|
}
|
|
@@ -18,7 +18,9 @@ export class InMemoryTaskScopeStore implements TaskScopeStore {
|
|
|
18
18
|
return scope;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
get(taskId: string): TaskProjectScope | undefined {
|
|
21
|
+
get(taskId: string): TaskProjectScope | undefined {
|
|
22
|
+
return this.scopes.get(taskId);
|
|
23
|
+
}
|
|
22
24
|
|
|
23
25
|
taskIds(projectRoot: string | undefined, limit: number): string[] {
|
|
24
26
|
return [...this.scopes.values()]
|
package/src/service.ts
CHANGED
|
@@ -1,50 +1,49 @@
|
|
|
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 { playbooksOperations, PLAYBOOKS_OPERATION_NAMES } from "./modules/playbooks.ts";
|
|
43
|
-
import { sessionIdentityOperations, SESSION_IDENTITY_OPERATION_NAMES } from "./modules/session-identity.ts";
|
|
44
|
-
import { discussOperations, DISCUSS_OPERATION_NAMES } from "./modules/discuss.ts";
|
|
45
|
-
import { tasksOperations, TASKS_OPERATION_NAMES } from "./modules/tasks.ts";
|
|
46
|
-
import { Discussions } from "./discussion-service.ts";
|
|
47
|
-
import { SQLiteDiscussionRoundStore } from "./adapters/sqlite-discussion-round-store.ts";
|
|
46
|
+
import { VERSION } from "./version.ts";
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* Operations with no registered module: the generic, cross-cutting kernel surface
|
|
@@ -57,9 +56,21 @@ import { SQLiteDiscussionRoundStore } from "./adapters/sqlite-discussion-round-s
|
|
|
57
56
|
* @danypops/discourse package plus host adapters.
|
|
58
57
|
*/
|
|
59
58
|
const COMPOSITION_ROOT_OPERATION_NAMES = [
|
|
60
|
-
"system.migrate",
|
|
61
|
-
"artifact.
|
|
62
|
-
"
|
|
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",
|
|
63
74
|
"rules.injectable",
|
|
64
75
|
] as const;
|
|
65
76
|
|
|
@@ -85,7 +96,7 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
85
96
|
...DISCUSS_OPERATION_NAMES,
|
|
86
97
|
] as const;
|
|
87
98
|
|
|
88
|
-
export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
|
|
99
|
+
export type OperationName = (typeof EXPECTED_OPERATION_NAMES)[number];
|
|
89
100
|
type OperationInput = Record<string, unknown>;
|
|
90
101
|
type OperationHandler = (input: OperationInput) => unknown;
|
|
91
102
|
|
|
@@ -107,7 +118,7 @@ function optionalString(input: OperationInput, key: string): string | undefined
|
|
|
107
118
|
return value;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
function
|
|
121
|
+
function _optionalStringArray(input: OperationInput, key: string): string[] | undefined {
|
|
111
122
|
const value = input[key];
|
|
112
123
|
if (value === undefined) return undefined;
|
|
113
124
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`${key} must be an array of strings`);
|
|
@@ -128,9 +139,9 @@ function normalizeCreateInput(input: OperationInput): CreateArtifactInput {
|
|
|
128
139
|
|
|
129
140
|
function templateSubtype(artifacts: ArtifactStore, templateId: string | undefined): string | undefined {
|
|
130
141
|
if (!templateId) return undefined;
|
|
131
|
-
const defaults = artifacts.get(templateId)?.extra
|
|
142
|
+
const defaults = artifacts.get(templateId)?.extra.defaults;
|
|
132
143
|
if (typeof defaults !== "object" || defaults === null || Array.isArray(defaults)) return undefined;
|
|
133
|
-
const subtype = (defaults as Record<string, unknown>)
|
|
144
|
+
const subtype = (defaults as Record<string, unknown>).subtype;
|
|
134
145
|
return typeof subtype === "string" ? subtype : undefined;
|
|
135
146
|
}
|
|
136
147
|
|
|
@@ -225,9 +236,9 @@ function handlers(
|
|
|
225
236
|
artifacts: ArtifactStore & ArtifactTrashStore & ArtifactEventReader,
|
|
226
237
|
gates: GateRunner,
|
|
227
238
|
tasks: Tasks,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
_notes: Notes,
|
|
240
|
+
_events: TaskEventStore,
|
|
241
|
+
_scopes: TaskScopeStore,
|
|
231
242
|
migrate: () => unknown,
|
|
232
243
|
moduleRegistry: OperationRegistry,
|
|
233
244
|
authority: AuthorityRegistry,
|
|
@@ -237,7 +248,10 @@ function handlers(
|
|
|
237
248
|
// these six entries stay in this completeness-checked table only as a thin forward so
|
|
238
249
|
// `Record<OperationName, OperationHandler>` still guarantees every operation has an entry
|
|
239
250
|
// at compile time. The actual notes.* logic now lives in the module, not here.
|
|
240
|
-
const forwardToModule =
|
|
251
|
+
const forwardToModule =
|
|
252
|
+
(name: OperationName): OperationHandler =>
|
|
253
|
+
(input) =>
|
|
254
|
+
moduleRegistry.get(name)!.execute(input);
|
|
241
255
|
const eventContext = (input: OperationInput): TaskEventContext => ({
|
|
242
256
|
actor: optionalString(input, "actor"),
|
|
243
257
|
source: optionalString(input, "source"),
|
|
@@ -264,30 +278,41 @@ function handlers(
|
|
|
264
278
|
"system.migrate": () => migrate(),
|
|
265
279
|
"artifact.create": (input) => {
|
|
266
280
|
const normalized = normalizeCreateInput(input);
|
|
267
|
-
authority.requireArtifactAllowed(
|
|
281
|
+
authority.requireArtifactAllowed(
|
|
282
|
+
normalized.kind,
|
|
283
|
+
normalized.subtype ?? templateSubtype(artifacts, normalized.templateId),
|
|
284
|
+
"create",
|
|
285
|
+
GENERIC_CALLER,
|
|
286
|
+
);
|
|
268
287
|
authority.requireArtifactAllowed(normalized.kind, normalized.subtype, "create", GENERIC_CALLER);
|
|
269
288
|
if (normalized.kind !== "task") return artifacts.create(normalized);
|
|
270
|
-
return tasks.create(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
);
|
|
282
304
|
},
|
|
283
305
|
"artifact.query": (input) => artifacts.query(input),
|
|
284
|
-
"artifact.show": (input) =>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"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) }),
|
|
291
316
|
"artifact.restore": (input) => artifacts.restore(string(input, "id"), eventContext(input)),
|
|
292
317
|
"artifact.trash_status": (input) => artifacts.trashStatus(string(input, "id")),
|
|
293
318
|
"artifact.trash_list": () => artifacts.listTrash(),
|
|
@@ -310,7 +335,11 @@ function handlers(
|
|
|
310
335
|
genericWriter.checkLink({ from, relation, to });
|
|
311
336
|
let removed: boolean;
|
|
312
337
|
if (relation === "depends_on" && artifacts.get(from)?.kind === "task" && artifacts.get(to)?.kind === "task") {
|
|
313
|
-
const before =
|
|
338
|
+
const before =
|
|
339
|
+
tasks
|
|
340
|
+
.graph()
|
|
341
|
+
.nodes.find((node) => node.task.id === from)
|
|
342
|
+
?.dependencyIds.includes(to) ?? false;
|
|
314
343
|
tasks.undepend(from, to, eventContext(input));
|
|
315
344
|
removed = before;
|
|
316
345
|
} else {
|
|
@@ -318,29 +347,29 @@ function handlers(
|
|
|
318
347
|
}
|
|
319
348
|
return { removed };
|
|
320
349
|
},
|
|
321
|
-
"graph.tree": (input) =>
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
}),
|
|
326
356
|
"graph.status": (input) => genericWriter.setStatus(string(input, "id"), string(input, "status"), eventContext(input)),
|
|
327
|
-
"graph.history": (input) =>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
}),
|
|
336
367
|
"gates.run": (input) => {
|
|
337
368
|
const id = string(input, "id");
|
|
338
|
-
return artifacts.get(id)?.kind === "task"
|
|
339
|
-
? tasks.runGates(id, eventContextFor(input, "gates-api"))
|
|
340
|
-
: gates.runAsync(id);
|
|
369
|
+
return artifacts.get(id)?.kind === "task" ? tasks.runGates(id, eventContextFor(input, "gates-api")) : gates.runAsync(id);
|
|
341
370
|
},
|
|
342
|
-
"rules.injectable": (input) =>
|
|
343
|
-
.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 })),
|
|
344
373
|
"tasks.create": forwardToModule("tasks.create"),
|
|
345
374
|
"tasks.update": forwardToModule("tasks.update"),
|
|
346
375
|
"tasks.list": forwardToModule("tasks.list"),
|
|
@@ -453,7 +482,16 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
453
482
|
const sessionIdentity = new SessionIdentity(new SQLiteSessionIdentityStore(db));
|
|
454
483
|
const discussions = new Discussions(artifacts, new SQLiteDiscussionRoundStore(db));
|
|
455
484
|
const authority = createAuthorityRegistry();
|
|
456
|
-
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
|
+
});
|
|
457
495
|
const moduleRegistry = new OperationRegistry();
|
|
458
496
|
moduleRegistry.registerAll(notesOperations(notes));
|
|
459
497
|
moduleRegistry.registerAll(logsOperations(logs));
|
|
@@ -481,8 +519,12 @@ export function createPapyrusService(path: string): PapyrusService {
|
|
|
481
519
|
}
|
|
482
520
|
return handler(input);
|
|
483
521
|
},
|
|
484
|
-
checkpoint: () => {
|
|
485
|
-
|
|
522
|
+
checkpoint: () => {
|
|
523
|
+
db.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
|
524
|
+
},
|
|
525
|
+
optimize: () => {
|
|
526
|
+
db.exec("PRAGMA optimize");
|
|
527
|
+
},
|
|
486
528
|
reapStaleFocus: () => tasks.reapStaleFocus(),
|
|
487
529
|
purgeDueTrash: () => artifacts.purgeDueTrash(),
|
|
488
530
|
close: () => {
|
|
@@ -517,7 +559,10 @@ async function readOperationBody(request: Request): Promise<{ op?: unknown; inpu
|
|
|
517
559
|
}
|
|
518
560
|
const bytes = new Uint8Array(size);
|
|
519
561
|
let offset = 0;
|
|
520
|
-
for (const chunk of chunks) {
|
|
562
|
+
for (const chunk of chunks) {
|
|
563
|
+
bytes.set(chunk, offset);
|
|
564
|
+
offset += chunk.byteLength;
|
|
565
|
+
}
|
|
521
566
|
return JSON.parse(new TextDecoder().decode(bytes)) as { op?: unknown; input?: unknown };
|
|
522
567
|
}
|
|
523
568
|
|
|
@@ -531,9 +576,11 @@ export function createApp(deps: {
|
|
|
531
576
|
* (daemon.ts) wires this to a PushChannel; tests and other embedders can ignore it.
|
|
532
577
|
*/
|
|
533
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;
|
|
534
581
|
}): { fetch(request: Request): Promise<Response> } {
|
|
535
582
|
// Same Bearer token, daemon, and port as the rest of this API -- see ./vehicle/papyrus-vehicle.ts.
|
|
536
|
-
const vehicleApp = createVehicleHttpApp({ registry: deps.service.vehicle, token: deps.token });
|
|
583
|
+
const vehicleApp = createVehicleHttpApp({ registry: deps.service.vehicle, token: deps.token, logger: deps.logger });
|
|
537
584
|
return {
|
|
538
585
|
async fetch(request: Request): Promise<Response> {
|
|
539
586
|
if (request.headers.get("authorization") !== `Bearer ${deps.token}`) {
|
|
@@ -559,7 +606,14 @@ export function createApp(deps: {
|
|
|
559
606
|
deps.onOperationExecuted?.(body.op, input as OperationInput);
|
|
560
607
|
return json({ result });
|
|
561
608
|
} catch (error) {
|
|
562
|
-
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;
|
|
563
617
|
return json({ error: error instanceof Error ? error.message : String(error) }, { status });
|
|
564
618
|
}
|
|
565
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
|
}
|