@danypops/papyrus 0.55.0 → 0.57.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 (110) hide show
  1. package/package.json +14 -1
  2. package/src/artifact/artifact-relationship-view.ts +1 -1
  3. package/src/artifact/artifact-scope-store.ts +32 -26
  4. package/src/artifact/in-memory-artifact-scope-store.ts +82 -43
  5. package/src/artifact/index.ts +3 -0
  6. package/src/artifact/sqlite-artifact-scope-store.ts +105 -56
  7. package/src/cli/docs-command.ts +71 -0
  8. package/src/cli/gates-command.ts +1 -1
  9. package/src/cli/note-command.ts +1 -1
  10. package/src/cli/playbooks-command.ts +71 -0
  11. package/src/cli/rules-command.ts +71 -0
  12. package/src/cli/scope-groups-command.ts +197 -0
  13. package/src/cli/task-command.ts +1 -1
  14. package/src/cli.ts +7 -0
  15. package/src/constants.ts +10 -2
  16. package/src/db.ts +117 -7
  17. package/src/{stores → discussion}/discussion-round-store.ts +1 -1
  18. package/src/discussion/discussion-service.ts +2 -2
  19. package/src/discussion/index.ts +1 -0
  20. package/src/{stores → discussion}/sqlite-discussion-round-store.ts +2 -2
  21. package/src/display-graph/graph-renderer.ts +5 -0
  22. package/src/docs/docs-service.ts +55 -20
  23. package/src/domain-service-shared.ts +90 -2
  24. package/src/{domain → gate}/gate-execution.ts +1 -1
  25. package/src/{stores → gate}/gate-runner.ts +1 -1
  26. package/src/{stores → gate}/sqlite-gate-runner.ts +3 -3
  27. package/src/graph-projection/graph-projection-service.ts +2 -2
  28. package/src/{stores → graph-projection}/graph-projection-store.ts +1 -1
  29. package/src/{stores → graph-projection}/sqlite-graph-projection-store.ts +2 -2
  30. package/src/handlers/discuss.ts +1 -1
  31. package/src/handlers/docs.ts +57 -2
  32. package/src/handlers/playbooks.ts +49 -8
  33. package/src/handlers/projects.ts +1 -1
  34. package/src/handlers/registry.ts +10 -5
  35. package/src/handlers/rules.ts +66 -2
  36. package/src/handlers/scope-groups.ts +100 -0
  37. package/src/handlers/task-classifiers.ts +1 -1
  38. package/src/handlers/tasks.ts +4 -4
  39. package/src/index.ts +9 -9
  40. package/src/log/log-service.ts +2 -2
  41. package/src/{stores → log}/log-store.ts +1 -1
  42. package/src/{stores → log}/sqlite-log-store.ts +2 -2
  43. package/src/modules/docs.ts +21 -1
  44. package/src/modules/graph-projection.ts +2 -2
  45. package/src/modules/logs.ts +1 -1
  46. package/src/modules/notes.ts +1 -1
  47. package/src/modules/playbooks.ts +24 -3
  48. package/src/modules/projects.ts +3 -3
  49. package/src/modules/rules.ts +21 -1
  50. package/src/modules/scope-groups.ts +92 -0
  51. package/src/modules/tasks.ts +3 -3
  52. package/src/note/index.ts +1 -0
  53. package/src/{stores → note}/note-event-store.ts +1 -1
  54. package/src/note/note-service.ts +2 -2
  55. package/src/{stores → note}/sqlite-note-event-store.ts +2 -2
  56. package/src/ops.ts +1 -1
  57. package/src/playbook/index.ts +2 -0
  58. package/src/playbook/playbook-definition.ts +2 -2
  59. package/src/playbook/playbook-execution.ts +1 -1
  60. package/src/playbook/playbook-service.ts +61 -26
  61. package/src/playbook/workflow-execution.ts +8 -8
  62. package/src/{stores → project-registry}/in-memory-project-registry-store.ts +2 -2
  63. package/src/{ports → project-registry}/project-registry-store.ts +1 -1
  64. package/src/{stores → project-registry}/sqlite-project-registry-store.ts +3 -3
  65. package/src/rules/rules-service.ts +56 -24
  66. package/src/scope-group/delete-scope-group.ts +17 -0
  67. package/src/scope-group/in-memory-scope-group-store.ts +173 -0
  68. package/src/scope-group/scope-group-store.ts +28 -0
  69. package/src/scope-group/scope-group.ts +79 -0
  70. package/src/scope-group/sqlite-scope-group-store.ts +223 -0
  71. package/src/service.ts +46 -21
  72. package/src/session-identity/session-identity-service.ts +2 -2
  73. package/src/{stores → session-identity}/sqlite-session-identity-store.ts +1 -1
  74. package/src/task/index.ts +13 -0
  75. package/src/task/task-context.ts +1 -1
  76. package/src/task/task-edges.ts +2 -2
  77. package/src/task/task-focus-coordinator.ts +3 -3
  78. package/src/task/task-graph-view.ts +1 -1
  79. package/src/task/task-lease-coordinator.ts +2 -2
  80. package/src/task/task-mutation-coordinator.ts +1 -1
  81. package/src/task/task-project-scope.ts +5 -5
  82. package/src/task/task-relationship-view.ts +1 -1
  83. package/src/task/task-service.ts +24 -24
  84. package/src/{stores → task-create-request}/sqlite-task-create-request-store.ts +1 -1
  85. package/src/{stores → task-event}/sqlite-task-event-store.ts +2 -2
  86. package/src/{stores → task-event}/task-event-store.ts +1 -1
  87. package/src/{stores → task-focus}/sqlite-task-focus-store.ts +1 -1
  88. package/src/{stores → task-lease}/sqlite-task-lease-store.ts +2 -2
  89. package/src/{stores → task-lease}/task-lease-store.ts +1 -1
  90. package/src/{stores → task-mutation-request}/sqlite-task-mutation-request-store.ts +5 -1
  91. package/src/{stores → task-scope}/sqlite-task-scope-store.ts +3 -3
  92. package/src/{stores → task-scope}/task-scope-store.ts +3 -3
  93. package/src/{domain → task-scope}/task-scope.ts +1 -1
  94. package/src/stores/graph-renderer.ts +0 -5
  95. /package/src/{domain → discussion}/discussion.ts +0 -0
  96. /package/src/{domain → display-graph}/display-graph.ts +0 -0
  97. /package/src/{domain → gate}/gate.ts +0 -0
  98. /package/src/{domain → graph-projection}/graph-projection.ts +0 -0
  99. /package/src/{domain → log}/log-entry.ts +0 -0
  100. /package/src/{domain → note}/note-event.ts +0 -0
  101. /package/src/{domain → playbook}/blueprint-definition.ts +0 -0
  102. /package/src/{domain → project-registry}/project-registry.ts +0 -0
  103. /package/src/{stores → session-identity}/session-identity-store.ts +0 -0
  104. /package/src/{domain → session-identity}/session-identity.ts +0 -0
  105. /package/src/{domain → task}/checklist.ts +0 -0
  106. /package/src/{stores → task-create-request}/task-create-request-store.ts +0 -0
  107. /package/src/{domain → task-event}/task-event.ts +0 -0
  108. /package/src/{stores → task-focus}/task-focus-store.ts +0 -0
  109. /package/src/{domain → task-lease}/task-lease.ts +0 -0
  110. /package/src/{stores → task-mutation-request}/task-mutation-request-store.ts +0 -0
@@ -210,6 +210,73 @@ const replaceProjectsCommand = buildCommand({
210
210
  docs: { brief: "Replace a Doc's entire project membership" },
211
211
  });
212
212
 
213
+ const setNoneCommand = buildCommand({
214
+ func: async function (this: DocsContext, _flags: Record<string, never>, id: string) {
215
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("docs.set_none", { id });
216
+ render.call(this, scope, renderScope(scope));
217
+ },
218
+ parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Document id", parse: String, placeholder: "id" }] } },
219
+ docs: { brief: "Fully hide a Doc -- never applicable, never context-injected, regardless of project" },
220
+ });
221
+
222
+ const addGroupCommand = buildCommand({
223
+ func: async function (this: DocsContext, _flags: Record<string, never>, id: string, group: string) {
224
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("docs.add_group", { id, group });
225
+ render.call(this, scope, renderScope(scope));
226
+ },
227
+ parameters: {
228
+ flags: {},
229
+ positional: {
230
+ kind: "tuple",
231
+ parameters: [
232
+ { brief: "Document id", parse: String, placeholder: "id" },
233
+ { brief: "Scope group id/name/alias to add", parse: String, placeholder: "group" },
234
+ ],
235
+ },
236
+ },
237
+ docs: { brief: "Add one scope group to a Doc's explicit scope" },
238
+ });
239
+
240
+ const removeGroupCommand = buildCommand({
241
+ func: async function (this: DocsContext, _flags: Record<string, never>, id: string, group: string) {
242
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("docs.remove_group", { id, group });
243
+ render.call(this, scope, renderScope(scope));
244
+ },
245
+ parameters: {
246
+ flags: {},
247
+ positional: {
248
+ kind: "tuple",
249
+ parameters: [
250
+ { brief: "Document id", parse: String, placeholder: "id" },
251
+ { brief: "Scope group id/name/alias to remove", parse: String, placeholder: "group" },
252
+ ],
253
+ },
254
+ },
255
+ docs: { brief: "Remove one scope group from a Doc's explicit scope" },
256
+ });
257
+
258
+ const replaceGroupsCommand = buildCommand({
259
+ func: async function (this: DocsContext, flags: { groupsJson: string[] }, id: string) {
260
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("docs.replace_groups", {
261
+ id,
262
+ groups: flags.groupsJson,
263
+ });
264
+ render.call(this, scope, renderScope(scope));
265
+ },
266
+ parameters: {
267
+ flags: {
268
+ groupsJson: {
269
+ brief: "JSON string array of scope group id/name/alias references",
270
+ kind: "parsed",
271
+ parse: parseStringArray,
272
+ placeholder: "json",
273
+ },
274
+ },
275
+ positional: { kind: "tuple", parameters: [{ brief: "Document id", parse: String, placeholder: "id" }] },
276
+ },
277
+ docs: { brief: "Replace a Doc's entire scope-group membership" },
278
+ });
279
+
213
280
  function buildStatusTransitionCommand(action: "activate" | "archive" | "reopen") {
214
281
  return buildCommand({
215
282
  func: async function (this: DocsContext, _flags: Record<string, never>, id: string) {
@@ -271,9 +338,13 @@ const app = buildApplication(
271
338
  "assign-project": assignProjectCommand,
272
339
  scope: scopeCommand,
273
340
  "set-global": setGlobalCommand,
341
+ "set-none": setNoneCommand,
274
342
  "add-project": addProjectCommand,
275
343
  "remove-project": removeProjectCommand,
276
344
  "replace-projects": replaceProjectsCommand,
345
+ "add-group": addGroupCommand,
346
+ "remove-group": removeGroupCommand,
347
+ "replace-groups": replaceGroupsCommand,
277
348
  show: showCommand,
278
349
  activate: buildStatusTransitionCommand("activate"),
279
350
  archive: buildStatusTransitionCommand("archive"),
@@ -1,7 +1,7 @@
1
1
  import type { CommandContext } from "@stricli/core";
2
2
  import { buildApplication, buildCommand, buildRouteMap } from "@stricli/core";
3
3
  import type { PapyrusClient } from "../client.ts";
4
- import type { GateResult } from "../domain/gate.ts";
4
+ import type { GateResult } from "../gate/gate.ts";
5
5
  import { runStricliToString } from "./stricli-run.ts";
6
6
 
7
7
  type GatesClient = Pick<PapyrusClient, "call">;
@@ -1,7 +1,7 @@
1
1
  import type { CommandContext } from "@stricli/core";
2
2
  import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
3
3
  import type { PapyrusClient } from "../client.ts";
4
- import type { NoteHistoryPage } from "../domain/note-event.ts";
4
+ import type { NoteHistoryPage } from "../note/note-event.ts";
5
5
  import { artifactLabel, type CliArtifact } from "./shared.ts";
6
6
  import { runStricliToString } from "./stricli-run.ts";
7
7
 
@@ -318,6 +318,73 @@ const replaceProjectsCommand = buildCommand({
318
318
  docs: { brief: "Replace a Playbook's entire project membership" },
319
319
  });
320
320
 
321
+ const setNoneCommand = buildCommand({
322
+ func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string) {
323
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("playbooks.set_none", { id });
324
+ render.call(this, scope, renderScope(scope));
325
+ },
326
+ parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] } },
327
+ docs: { brief: "Fully hide a Playbook -- never applicable, never injected, regardless of project" },
328
+ });
329
+
330
+ const addGroupCommand = buildCommand({
331
+ func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string, group: string) {
332
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("playbooks.add_group", { id, group });
333
+ render.call(this, scope, renderScope(scope));
334
+ },
335
+ parameters: {
336
+ flags: {},
337
+ positional: {
338
+ kind: "tuple",
339
+ parameters: [
340
+ { brief: "Playbook id", parse: String, placeholder: "id" },
341
+ { brief: "Scope group id/name/alias to add", parse: String, placeholder: "group" },
342
+ ],
343
+ },
344
+ },
345
+ docs: { brief: "Add one scope group to a Playbook's explicit scope" },
346
+ });
347
+
348
+ const removeGroupCommand = buildCommand({
349
+ func: async function (this: PlaybooksContext, _flags: Record<string, never>, id: string, group: string) {
350
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("playbooks.remove_group", { id, group });
351
+ render.call(this, scope, renderScope(scope));
352
+ },
353
+ parameters: {
354
+ flags: {},
355
+ positional: {
356
+ kind: "tuple",
357
+ parameters: [
358
+ { brief: "Playbook id", parse: String, placeholder: "id" },
359
+ { brief: "Scope group id/name/alias to remove", parse: String, placeholder: "group" },
360
+ ],
361
+ },
362
+ },
363
+ docs: { brief: "Remove one scope group from a Playbook's explicit scope" },
364
+ });
365
+
366
+ const replaceGroupsCommand = buildCommand({
367
+ func: async function (this: PlaybooksContext, flags: { groupsJson: string[] }, id: string) {
368
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("playbooks.replace_groups", {
369
+ id,
370
+ groups: flags.groupsJson,
371
+ });
372
+ render.call(this, scope, renderScope(scope));
373
+ },
374
+ parameters: {
375
+ flags: {
376
+ groupsJson: {
377
+ brief: "JSON string array of scope group id/name/alias references",
378
+ kind: "parsed",
379
+ parse: parseStringArray,
380
+ placeholder: "json",
381
+ },
382
+ },
383
+ positional: { kind: "tuple", parameters: [{ brief: "Playbook id", parse: String, placeholder: "id" }] },
384
+ },
385
+ docs: { brief: "Replace a Playbook's entire scope-group membership" },
386
+ });
387
+
321
388
  const updateCommand = buildCommand({
322
389
  func: async function (
323
390
  this: PlaybooksContext,
@@ -402,9 +469,13 @@ const app = buildApplication(
402
469
  "assign-project": assignProjectCommand,
403
470
  scope: scopeCommand,
404
471
  "set-global": setGlobalCommand,
472
+ "set-none": setNoneCommand,
405
473
  "add-project": addProjectCommand,
406
474
  "remove-project": removeProjectCommand,
407
475
  "replace-projects": replaceProjectsCommand,
476
+ "add-group": addGroupCommand,
477
+ "remove-group": removeGroupCommand,
478
+ "replace-groups": replaceGroupsCommand,
408
479
  update: updateCommand,
409
480
  contain: buildPairedIdCommand(
410
481
  "playbooks.contain",
@@ -200,6 +200,73 @@ const replaceProjectsCommand = buildCommand({
200
200
  docs: { brief: "Replace a Rule's entire project membership" },
201
201
  });
202
202
 
203
+ const setNoneCommand = buildCommand({
204
+ func: async function (this: RulesContext, _flags: Record<string, never>, id: string) {
205
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("rules.set_none", { id });
206
+ render.call(this, scope, renderScope(scope));
207
+ },
208
+ parameters: { flags: {}, positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] } },
209
+ docs: { brief: "Fully hide a Rule -- never applicable, never injected, regardless of project" },
210
+ });
211
+
212
+ const addGroupCommand = buildCommand({
213
+ func: async function (this: RulesContext, _flags: Record<string, never>, id: string, group: string) {
214
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("rules.add_group", { id, group });
215
+ render.call(this, scope, renderScope(scope));
216
+ },
217
+ parameters: {
218
+ flags: {},
219
+ positional: {
220
+ kind: "tuple",
221
+ parameters: [
222
+ { brief: "Rule id", parse: String, placeholder: "id" },
223
+ { brief: "Scope group id/name/alias to add", parse: String, placeholder: "group" },
224
+ ],
225
+ },
226
+ },
227
+ docs: { brief: "Add one scope group to a Rule's explicit scope" },
228
+ });
229
+
230
+ const removeGroupCommand = buildCommand({
231
+ func: async function (this: RulesContext, _flags: Record<string, never>, id: string, group: string) {
232
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("rules.remove_group", { id, group });
233
+ render.call(this, scope, renderScope(scope));
234
+ },
235
+ parameters: {
236
+ flags: {},
237
+ positional: {
238
+ kind: "tuple",
239
+ parameters: [
240
+ { brief: "Rule id", parse: String, placeholder: "id" },
241
+ { brief: "Scope group id/name/alias to remove", parse: String, placeholder: "group" },
242
+ ],
243
+ },
244
+ },
245
+ docs: { brief: "Remove one scope group from a Rule's explicit scope" },
246
+ });
247
+
248
+ const replaceGroupsCommand = buildCommand({
249
+ func: async function (this: RulesContext, flags: { groupsJson: string[] }, id: string) {
250
+ const scope = await this.client.call<Record<string, unknown>, CliArtifactScope>("rules.replace_groups", {
251
+ id,
252
+ groups: flags.groupsJson,
253
+ });
254
+ render.call(this, scope, renderScope(scope));
255
+ },
256
+ parameters: {
257
+ flags: {
258
+ groupsJson: {
259
+ brief: "JSON string array of scope group id/name/alias references",
260
+ kind: "parsed",
261
+ parse: parseStringArray,
262
+ placeholder: "json",
263
+ },
264
+ },
265
+ positional: { kind: "tuple", parameters: [{ brief: "Rule id", parse: String, placeholder: "id" }] },
266
+ },
267
+ docs: { brief: "Replace a Rule's entire scope-group membership" },
268
+ });
269
+
203
270
  const showCommand = buildCommand({
204
271
  func: async function (this: RulesContext, _flags: Record<string, never>, id: string) {
205
272
  const artifact = await this.client.call<Record<string, unknown>, CliArtifact>("rules.show", { id });
@@ -293,9 +360,13 @@ const app = buildApplication(
293
360
  "assign-project": assignProjectCommand,
294
361
  scope: scopeCommand,
295
362
  "set-global": setGlobalCommand,
363
+ "set-none": setNoneCommand,
296
364
  "add-project": addProjectCommand,
297
365
  "remove-project": removeProjectCommand,
298
366
  "replace-projects": replaceProjectsCommand,
367
+ "add-group": addGroupCommand,
368
+ "remove-group": removeGroupCommand,
369
+ "replace-groups": replaceGroupsCommand,
299
370
  show: showCommand,
300
371
  preview: previewCommand,
301
372
  enable: buildEnableDisableCommand("enable"),
@@ -0,0 +1,197 @@
1
+ import type { CommandContext } from "@stricli/core";
2
+ import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
3
+ import type { PapyrusClient } from "../client.ts";
4
+ import { runStricliToString } from "./stricli-run.ts";
5
+
6
+ type ScopeGroupsClient = Pick<PapyrusClient, "call">;
7
+
8
+ interface ScopeGroupsContext extends CommandContext {
9
+ readonly client: ScopeGroupsClient;
10
+ readonly json: boolean;
11
+ }
12
+
13
+ function parseStringArray(value: string): string[] {
14
+ const parsed = JSON.parse(value) as unknown;
15
+ if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) throw new Error("value must be a JSON string array");
16
+ return parsed as string[];
17
+ }
18
+
19
+ interface CliScopeGroup {
20
+ id: string;
21
+ name: string;
22
+ aliases: string[];
23
+ }
24
+
25
+ interface CliScopeMember {
26
+ type: "project" | "group";
27
+ id: string;
28
+ }
29
+
30
+ function render(this: ScopeGroupsContext, result: unknown, human: string): void {
31
+ this.process.stdout.write(this.json ? JSON.stringify(result) : human);
32
+ }
33
+
34
+ const listCommand = buildCommand({
35
+ func: async function (this: ScopeGroupsContext, flags: { query?: string; limit?: number }) {
36
+ const groups = await this.client.call<Record<string, unknown>, CliScopeGroup[]>("scope_groups.list", {
37
+ query: flags.query,
38
+ limit: flags.limit,
39
+ });
40
+ render.call(this, groups, groups.length === 0 ? "No registered scope groups." : groups.map((group) => group.name).join("\n"));
41
+ },
42
+ parameters: {
43
+ flags: {
44
+ query: { brief: "Filter by scope group name or alias", kind: "parsed", parse: String, placeholder: "text", optional: true },
45
+ limit: { brief: "Maximum results", kind: "parsed", parse: numberParser, placeholder: "n", optional: true },
46
+ },
47
+ },
48
+ docs: { brief: "List registered scope groups" },
49
+ });
50
+
51
+ const resolveCommand = buildCommand({
52
+ func: async function (this: ScopeGroupsContext, _flags: Record<string, never>, reference: string) {
53
+ const group = await this.client.call<Record<string, unknown>, CliScopeGroup>("scope_groups.resolve", { reference });
54
+ render.call(this, group, group.name);
55
+ },
56
+ parameters: {
57
+ flags: {},
58
+ positional: { kind: "tuple", parameters: [{ brief: "Scope group id, name, or alias", parse: String, placeholder: "reference" }] },
59
+ },
60
+ docs: { brief: "Resolve a scope group reference to its canonical identity" },
61
+ });
62
+
63
+ const registerCommand = buildCommand({
64
+ func: async function (this: ScopeGroupsContext, flags: { aliasesJson?: string[]; existingId?: string }, name: string) {
65
+ const registered = await this.client.call<Record<string, unknown>, CliScopeGroup>("scope_groups.register", {
66
+ name,
67
+ aliases: flags.aliasesJson,
68
+ existing_id: flags.existingId,
69
+ });
70
+ render.call(this, registered, registered.name);
71
+ },
72
+ parameters: {
73
+ flags: {
74
+ aliasesJson: { brief: "JSON string array of aliases", kind: "parsed", parse: parseStringArray, placeholder: "json", optional: true },
75
+ existingId: {
76
+ brief: "Existing scope group id when renaming",
77
+ kind: "parsed",
78
+ parse: String,
79
+ placeholder: "id",
80
+ optional: true,
81
+ },
82
+ },
83
+ positional: { kind: "tuple", parameters: [{ brief: "Scope group name", parse: String, placeholder: "name" }] },
84
+ },
85
+ docs: { brief: "Register a new scope group, or rename an existing one" },
86
+ });
87
+
88
+ const showCommand = buildCommand({
89
+ func: async function (this: ScopeGroupsContext, _flags: Record<string, never>, group: string) {
90
+ const result = await this.client.call<Record<string, unknown>, { group: CliScopeGroup; members: CliScopeMember[] }>(
91
+ "scope_groups.show",
92
+ { group },
93
+ );
94
+ render.call(
95
+ this,
96
+ result,
97
+ `${result.group.name}: ${result.members.length === 0 ? "(no members)" : result.members.map((m) => `${m.type}:${m.id}`).join(", ")}`,
98
+ );
99
+ },
100
+ parameters: {
101
+ flags: {},
102
+ positional: { kind: "tuple", parameters: [{ brief: "Scope group id, name, or alias", parse: String, placeholder: "group" }] },
103
+ },
104
+ docs: { brief: "Show a scope group's identity and its own direct membership" },
105
+ });
106
+
107
+ const addMemberCommand = buildCommand({
108
+ func: async function (
109
+ this: ScopeGroupsContext,
110
+ _flags: Record<string, never>,
111
+ group: string,
112
+ memberType: string,
113
+ memberReference: string,
114
+ ) {
115
+ const result = await this.client.call<Record<string, unknown>, { group: CliScopeGroup; members: CliScopeMember[] }>(
116
+ "scope_groups.add_member",
117
+ { group, member_type: memberType, member_reference: memberReference },
118
+ );
119
+ render.call(this, result, `${result.group.name}: ${result.members.length} member(s)`);
120
+ },
121
+ parameters: {
122
+ flags: {},
123
+ positional: {
124
+ kind: "tuple",
125
+ parameters: [
126
+ { brief: "Scope group id, name, or alias", parse: String, placeholder: "group" },
127
+ { brief: '"project" or "group"', parse: String, placeholder: "member-type" },
128
+ { brief: "Exact project or scope group id/name/alias/root reference", parse: String, placeholder: "member-reference" },
129
+ ],
130
+ },
131
+ },
132
+ docs: { brief: "Add one member (a project, or another scope group) to a scope group" },
133
+ });
134
+
135
+ const removeMemberCommand = buildCommand({
136
+ func: async function (
137
+ this: ScopeGroupsContext,
138
+ _flags: Record<string, never>,
139
+ group: string,
140
+ memberType: string,
141
+ memberReference: string,
142
+ ) {
143
+ const result = await this.client.call<Record<string, unknown>, { group: CliScopeGroup; members: CliScopeMember[] }>(
144
+ "scope_groups.remove_member",
145
+ { group, member_type: memberType, member_reference: memberReference },
146
+ );
147
+ render.call(this, result, `${result.group.name}: ${result.members.length} member(s)`);
148
+ },
149
+ parameters: {
150
+ flags: {},
151
+ positional: {
152
+ kind: "tuple",
153
+ parameters: [
154
+ { brief: "Scope group id, name, or alias", parse: String, placeholder: "group" },
155
+ { brief: '"project" or "group"', parse: String, placeholder: "member-type" },
156
+ { brief: "Exact project or scope group id/name/alias/root reference", parse: String, placeholder: "member-reference" },
157
+ ],
158
+ },
159
+ },
160
+ docs: { brief: "Remove one member from a scope group" },
161
+ });
162
+
163
+ const deleteCommand = buildCommand({
164
+ func: async function (this: ScopeGroupsContext, _flags: Record<string, never>, group: string) {
165
+ const result = await this.client.call<Record<string, unknown>, { deleted: boolean; id: string }>("scope_groups.delete", { group });
166
+ render.call(this, result, `Deleted scope group ${result.id}`);
167
+ },
168
+ parameters: {
169
+ flags: {},
170
+ positional: { kind: "tuple", parameters: [{ brief: "Scope group id, name, or alias", parse: String, placeholder: "group" }] },
171
+ },
172
+ docs: { brief: "Delete a scope group outright (refuses if still referenced)" },
173
+ });
174
+
175
+ const app = buildApplication(
176
+ buildRouteMap({
177
+ routes: {
178
+ list: listCommand,
179
+ resolve: resolveCommand,
180
+ register: registerCommand,
181
+ show: showCommand,
182
+ "add-member": addMemberCommand,
183
+ "remove-member": removeMemberCommand,
184
+ delete: deleteCommand,
185
+ },
186
+ docs: {
187
+ brief: "Scope group operations -- named, reusable, possibly-nested collections of projects/groups for Doc/Rule/Playbook scoping",
188
+ },
189
+ }),
190
+ { name: "scope-groups", scanner: { caseStyle: "allow-kebab-for-camel" } },
191
+ );
192
+
193
+ export async function runScopeGroupsCli(args: string[], client: ScopeGroupsClient): Promise<string> {
194
+ const json = args.includes("--json");
195
+ const positional = args.filter((arg) => arg !== "--json");
196
+ return runStricliToString(app, positional, { client, json });
197
+ }
@@ -2,7 +2,7 @@ import type { CommandContext } from "@stricli/core";
2
2
  import { buildApplication, buildCommand, buildRouteMap, numberParser } from "@stricli/core";
3
3
  import type { PapyrusClient } from "../client.ts";
4
4
  import { TASK_EXECUTION_MAX_NODES } from "../constants.ts";
5
- import type { GateResult } from "../domain/gate.ts";
5
+ import type { GateResult } from "../gate/gate.ts";
6
6
  import type { OperationName } from "../service.ts";
7
7
  import type { TaskExecutionPlan } from "../task/task-execution.ts";
8
8
  import type { TaskBlockage, TaskCompletion } from "../task/task-service.ts";
package/src/cli.ts CHANGED
@@ -16,6 +16,7 @@ import { runNoteCli } from "./cli/note-command.ts";
16
16
  import { runPlaybooksCli } from "./cli/playbooks-command.ts";
17
17
  import { runProjectsCli } from "./cli/projects-command.ts";
18
18
  import { runRulesCli } from "./cli/rules-command.ts";
19
+ import { runScopeGroupsCli } from "./cli/scope-groups-command.ts";
19
20
  import { runSessionIdentityCli } from "./cli/session-identity-command.ts";
20
21
  import { runTaskCli } from "./cli/task-command.ts";
21
22
  import { connectPapyrusClient } from "./client.ts";
@@ -362,6 +363,7 @@ export {
362
363
  runPlaybooksCli,
363
364
  runProjectsCli,
364
365
  runRulesCli,
366
+ runScopeGroupsCli,
365
367
  runSessionIdentityCli,
366
368
  runTaskCli,
367
369
  };
@@ -387,6 +389,11 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
387
389
  console.log(await runProjectsCli(args.slice(1), client));
388
390
  return;
389
391
  }
392
+ if (command === "scope-groups") {
393
+ const client = await connectPapyrusClient();
394
+ console.log(await runScopeGroupsCli(args.slice(1), client));
395
+ return;
396
+ }
390
397
  if (command === "notes") {
391
398
  const client = await connectPapyrusClient();
392
399
  console.log(await runNoteCli(args.slice(1), client));
package/src/constants.ts CHANGED
@@ -17,7 +17,7 @@ export const DAEMON_PROBE_TIMEOUT_MS = 800;
17
17
  export const DAEMON_UNIT_NAME = "papyrus.service";
18
18
  export const DAEMON_DIR_ENV = "PAPYRUS_DAEMON_DIR";
19
19
  export const SQLITE_BUSY_TIMEOUT_MS = 5_000;
20
- export const SQLITE_SCHEMA_VERSION = 28;
20
+ export const SQLITE_SCHEMA_VERSION = 29;
21
21
  export const SERVICE_MAX_BODY_BYTES = 1_048_576;
22
22
 
23
23
  export const WAL_CHECKPOINT_INTERVAL_MS = 60_000;
@@ -278,12 +278,20 @@ export const ARTIFACT_TRASH_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
278
278
  export const TASK_SCOPE_MAX_TASKS = 1_000;
279
279
  /** Docs/Rules/Skills project scope listing bound, mirroring TASK_SCOPE_MAX_TASKS. */
280
280
  export const ARTIFACT_SCOPE_MAX_ARTIFACTS = 1_000;
281
- /** How many distinct registered projects a single Doc/Rule/Playbook may belong to at once, in "projects" scope mode. */
281
+ /** How many distinct registered projects a single Doc/Rule/Playbook may belong to at once, in "projects" scope mode. Kept alongside ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT (identical value) for the pure-project call sites/tests that predate mixed project+group membership. */
282
282
  export const ARTIFACT_SCOPE_MAX_PROJECTS_PER_ARTIFACT = 50;
283
+ /** How many total members (projects and/or scope groups combined) a single Doc/Rule/Playbook may have at once, in "explicit" scope mode. */
284
+ export const ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT = 50;
283
285
  export const TASK_PROJECT_ROOT_MAX_LENGTH = 4_096;
284
286
  export const TASK_PROJECT_NAME_MAX_LENGTH = 200;
285
287
  export const TASK_PROJECT_ALIAS_MAX_COUNT = 20;
286
288
  export const TASK_PROJECT_LIST_MAX_RESULTS = 100;
289
+ export const SCOPE_GROUP_NAME_MAX_LENGTH = 200;
290
+ export const SCOPE_GROUP_ALIAS_MAX_COUNT = 20;
291
+ export const SCOPE_GROUP_MAX_MEMBERS = 50;
292
+ /** Bounds recursive group-in-group expansion at both add-time (refuses a nesting that would exceed it) and resolution time. */
293
+ export const SCOPE_GROUP_MAX_NESTING_DEPTH = 5;
294
+ export const SCOPE_GROUP_LIST_MAX_RESULTS = 100;
287
295
  export const TASK_CREATE_IDEMPOTENCY_KEY_MAX_LENGTH = 200;
288
296
  export const TASK_CREATE_IDEMPOTENCY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
289
297
  export const TASK_MUTATION_IDEMPOTENCY_KEY_MAX_LENGTH = 200;