@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.
Files changed (73) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  3. package/src/adapters/sqlite-artifact-store.ts +7 -5
  4. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  5. package/src/adapters/sqlite-gate-runner.ts +1 -1
  6. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  7. package/src/adapters/sqlite-log-store.ts +36 -17
  8. package/src/adapters/sqlite-note-event-store.ts +20 -16
  9. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  10. package/src/adapters/sqlite-task-event-store.ts +29 -21
  11. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  12. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  13. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  14. package/src/artifact-relationship-view.ts +3 -3
  15. package/src/artifact-subtree.ts +4 -2
  16. package/src/authority-registry.ts +2 -1
  17. package/src/cli.ts +785 -179
  18. package/src/client.ts +46 -20
  19. package/src/constants.ts +15 -4
  20. package/src/daemon-state.ts +4 -12
  21. package/src/daemon.ts +31 -9
  22. package/src/db.ts +119 -98
  23. package/src/discussion-service.ts +109 -44
  24. package/src/domain/artifact-event.ts +17 -4
  25. package/src/domain/artifact.ts +3 -1
  26. package/src/domain/blueprint-definition.ts +26 -31
  27. package/src/domain/checklist.ts +20 -17
  28. package/src/domain/discussion.ts +37 -18
  29. package/src/domain/gate.ts +7 -7
  30. package/src/domain/log-entry.ts +1 -1
  31. package/src/domain/note-event.ts +20 -7
  32. package/src/domain/task-event.ts +17 -7
  33. package/src/domain-services.ts +241 -110
  34. package/src/graph-projection-service.ts +34 -8
  35. package/src/id-migration.ts +17 -4
  36. package/src/index.ts +16 -11
  37. package/src/log-service.ts +6 -5
  38. package/src/log.ts +19 -0
  39. package/src/modules/discuss.ts +63 -28
  40. package/src/modules/docs.ts +74 -17
  41. package/src/modules/graph-projection.ts +20 -9
  42. package/src/modules/logs.ts +33 -21
  43. package/src/modules/notes.ts +66 -28
  44. package/src/modules/playbooks.ts +88 -29
  45. package/src/modules/rules.ts +57 -15
  46. package/src/modules/session-identity.ts +6 -2
  47. package/src/modules/tasks.ts +142 -67
  48. package/src/note-service.ts +11 -7
  49. package/src/ops.ts +131 -68
  50. package/src/playbook-definition.ts +56 -17
  51. package/src/playbook-execution.ts +13 -3
  52. package/src/ports/note-event-store.ts +6 -4
  53. package/src/ports/task-event-store.ts +9 -6
  54. package/src/ports/task-focus-store.ts +17 -4
  55. package/src/ports/task-lease-store.ts +9 -4
  56. package/src/ports/task-scope-store.ts +3 -1
  57. package/src/service.ts +143 -89
  58. package/src/session-identity-service.ts +10 -2
  59. package/src/task-context.ts +28 -16
  60. package/src/task-execution.ts +4 -12
  61. package/src/task-graph-view.ts +12 -12
  62. package/src/task-relationship-view.ts +1 -3
  63. package/src/task-service.ts +167 -72
  64. package/src/vehicle/artifact-trash-vehicle.ts +25 -13
  65. package/src/vehicle/artifact-vehicle-shared.ts +28 -7
  66. package/src/vehicle/docs-vehicle.ts +48 -16
  67. package/src/vehicle/notes-vehicle.ts +24 -6
  68. package/src/vehicle/papyrus-vehicle.ts +14 -3
  69. package/src/vehicle/playbooks-vehicle.ts +87 -18
  70. package/src/vehicle/rules-vehicle.ts +58 -21
  71. package/src/vehicle/tasks-vehicle.ts +366 -54
  72. package/src/version.ts +1 -1
  73. 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 { return this.state.get(normalizeFocusScope(scope)); }
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 = { ...current, status: "paused", updatedAt: new Date().toISOString(), ...(reason ? { pauseReason: reason } : {}) };
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) { this.state.delete(key); removed++; }
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) { oldestKey = key; oldestAt = focus.updatedAt; }
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, type TaskLease } from "../domain/task-lease.ts";
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) throw new Error(`lease for task "${taskId}" is held by a different owner/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) throw new Error(`lease for task "${taskId}" is held by a different owner/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) { this.leases.delete(taskId); removed++; }
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 { return this.scopes.get(taskId); }
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 { SERVICE_MAX_BODY_BYTES, SQLITE_SCHEMA_VERSION } from "./constants.ts";
2
- import { VERSION } from "./version.ts";
3
- import { migrateDb, openDb, schemaVersion } from "./db.ts";
4
- import { removeArtifactSubtree } from "./artifact-subtree.ts";
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 { SQLiteSessionIdentityStore } from "./adapters/sqlite-session-identity-store.ts";
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 type { ArtifactStore } from "./ports/artifact-store.ts";
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 { Tasks, type TaskStatus } from "./task-service.ts";
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 type { VehicleRegistry } from "@danypops/vehicle-server";
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", "artifact.create", "artifact.query", "artifact.show",
61
- "artifact.remove", "artifact.remove_subtree", "artifact.restore", "artifact.trash_status", "artifact.trash_list",
62
- "graph.link", "graph.unlink", "graph.tree", "graph.status", "graph.history", "gates.run",
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 optionalStringArray(input: OperationInput, key: string): string[] | undefined {
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["defaults"];
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>)["subtype"];
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
- notes: Notes,
229
- events: TaskEventStore,
230
- scopes: TaskScopeStore,
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 = (name: OperationName): OperationHandler => (input) => moduleRegistry.get(name)!.execute(input);
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(normalized.kind, normalized.subtype ?? templateSubtype(artifacts, normalized.templateId), "create", GENERIC_CALLER);
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
- id: normalized.id,
272
- title: string(input, "title"),
273
- body: normalized.body,
274
- subtype: normalized.subtype,
275
- status: normalized.status as TaskStatus | undefined,
276
- labels: normalized.labels,
277
- extra: normalized.extra,
278
- templateId: normalized.templateId,
279
- projectRoot: string(input, "project_root"),
280
- projectSource: "cwd",
281
- }, eventContextFor(input, "artifact-api"));
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) => artifacts.get(string(input, "id"), {
285
- tree: input["tree"] === true,
286
- depth: optionalNumber(input, "depth"),
287
- maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
288
- }),
289
- "artifact.remove": (input) => artifacts.trash(string(input, "id"), { reason: optionalString(input, "reason"), context: eventContext(input) }),
290
- "artifact.remove_subtree": (input) => removeArtifactSubtree(artifacts, string(input, "id"), { reason: optionalString(input, "reason"), context: eventContext(input) }),
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 = tasks.graph().nodes.find((node) => node.task.id === from)?.dependencyIds.includes(to) ?? false;
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) => artifacts.get(string(input, "id"), {
322
- tree: true,
323
- depth: optionalNumber(input, "depth"),
324
- maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
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) => artifacts.events({
328
- artifactId: optionalString(input, "id"),
329
- actor: optionalString(input, "actor"),
330
- sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
331
- since: optionalString(input, "since"),
332
- limit: optionalNumber(input, "limit"),
333
- cursor: optionalNumber(input, "cursor"),
334
- direction: optionalString(input, "direction") as "asc" | "desc" | undefined,
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) => listInjectableRules(artifacts, tasks.active(taskFilter(input))?.id)
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({ artifacts, scopes: artifactScopes, authority, notes, events, taskScopes: scopes, tasks, sessionIdentity });
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: () => { db.exec("PRAGMA wal_checkpoint(PASSIVE)"); },
485
- optimize: () => { db.exec("PRAGMA optimize"); },
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) { bytes.set(chunk, offset); offset += chunk.byteLength; }
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 = error instanceof PayloadTooLargeError ? 413 : error instanceof UnknownOperationError ? 404 : error instanceof InvalidSessionSecretError ? 403 : 400;
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 { isSessionRegistered, registerSessionIdentity, releaseSessionIdentity, verifySessionSecret } from "@danypops/vehicle-server/session-identity";
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)) throw new InvalidSessionSecretError(`session "${sessionId}" is registered; a valid session_secret is required to mutate its Task Focus`);
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
  }
@@ -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["gates"];
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(artifacts: ArtifactStore, activeTaskId: string | undefined, taskIds: Set<string> | undefined): string[] {
49
- const discussions = artifacts.query({ kind: "task", subtype: DISCUSSION_SUBTYPE })
50
- .filter((discussion) => {
51
- try { return readDiscussionExtra(discussion.extra).state === "deferred"; } catch { return false; }
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.relationships({ artifactIds: discussions.map((discussion) => discussion.id) })
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(artifacts: ArtifactStore, activeTaskId?: string, taskIds?: Set<string>, verbosity: TaskContextVerbosity = "full"): string | null {
80
- const tasks = artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE })
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 ? [active] : open.filter((task) => task.status === "in-progress" || task.status === "review").slice(0, TASK_CONTEXT_CURRENT_LIMIT);
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`] : [];
@@ -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((left, right) =>
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[]]));
@@ -36,18 +36,18 @@ export function projectTaskGraph(graph: TaskGraph, view: TaskGraphView): Display
36
36
  }
37
37
  }
38
38
 
39
- const connected = view === "execution"
40
- ? new Set(graph.nodes.map((node) => node.task.id))
41
- : new Set(edges.flatMap((edge) => [edge.from, edge.to]));
42
- const nodes = 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 }));
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
  }