@danypops/papyrus 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +16 -2
  2. package/extension/src/active-task-continuation.ts +6 -0
  3. package/extension/src/domain-tools.ts +108 -52
  4. package/extension/src/index.ts +90 -37
  5. package/extension/src/notes.ts +14 -1
  6. package/extension/src/task-focus-events.ts +57 -0
  7. package/extension/src/tasks.ts +51 -15
  8. package/extension/src/tool-rendering/artifact-card.ts +117 -0
  9. package/extension/src/tool-rendering/artifact-list.ts +179 -0
  10. package/extension/src/tool-rendering/index.ts +107 -0
  11. package/extension/src/tool-rendering/render-model.ts +406 -0
  12. package/package.json +4 -2
  13. package/src/adapters/in-memory-conversation-journal-store.ts +48 -0
  14. package/src/adapters/sqlite-artifact-scope-store.ts +36 -0
  15. package/src/adapters/sqlite-artifact-store.ts +20 -11
  16. package/src/adapters/sqlite-discourse-store.ts +325 -0
  17. package/src/adapters/sqlite-graph-projection-store.ts +41 -0
  18. package/src/adapters/sqlite-task-focus-store.ts +34 -15
  19. package/src/authority-registry.ts +115 -0
  20. package/src/cli.ts +904 -124
  21. package/src/constants.ts +38 -5
  22. package/src/conversation-journal-service.ts +87 -0
  23. package/src/db.ts +336 -8
  24. package/src/domain/artifact-event.ts +99 -0
  25. package/src/domain/conversation-journal.ts +168 -0
  26. package/src/domain/discourse-store.ts +142 -0
  27. package/src/domain/graph-projection.ts +74 -0
  28. package/src/domain/task-event.ts +4 -0
  29. package/src/domain-services.ts +133 -38
  30. package/src/graph-projection-service.ts +103 -0
  31. package/src/id-migration.ts +200 -0
  32. package/src/module-registry.ts +53 -0
  33. package/src/modules/docs.ts +77 -0
  34. package/src/modules/graph-projection.ts +82 -0
  35. package/src/modules/notes.ts +76 -0
  36. package/src/modules/rules.ts +81 -0
  37. package/src/modules/skills.ts +113 -0
  38. package/src/modules/tasks.ts +164 -0
  39. package/src/ops.ts +142 -15
  40. package/src/ports/artifact-scope-store.ts +20 -0
  41. package/src/ports/artifact-store.ts +10 -5
  42. package/src/ports/conversation-journal-store.ts +17 -0
  43. package/src/ports/graph-projection-store.ts +15 -0
  44. package/src/ports/task-focus-store.ts +62 -20
  45. package/src/service.ts +218 -223
  46. package/src/task-service.ts +70 -38
package/src/service.ts CHANGED
@@ -3,111 +3,68 @@ import { VERSION } from "./version.ts";
3
3
  import { migrateDb, openDb, schemaVersion } from "./db.ts";
4
4
  import { SQLiteArtifactStore } from "./adapters/sqlite-artifact-store.ts";
5
5
  import { SQLiteGateRunner } from "./adapters/sqlite-gate-runner.ts";
6
+ import { SQLiteDiscourseStore } from "./adapters/sqlite-discourse-store.ts";
7
+ import { SQLiteArtifactScopeStore } from "./adapters/sqlite-artifact-scope-store.ts";
8
+ import { SQLiteGraphProjectionStore } from "./adapters/sqlite-graph-projection-store.ts";
6
9
  import { SQLiteTaskFocusStore } from "./adapters/sqlite-task-focus-store.ts";
7
10
  import { SQLiteTaskEventStore } from "./adapters/sqlite-task-event-store.ts";
8
11
  import { SQLiteTaskScopeStore } from "./adapters/sqlite-task-scope-store.ts";
9
12
  import type { CreateArtifactInput } from "./domain/artifact.ts";
10
- import type { Checklist } from "./domain/checklist.ts";
11
- import type { TaskEventContext, TaskEventDirection } from "./domain/task-event.ts";
13
+ import { DISCOURSE_RELATIONS, isDiscourseSubtype } from "./domain/discourse-store.ts";
14
+ import { AuthorityRegistry, AuthorizedArtifactWriter, type AuthorityClaim } from "./authority-registry.ts";
15
+ import type { TaskEventContext } from "./domain/task-event.ts";
12
16
  import type { TaskViewMode } from "./domain/task-scope.ts";
13
17
  import type { ArtifactStore } from "./ports/artifact-store.ts";
14
18
  import type { GateRunner } from "./ports/gate-runner.ts";
15
19
  import type { TaskEventStore } from "./ports/task-event-store.ts";
16
20
  import type { TaskScopeStore } from "./ports/task-scope-store.ts";
17
- import { projectTaskExecution } from "./task-execution.ts";
18
21
  import { Tasks, type TaskStatus } from "./task-service.ts";
19
22
  import {
20
- createArtifactTemplate,
21
- createDocument,
22
- createRule,
23
- createSkill,
24
- linkDocument,
25
- gateTaskWithRule,
26
23
  instantiateTemplate,
27
- listDocuments,
28
- listRules,
29
24
  listInjectableRules,
30
- listSkills,
31
- previewRule,
32
- showDocument,
33
- showRule,
34
- showSkill,
35
- skillInvocation,
36
- transitionDocument,
37
- transitionRule,
38
- transitionSkill,
39
- type DocumentRelation,
40
25
  } from "./domain-services.ts";
41
- import { taskContext } from "./task-context.ts";
42
- import { instantiateSkillWorkflow } from "./skill-execution.ts";
43
- import { Notes, type NoteDisposition } from "./note-service.ts";
26
+ import { Notes, NOTE_SUBTYPE } from "./note-service.ts";
27
+ import { OperationRegistry } from "./module-registry.ts";
28
+ import { docsOperations, DOCS_OPERATION_NAMES } from "./modules/docs.ts";
29
+ import { graphProjectionOperations, GRAPH_PROJECTION_OPERATION_NAMES } from "./modules/graph-projection.ts";
30
+ import { notesOperations, NOTES_OPERATION_NAMES } from "./modules/notes.ts";
31
+ import { rulesOperations, RULES_OPERATION_NAMES } from "./modules/rules.ts";
32
+ import { skillsOperations, SKILLS_OPERATION_NAMES } from "./modules/skills.ts";
33
+ import { tasksOperations, TASKS_OPERATION_NAMES } from "./modules/tasks.ts";
44
34
 
35
+ /**
36
+ * Operations with no registered module: the generic, cross-cutting kernel surface
37
+ * (artifact create/query/show, graph link/unlink/tree/status/history, gates run --
38
+ * no domain owns creation/linking/traversal for every kind, the same way system.migrate
39
+ * has no owning module) and two permanent composition-root exceptions (rules.injectable
40
+ * needs tasks.active(); skills.instantiate branches into tasks.create()) -- see
41
+ * src/modules/rules.ts and src/modules/skills.ts's module comments. discourse.store's
42
+ * eventual home depends on the still-open Discourse projection-target decision, not on
43
+ * module extraction.
44
+ */
45
+ const COMPOSITION_ROOT_OPERATION_NAMES = [
46
+ "system.migrate", "discourse.store", "artifact.create", "artifact.query", "artifact.show",
47
+ "graph.link", "graph.unlink", "graph.tree", "graph.status", "graph.history", "gates.run",
48
+ "rules.injectable", "skills.instantiate",
49
+ ] as const;
50
+
51
+ /**
52
+ * Each registered module owns its own operation-name list (src/modules/*.ts); this is a
53
+ * spread of those plus the composition-root exceptions above, not a second hand-
54
+ * maintained copy. TypeScript needs this to stay a compile-time-known array (it derives
55
+ * OperationName, which powers Record<OperationName, OperationHandler>'s exhaustiveness
56
+ * check below) — it cannot be generated from moduleRegistry.list() (a runtime value)
57
+ * without losing that guarantee, so this composition of `as const` arrays is the
58
+ * furthest this can go while keeping that safety net.
59
+ */
45
60
  export const EXPECTED_OPERATION_NAMES = [
46
- "system.migrate",
47
- "artifact.create",
48
- "artifact.query",
49
- "artifact.show",
50
- "graph.link",
51
- "graph.tree",
52
- "graph.status",
53
- "gates.run",
54
- "rules.injectable",
55
- "tasks.create",
56
- "tasks.update",
57
- "tasks.list",
58
- "tasks.graph",
59
- "tasks.plan",
60
- "tasks.show",
61
- "tasks.history",
62
- "tasks.scope",
63
- "tasks.set_scope",
64
- "tasks.assign_project",
65
- "tasks.active",
66
- "tasks.focused",
67
- "tasks.focus",
68
- "tasks.pause",
69
- "tasks.unpause",
70
- "tasks.clear_focus",
71
- "tasks.start",
72
- "tasks.submit",
73
- "tasks.complete",
74
- "tasks.run_gates",
75
- "tasks.set_checklist",
76
- "tasks.context",
77
- "tasks.reject",
78
- "tasks.retry",
79
- "tasks.cancel",
80
- "tasks.depend",
81
- "tasks.contain",
82
- "docs.create",
83
- "docs.list",
84
- "docs.show",
85
- "docs.activate",
86
- "docs.archive",
87
- "docs.reopen",
88
- "docs.link",
89
- "notes.capture",
90
- "notes.list",
91
- "notes.show",
92
- "notes.consume",
93
- "notes.promote",
94
- "notes.archive",
95
- "rules.create",
96
- "rules.list",
97
- "rules.show",
98
- "rules.preview",
99
- "rules.enable",
100
- "rules.disable",
101
- "rules.gate",
102
- "skills.create",
103
- "skills.create_template",
104
- "skills.list",
105
- "skills.show",
106
- "skills.invoke",
107
- "skills.run",
108
- "skills.enable",
109
- "skills.disable",
110
- "skills.instantiate",
61
+ ...COMPOSITION_ROOT_OPERATION_NAMES,
62
+ ...TASKS_OPERATION_NAMES,
63
+ ...DOCS_OPERATION_NAMES,
64
+ ...NOTES_OPERATION_NAMES,
65
+ ...RULES_OPERATION_NAMES,
66
+ ...SKILLS_OPERATION_NAMES,
67
+ ...GRAPH_PROJECTION_OPERATION_NAMES,
111
68
  ] as const;
112
69
 
113
70
  export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
@@ -150,6 +107,57 @@ function normalizeCreateInput(input: OperationInput): CreateArtifactInput {
150
107
  return { ...rest, templateId: typeof template_id === "string" ? template_id : undefined } as CreateArtifactInput;
151
108
  }
152
109
 
110
+ function templateSubtype(artifacts: ArtifactStore, templateId: string | undefined): string | undefined {
111
+ if (!templateId) return undefined;
112
+ const defaults = artifacts.get(templateId)?.extra["defaults"];
113
+ if (typeof defaults !== "object" || defaults === null || Array.isArray(defaults)) return undefined;
114
+ const subtype = (defaults as Record<string, unknown>)["subtype"];
115
+ return typeof subtype === "string" ? subtype : undefined;
116
+ }
117
+
118
+ /**
119
+ * The one deep enforcement point (step 4 of reducing-papyrus-consumer-change-amplification-with-modules--pvdo)
120
+ * replacing the previously scattered isDiscourseSubtype/NOTE_SUBTYPE/task-kind checks that used
121
+ * to be re-implemented at every write call site. "generic" is the caller identity for the
122
+ * low-level artifact.create / graph.link / graph.unlink / graph.status surface, which owns
123
+ * nothing itself, so any claimed kind, subtype, or relation is rejected for it — exactly
124
+ * matching the historical behavior these checks replace.
125
+ */
126
+ const GENERIC_CALLER = "generic";
127
+
128
+ const discourseAuthorityClaim: AuthorityClaim = {
129
+ owner: "discourse",
130
+ matchesArtifact: (_kind, subtype) => isDiscourseSubtype(subtype),
131
+ matchesRelation: (relation) => DISCOURSE_RELATIONS.has(relation),
132
+ denyMessage: (action) => action === "link" ? "forum-owned Context Mesh links require discourse.store" : "forum-owned Context Mesh Docs require discourse.store",
133
+ };
134
+
135
+ const notesAuthorityClaim: AuthorityClaim = {
136
+ owner: "notes",
137
+ matchesArtifact: (kind, subtype) => kind === "doc" && subtype === NOTE_SUBTYPE,
138
+ denyMessage: (action) => {
139
+ if (action === "link") return "note relationships require a notes.* operation so disposition provenance is preserved";
140
+ if (action === "status") return "note lifecycle changes require a notes.* operation so disposition provenance is preserved";
141
+ return "note creation requires notes.capture";
142
+ },
143
+ };
144
+
145
+ // Only ever checked for the "status" action today (graph.status): artifact.create redirects
146
+ // kind="task" to tasks.create rather than rejecting, and graph.link/unlink never checked task
147
+ // ownership historically. appliesToAction scopes the claim so it cannot leak into those paths.
148
+ const tasksAuthorityClaim: AuthorityClaim = {
149
+ owner: "tasks",
150
+ matchesArtifact: (kind) => kind === "task",
151
+ appliesToAction: (action) => action === "status",
152
+ denyMessage: () => "task lifecycle changes require a tasks.* operation so history and review invariants are preserved",
153
+ };
154
+
155
+ function createAuthorityRegistry(): AuthorityRegistry {
156
+ const authority = new AuthorityRegistry();
157
+ authority.claimAll([discourseAuthorityClaim, notesAuthorityClaim, tasksAuthorityClaim]);
158
+ return authority;
159
+ }
160
+
153
161
  export interface SchemaState {
154
162
  current: number;
155
163
  required: number;
@@ -170,10 +178,19 @@ function handlers(
170
178
  gates: GateRunner,
171
179
  tasks: Tasks,
172
180
  notes: Notes,
181
+ discourse: SQLiteDiscourseStore,
173
182
  events: TaskEventStore,
174
183
  scopes: TaskScopeStore,
175
184
  migrate: () => unknown,
185
+ moduleRegistry: OperationRegistry,
186
+ authority: AuthorityRegistry,
176
187
  ): Record<OperationName, OperationHandler> {
188
+ const genericWriter = new AuthorizedArtifactWriter(artifacts, authority, GENERIC_CALLER);
189
+ // Notes is the first module extracted behind the OperationRegistry (src/modules/notes.ts);
190
+ // these six entries stay in this completeness-checked table only as a thin forward so
191
+ // `Record<OperationName, OperationHandler>` still guarantees every operation has an entry
192
+ // at compile time. The actual notes.* logic now lives in the module, not here.
193
+ const forwardToModule = (name: OperationName): OperationHandler => (input) => moduleRegistry.get(name)!.execute(input);
177
194
  const eventContext = (input: OperationInput): TaskEventContext => ({
178
195
  actor: optionalString(input, "actor"),
179
196
  source: optionalString(input, "source"),
@@ -194,12 +211,15 @@ function handlers(
194
211
  projectRoot: string(input, "project_root"),
195
212
  scope: optionalString(input, "scope") as TaskViewMode | undefined,
196
213
  rootTaskId: optionalString(input, "root_task_id"),
214
+ sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
197
215
  });
198
216
  return {
199
217
  "system.migrate": () => migrate(),
218
+ "discourse.store": (input) => discourse.execute(input),
200
219
  "artifact.create": (input) => {
201
220
  const normalized = normalizeCreateInput(input);
202
- if (normalized.kind === "doc" && normalized.subtype === "note") throw new Error("note creation requires notes.capture");
221
+ authority.requireArtifactAllowed(normalized.kind, normalized.subtype ?? templateSubtype(artifacts, normalized.templateId), "create", GENERIC_CALLER);
222
+ authority.requireArtifactAllowed(normalized.kind, normalized.subtype, "create", GENERIC_CALLER);
203
223
  if (normalized.kind !== "task") return artifacts.create(normalized);
204
224
  return tasks.create({
205
225
  id: normalized.id,
@@ -224,25 +244,44 @@ function handlers(
224
244
  const from = string(input, "from");
225
245
  const relation = string(input, "relation");
226
246
  const to = string(input, "to");
247
+ genericWriter.checkLink({ from, relation, to });
227
248
  if (relation === "depends_on" && artifacts.get(from)?.kind === "task" && artifacts.get(to)?.kind === "task") {
228
- tasks.depend(from, to);
249
+ tasks.depend(from, to, eventContext(input));
229
250
  } else {
230
- artifacts.link({ from, relation, to });
251
+ artifacts.link({ from, relation, to }, eventContext(input));
231
252
  }
232
253
  return { ok: true };
233
254
  },
255
+ "graph.unlink": (input) => {
256
+ const from = string(input, "from");
257
+ const relation = string(input, "relation");
258
+ const to = string(input, "to");
259
+ genericWriter.checkLink({ from, relation, to });
260
+ let removed: boolean;
261
+ if (relation === "depends_on" && artifacts.get(from)?.kind === "task" && artifacts.get(to)?.kind === "task") {
262
+ const before = tasks.graph().nodes.find((node) => node.task.id === from)?.dependencyIds.includes(to) ?? false;
263
+ tasks.undepend(from, to, eventContext(input));
264
+ removed = before;
265
+ } else {
266
+ removed = artifacts.unlink({ from, relation, to }, eventContext(input));
267
+ }
268
+ return { removed };
269
+ },
234
270
  "graph.tree": (input) => artifacts.get(string(input, "id"), {
235
271
  tree: true,
236
272
  depth: optionalNumber(input, "depth"),
237
273
  maxNodes: optionalNumber(input, "max_nodes") ?? optionalNumber(input, "maxNodes"),
238
274
  }),
239
- "graph.status": (input) => {
240
- const id = string(input, "id");
241
- const artifact = artifacts.get(id);
242
- if (artifact?.kind === "task") throw new Error("task lifecycle changes require a tasks.* operation so history and review invariants are preserved");
243
- if (artifact?.kind === "doc" && artifact.subtype === "note") throw new Error("note lifecycle changes require a notes.* operation so disposition provenance is preserved");
244
- return artifacts.setStatus(id, string(input, "status"));
245
- },
275
+ "graph.status": (input) => genericWriter.setStatus(string(input, "id"), string(input, "status"), eventContext(input)),
276
+ "graph.history": (input) => artifacts.events({
277
+ artifactId: optionalString(input, "id"),
278
+ actor: optionalString(input, "actor"),
279
+ sessionId: optionalString(input, "session_id") ?? optionalString(input, "sessionId"),
280
+ since: optionalString(input, "since"),
281
+ limit: optionalNumber(input, "limit"),
282
+ cursor: optionalNumber(input, "cursor"),
283
+ direction: optionalString(input, "direction") as "asc" | "desc" | undefined,
284
+ }),
246
285
  "gates.run": (input) => {
247
286
  const id = string(input, "id");
248
287
  return artifacts.get(id)?.kind === "task"
@@ -251,131 +290,74 @@ function handlers(
251
290
  },
252
291
  "rules.injectable": (input) => listInjectableRules(artifacts, tasks.active(taskFilter(input))?.id)
253
292
  .map(({ id, title, body, extra }) => ({ id, title, body, extra })),
254
- "tasks.create": (input) => tasks.create({
255
- title: string(input, "title"),
256
- body: optionalString(input, "body"),
257
- status: optionalString(input, "status") as TaskStatus | undefined,
258
- labels: input["labels"] as string[] | undefined,
259
- extra: input["extra"] as Record<string, unknown> | undefined,
260
- gates: input["gates"] as Parameters<Tasks["create"]>[0]["gates"],
261
- checklist: input["checklist"] as Checklist | undefined,
262
- templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
263
- parentId: optionalString(input, "parent_id") ?? optionalString(input, "parentId"),
264
- dependsOn: (input["depends_on"] ?? input["dependsOn"]) as string[] | undefined,
265
- projectRoot: string(input, "project_root"),
266
- projectSource: "cwd",
267
- }, eventContext(input)),
268
- "tasks.update": (input) => tasks.update(string(input, "id"), {
269
- ...(input["title"] !== undefined ? { title: optionalString(input, "title")! } : {}),
270
- ...(input["body"] !== undefined ? { body: optionalString(input, "body")! } : {}),
271
- ...(input["labels"] !== undefined ? { labels: optionalStringArray(input, "labels")! } : {}),
272
- ...(input["status"] !== undefined ? { status: string(input, "status") as "todo" } : {}),
273
- }, eventContext(input)),
274
- "tasks.list": (input) => tasks.list(taskFilter(input)),
275
- "tasks.graph": (input) => tasks.graph(taskFilter(input)),
276
- "tasks.plan": (input) => projectTaskExecution(tasks.graph(taskFilter(input))),
277
- "tasks.show": (input) => tasks.show(string(input, "id")),
278
- "tasks.history": (input) => tasks.history(string(input, "id"), {
279
- limit: optionalNumber(input, "limit"),
280
- cursor: optionalNumber(input, "cursor"),
281
- direction: optionalString(input, "direction") as TaskEventDirection | undefined,
282
- }),
283
- "tasks.scope": (input) => tasks.scopeSelection(string(input, "project_root")),
284
- "tasks.set_scope": (input) => tasks.setView(
285
- string(input, "project_root"),
286
- string(input, "scope") as TaskViewMode,
287
- optionalString(input, "root_task_id"),
288
- ),
289
- "tasks.assign_project": (input) => tasks.assignProject(
290
- string(input, "id"),
291
- string(input, "project_root"),
292
- eventContext(input),
293
- ),
294
- "tasks.active": (input) => tasks.active(taskFilter(input)),
295
- "tasks.focused": (input) => tasks.focused(taskFilter(input)),
296
- "tasks.focus": (input) => tasks.focus(string(input, "id"), eventContext(input)),
297
- "tasks.pause": (input) => tasks.pauseFocus(eventContext(input)),
298
- "tasks.unpause": (input) => tasks.unpauseFocus(eventContext(input)),
299
- "tasks.clear_focus": (input) => tasks.clearFocus(eventContext(input)),
300
- "tasks.start": (input) => tasks.transition(string(input, "id"), "start", eventContext(input)),
301
- "tasks.submit": (input) => tasks.transition(string(input, "id"), "submit", eventContext(input)),
302
- "tasks.complete": (input) => tasks.completeAsync(string(input, "id"), eventContext(input)),
303
- "tasks.run_gates": (input) => tasks.runGates(string(input, "id"), eventContext(input)),
304
- "tasks.set_checklist": (input) => tasks.setChecklist(string(input, "id"), input["checklist"] as Checklist),
305
- "tasks.context": (input) => taskContext(artifacts, tasks.active()?.id, new Set(tasks.list(taskFilter(input)).map((task) => task.id))),
306
- "tasks.reject": (input) => tasks.transition(string(input, "id"), "reject", eventContext(input)),
307
- "tasks.retry": (input) => tasks.transition(string(input, "id"), "retry", eventContext(input)),
308
- "tasks.cancel": (input) => tasks.transition(string(input, "id"), "cancel", eventContext(input)),
309
- "tasks.depend": (input) => tasks.depend(string(input, "id"), string(input, "dependency_id")),
310
- "tasks.contain": (input) => tasks.contain(string(input, "parent_id"), string(input, "child_id")),
311
- "docs.create": (input) => createDocument(artifacts, {
312
- title: string(input, "title"), body: optionalString(input, "body"), subtype: optionalString(input, "subtype"),
313
- labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
314
- templateId: optionalString(input, "template_id") ?? optionalString(input, "templateId"),
315
- }),
316
- "docs.list": (input) => listDocuments(artifacts, artifactFilter(input)),
317
- "docs.show": (input) => showDocument(artifacts, string(input, "id")),
318
- "docs.activate": (input) => transitionDocument(artifacts, string(input, "id"), "activate"),
319
- "docs.archive": (input) => transitionDocument(artifacts, string(input, "id"), "archive"),
320
- "docs.reopen": (input) => transitionDocument(artifacts, string(input, "id"), "reopen"),
321
- "docs.link": (input) => linkDocument(artifacts, string(input, "id"), string(input, "relation") as DocumentRelation, string(input, "target_id")),
322
- "notes.capture": (input) => notes.capture({
323
- body: string(input, "body"), title: optionalString(input, "title"), projectRoot: string(input, "project_root"),
324
- actor: optionalString(input, "actor"), source: optionalString(input, "source"), sessionId: optionalString(input, "session_id"),
325
- }),
326
- "notes.list": (input) => notes.list({
327
- projectRoot: string(input, "project_root"), status: optionalString(input, "status") as "draft" | "active" | "archived" | undefined,
328
- text: optionalString(input, "text"), limit: optionalNumber(input, "limit"),
329
- }),
330
- "notes.show": (input) => notes.show(string(input, "id"), string(input, "project_root")),
331
- "notes.consume": (input) => notes.consume(string(input, "id"), {
332
- projectRoot: string(input, "project_root"), actor: optionalString(input, "actor"), source: optionalString(input, "source"),
333
- sessionId: optionalString(input, "session_id"), reason: optionalString(input, "reason"),
334
- }),
335
- "notes.promote": (input) => notes.promote(string(input, "id"), string(input, "target_id"), {
336
- projectRoot: string(input, "project_root"), actor: optionalString(input, "actor"), source: optionalString(input, "source"),
337
- sessionId: optionalString(input, "session_id"), reason: optionalString(input, "reason"),
338
- }),
339
- "notes.archive": (input) => notes.archive(string(input, "id"), {
340
- projectRoot: string(input, "project_root"), disposition: string(input, "disposition") as NoteDisposition,
341
- actor: optionalString(input, "actor"), source: optionalString(input, "source"), sessionId: optionalString(input, "session_id"),
342
- reason: optionalString(input, "reason"),
343
- }),
344
- "rules.create": (input) => createRule(artifacts, {
345
- title: string(input, "title"), body: optionalString(input, "body"), condition: optionalString(input, "condition"),
346
- action: optionalString(input, "rule_action") ?? optionalString(input, "governance_action"),
347
- severity: optionalString(input, "severity") as "block" | "warn" | "info" | undefined,
348
- labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
349
- }),
350
- "rules.list": (input) => listRules(artifacts, artifactFilter(input)),
351
- "rules.show": (input) => showRule(artifacts, string(input, "id")),
352
- "rules.preview": (input) => previewRule(artifacts, string(input, "id")),
353
- "rules.enable": (input) => transitionRule(artifacts, string(input, "id"), "enable"),
354
- "rules.disable": (input) => transitionRule(artifacts, string(input, "id"), "disable"),
355
- "rules.gate": (input) => gateTaskWithRule(artifacts, string(input, "id"), string(input, "task_id")),
356
- "skills.create": (input) => createSkill(artifacts, {
357
- title: string(input, "title"), body: optionalString(input, "body"), trigger: optionalString(input, "trigger"),
358
- steps: input["steps"] as string[] | undefined, tools: input["tools"] as string[] | undefined,
359
- definition: input["definition"],
360
- labels: input["labels"] as string[] | undefined, extra: input["extra"] as Record<string, unknown> | undefined,
361
- }),
362
- "skills.create_template": (input) => createArtifactTemplate(artifacts, {
363
- title: string(input, "title"), targetKind: string(input, "target_kind"), defaults: input["defaults"] as Record<string, unknown> | undefined,
364
- required: input["required"] as string[] | undefined, body: optionalString(input, "body"), labels: input["labels"] as string[] | undefined,
365
- }),
366
- "skills.list": (input) => listSkills(artifacts, artifactFilter(input)),
367
- "skills.show": (input) => showSkill(artifacts, string(input, "id")),
368
- "skills.invoke": (input) => skillInvocation(artifacts, string(input, "id")),
369
- "skills.run": (input) => instantiateSkillWorkflow(artifacts, string(input, "id"), {
370
- runId: optionalString(input, "run_id") ?? optionalString(input, "runId"),
371
- arguments: input["arguments"] as Record<string, unknown> | undefined,
372
- }, { events, scopes, projectRoot: string(input, "project_root"), context: eventContextFor(input, "skill-run") }),
373
- "skills.enable": (input) => transitionSkill(artifacts, string(input, "id"), "enable"),
374
- "skills.disable": (input) => transitionSkill(artifacts, string(input, "id"), "disable"),
293
+ "tasks.create": forwardToModule("tasks.create"),
294
+ "tasks.update": forwardToModule("tasks.update"),
295
+ "tasks.list": forwardToModule("tasks.list"),
296
+ "tasks.graph": forwardToModule("tasks.graph"),
297
+ "tasks.plan": forwardToModule("tasks.plan"),
298
+ "tasks.show": forwardToModule("tasks.show"),
299
+ "tasks.history": forwardToModule("tasks.history"),
300
+ "tasks.scope": forwardToModule("tasks.scope"),
301
+ "tasks.set_scope": forwardToModule("tasks.set_scope"),
302
+ "tasks.assign_project": forwardToModule("tasks.assign_project"),
303
+ "tasks.active": forwardToModule("tasks.active"),
304
+ "tasks.focused": forwardToModule("tasks.focused"),
305
+ "tasks.focus": forwardToModule("tasks.focus"),
306
+ "tasks.pause": forwardToModule("tasks.pause"),
307
+ "tasks.unpause": forwardToModule("tasks.unpause"),
308
+ "tasks.clear_focus": forwardToModule("tasks.clear_focus"),
309
+ "tasks.start": forwardToModule("tasks.start"),
310
+ "tasks.submit": forwardToModule("tasks.submit"),
311
+ "tasks.complete": forwardToModule("tasks.complete"),
312
+ "tasks.run_gates": forwardToModule("tasks.run_gates"),
313
+ "tasks.set_checklist": forwardToModule("tasks.set_checklist"),
314
+ "tasks.context": forwardToModule("tasks.context"),
315
+ "tasks.reject": forwardToModule("tasks.reject"),
316
+ "tasks.retry": forwardToModule("tasks.retry"),
317
+ "tasks.cancel": forwardToModule("tasks.cancel"),
318
+ "tasks.depend": forwardToModule("tasks.depend"),
319
+ "tasks.undepend": forwardToModule("tasks.undepend"),
320
+ "tasks.contain": forwardToModule("tasks.contain"),
321
+ "tasks.uncontain": forwardToModule("tasks.uncontain"),
322
+ "docs.create": forwardToModule("docs.create"),
323
+ "docs.list": forwardToModule("docs.list"),
324
+ "docs.show": forwardToModule("docs.show"),
325
+ "docs.activate": forwardToModule("docs.activate"),
326
+ "docs.archive": forwardToModule("docs.archive"),
327
+ "docs.reopen": forwardToModule("docs.reopen"),
328
+ "docs.link": forwardToModule("docs.link"),
329
+ "docs.assign_project": forwardToModule("docs.assign_project"),
330
+ "notes.capture": forwardToModule("notes.capture"),
331
+ "notes.list": forwardToModule("notes.list"),
332
+ "notes.show": forwardToModule("notes.show"),
333
+ "notes.consume": forwardToModule("notes.consume"),
334
+ "notes.promote": forwardToModule("notes.promote"),
335
+ "notes.archive": forwardToModule("notes.archive"),
336
+ "rules.create": forwardToModule("rules.create"),
337
+ "rules.list": forwardToModule("rules.list"),
338
+ "rules.show": forwardToModule("rules.show"),
339
+ "rules.preview": forwardToModule("rules.preview"),
340
+ "rules.enable": forwardToModule("rules.enable"),
341
+ "rules.disable": forwardToModule("rules.disable"),
342
+ "rules.gate": forwardToModule("rules.gate"),
343
+ "rules.assign_project": forwardToModule("rules.assign_project"),
344
+ "skills.create": forwardToModule("skills.create"),
345
+ "skills.create_template": forwardToModule("skills.create_template"),
346
+ "skills.list": forwardToModule("skills.list"),
347
+ "skills.show": forwardToModule("skills.show"),
348
+ "skills.invoke": forwardToModule("skills.invoke"),
349
+ "skills.run": forwardToModule("skills.run"),
350
+ "skills.enable": forwardToModule("skills.enable"),
351
+ "skills.disable": forwardToModule("skills.disable"),
352
+ "skills.assign_project": forwardToModule("skills.assign_project"),
375
353
  "skills.instantiate": (input) => {
376
354
  const templateId = string(input, "template_id");
377
355
  const template = artifacts.get(templateId);
378
- if (template?.extra["targetKind"] !== "task") return instantiateTemplate(artifacts, templateId, normalizeCreateInput(input));
356
+ // Deliberately pass an unresolved kind so only the discourse claim (kind-agnostic) can match here —
357
+ // the historical requireDiscourseStoreForSubtype never checked notes at this call site; that check
358
+ // already happens inside instantiateTemplate's own rejectsNoteTemplate for the non-task branch below.
359
+ authority.requireArtifactAllowed(undefined, templateSubtype(artifacts, templateId), "create", GENERIC_CALLER);
360
+ if (template?.extra["targetKind"] !== "task") return instantiateTemplate(artifacts, templateId, normalizeCreateInput(input), authority, eventContext(input));
379
361
  return tasks.create({
380
362
  title: optionalString(input, "title") as string,
381
363
  body: optionalString(input, "body"),
@@ -387,6 +369,8 @@ function handlers(
387
369
  projectSource: "cwd",
388
370
  }, eventContextFor(input, "template-instantiation"));
389
371
  },
372
+ "graph_projection.apply": forwardToModule("graph_projection.apply"),
373
+ "graph_projection.checkpoint": forwardToModule("graph_projection.checkpoint"),
390
374
  };
391
375
  }
392
376
 
@@ -399,7 +383,18 @@ export function createPapyrusService(path: string): PapyrusService {
399
383
  const scopes = new SQLiteTaskScopeStore(db);
400
384
  const tasks = new Tasks(artifacts, gates, focus, events, scopes);
401
385
  const notes = new Notes(artifacts);
402
- const registry = handlers(artifacts, gates, tasks, notes, events, scopes, () => migrateDb(db));
386
+ const discourse = new SQLiteDiscourseStore(db, artifacts);
387
+ const projections = new SQLiteGraphProjectionStore(db);
388
+ const artifactScopes = new SQLiteArtifactScopeStore(db);
389
+ const authority = createAuthorityRegistry();
390
+ const moduleRegistry = new OperationRegistry();
391
+ moduleRegistry.registerAll(notesOperations(notes));
392
+ moduleRegistry.registerAll(tasksOperations(tasks, artifacts));
393
+ moduleRegistry.registerAll(docsOperations(artifacts, artifactScopes, authority));
394
+ moduleRegistry.registerAll(rulesOperations(artifacts, artifactScopes));
395
+ moduleRegistry.registerAll(skillsOperations({ artifacts, events, scopes, artifactScopes, authority }));
396
+ moduleRegistry.registerAll(graphProjectionOperations(artifacts, projections, authority));
397
+ const registry = handlers(artifacts, gates, tasks, notes, discourse, events, scopes, () => migrateDb(db), moduleRegistry, authority);
403
398
  const state = (): SchemaState => {
404
399
  const current = schemaVersion(db);
405
400
  return { current, required: SQLITE_SCHEMA_VERSION, migrationRequired: current !== SQLITE_SCHEMA_VERSION };
@@ -411,7 +406,7 @@ export function createPapyrusService(path: string): PapyrusService {
411
406
  const handler = registry[operation as OperationName];
412
407
  if (!handler) throw new UnknownOperationError(`unknown operation "${operation}"`);
413
408
  if (operation !== "system.migrate" && state().migrationRequired) {
414
- throw new MigrationRequiredError("database migration required; run `papyrus migrate task-focus`");
409
+ throw new MigrationRequiredError("database migration required; run `papyrus migrate schema`");
415
410
  }
416
411
  return handler(input);
417
412
  },