@markjaquith/agency 2.10.0 → 2.11.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.
@@ -0,0 +1,519 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/markjaquith/agency/schemas/agency-graph-v1.schema.json",
4
+ "title": "Agency workbase graph v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "version",
9
+ "workbase",
10
+ "filters",
11
+ "includes",
12
+ "nodes",
13
+ "edges",
14
+ "summary",
15
+ "validation"
16
+ ],
17
+ "properties": {
18
+ "version": { "const": 1 },
19
+ "workbase": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "required": ["version"],
23
+ "properties": {
24
+ "version": { "const": 2 },
25
+ "root": { "type": "string" }
26
+ }
27
+ },
28
+ "filters": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["ready", "blocked", "statuses", "repositories", "kinds"],
32
+ "properties": {
33
+ "ready": { "type": ["boolean", "null"] },
34
+ "blocked": { "type": ["boolean", "null"] },
35
+ "statuses": {
36
+ "type": "array",
37
+ "items": { "$ref": "#/$defs/status" }
38
+ },
39
+ "repositories": {
40
+ "type": "array",
41
+ "items": { "type": "string" }
42
+ },
43
+ "kinds": {
44
+ "type": "array",
45
+ "items": {
46
+ "enum": ["epic", "task", "phase", "repository", "execution-unit"]
47
+ }
48
+ }
49
+ }
50
+ },
51
+ "includes": {
52
+ "type": "array",
53
+ "items": { "enum": ["bodies", "workspace", "git", "pr"] },
54
+ "uniqueItems": true
55
+ },
56
+ "nodes": {
57
+ "type": "array",
58
+ "items": { "$ref": "#/$defs/node" }
59
+ },
60
+ "edges": {
61
+ "type": "array",
62
+ "items": { "$ref": "#/$defs/edge" }
63
+ },
64
+ "summary": { "$ref": "#/$defs/progress" },
65
+ "validation": {
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "required": ["valid", "issues"],
69
+ "properties": {
70
+ "valid": { "type": "boolean" },
71
+ "issues": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": ["path", "message"],
77
+ "properties": {
78
+ "path": { "type": "string" },
79
+ "message": { "type": "string" }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "$defs": {
87
+ "status": {
88
+ "enum": ["open", "working", "delegated", "done", "dropped"]
89
+ },
90
+ "progress": {
91
+ "type": "object",
92
+ "additionalProperties": false,
93
+ "required": [
94
+ "status",
95
+ "total",
96
+ "open",
97
+ "working",
98
+ "delegated",
99
+ "done",
100
+ "dropped",
101
+ "terminal"
102
+ ],
103
+ "properties": {
104
+ "status": { "$ref": "#/$defs/status" },
105
+ "total": { "type": "number" },
106
+ "open": { "type": "number" },
107
+ "working": { "type": "number" },
108
+ "delegated": { "type": "number" },
109
+ "done": { "type": "number" },
110
+ "dropped": { "type": "number" },
111
+ "terminal": { "type": "number" }
112
+ }
113
+ },
114
+ "blocker": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "required": ["kind", "id", "reason"],
118
+ "properties": {
119
+ "kind": { "enum": ["dependency", "validation", "status"] },
120
+ "id": { "type": "string" },
121
+ "status": { "$ref": "#/$defs/status" },
122
+ "reason": { "type": "string" }
123
+ }
124
+ },
125
+ "readiness": {
126
+ "type": "object",
127
+ "additionalProperties": false,
128
+ "required": ["ready", "blocked", "blockers"],
129
+ "properties": {
130
+ "ready": { "type": "boolean" },
131
+ "blocked": { "type": "boolean" },
132
+ "blockers": {
133
+ "type": "array",
134
+ "items": { "$ref": "#/$defs/blocker" }
135
+ }
136
+ }
137
+ },
138
+ "node": {
139
+ "type": "object",
140
+ "additionalProperties": false,
141
+ "required": [
142
+ "id",
143
+ "kind",
144
+ "key",
145
+ "status",
146
+ "readiness",
147
+ "aggregate",
148
+ "dependents",
149
+ "repositories",
150
+ "data"
151
+ ],
152
+ "properties": {
153
+ "id": { "type": "string" },
154
+ "kind": {
155
+ "enum": ["epic", "task", "phase", "repository", "execution-unit"]
156
+ },
157
+ "key": { "type": "string" },
158
+ "status": {
159
+ "oneOf": [{ "$ref": "#/$defs/status" }, { "type": "null" }]
160
+ },
161
+ "readiness": {
162
+ "oneOf": [{ "$ref": "#/$defs/readiness" }, { "type": "null" }]
163
+ },
164
+ "aggregate": {
165
+ "oneOf": [{ "$ref": "#/$defs/progress" }, { "type": "null" }]
166
+ },
167
+ "dependents": { "type": "array", "items": { "type": "string" } },
168
+ "repositories": { "type": "array", "items": { "type": "string" } },
169
+ "data": {
170
+ "oneOf": [
171
+ { "$ref": "#/$defs/epicData" },
172
+ { "$ref": "#/$defs/singleTaskData" },
173
+ { "$ref": "#/$defs/multiTaskData" },
174
+ { "$ref": "#/$defs/phaseData" },
175
+ { "$ref": "#/$defs/executionData" },
176
+ { "$ref": "#/$defs/repositoryData" }
177
+ ]
178
+ },
179
+ "body": { "type": "string" },
180
+ "workspace": {
181
+ "oneOf": [
182
+ { "$ref": "#/$defs/documentWorkspace" },
183
+ { "$ref": "#/$defs/repositoryWorkspace" },
184
+ { "$ref": "#/$defs/executionWorkspace" }
185
+ ]
186
+ },
187
+ "git": {
188
+ "oneOf": [
189
+ { "$ref": "#/$defs/repositoryGit" },
190
+ { "$ref": "#/$defs/executionGit" }
191
+ ]
192
+ },
193
+ "pr": { "$ref": "#/$defs/pr" }
194
+ },
195
+ "allOf": [
196
+ {
197
+ "if": { "properties": { "kind": { "const": "epic" } } },
198
+ "then": {
199
+ "properties": {
200
+ "data": { "$ref": "#/$defs/epicData" },
201
+ "workspace": { "$ref": "#/$defs/documentWorkspace" },
202
+ "status": { "$ref": "#/$defs/status" },
203
+ "readiness": { "$ref": "#/$defs/readiness" },
204
+ "aggregate": { "$ref": "#/$defs/progress" }
205
+ },
206
+ "not": {
207
+ "anyOf": [{ "required": ["git"] }, { "required": ["pr"] }]
208
+ }
209
+ }
210
+ },
211
+ {
212
+ "if": { "properties": { "kind": { "const": "task" } } },
213
+ "then": {
214
+ "properties": {
215
+ "data": {
216
+ "oneOf": [
217
+ { "$ref": "#/$defs/singleTaskData" },
218
+ { "$ref": "#/$defs/multiTaskData" }
219
+ ]
220
+ },
221
+ "workspace": { "$ref": "#/$defs/documentWorkspace" },
222
+ "status": { "$ref": "#/$defs/status" },
223
+ "readiness": { "$ref": "#/$defs/readiness" },
224
+ "aggregate": { "$ref": "#/$defs/progress" }
225
+ },
226
+ "not": {
227
+ "anyOf": [{ "required": ["git"] }, { "required": ["pr"] }]
228
+ }
229
+ }
230
+ },
231
+ {
232
+ "if": { "properties": { "kind": { "const": "phase" } } },
233
+ "then": {
234
+ "properties": {
235
+ "data": { "$ref": "#/$defs/phaseData" },
236
+ "workspace": { "$ref": "#/$defs/documentWorkspace" },
237
+ "status": { "$ref": "#/$defs/status" },
238
+ "readiness": { "$ref": "#/$defs/readiness" },
239
+ "aggregate": { "$ref": "#/$defs/progress" }
240
+ },
241
+ "not": {
242
+ "anyOf": [{ "required": ["git"] }, { "required": ["pr"] }]
243
+ }
244
+ }
245
+ },
246
+ {
247
+ "if": { "properties": { "kind": { "const": "repository" } } },
248
+ "then": {
249
+ "properties": {
250
+ "data": { "$ref": "#/$defs/repositoryData" },
251
+ "workspace": { "$ref": "#/$defs/repositoryWorkspace" },
252
+ "git": { "$ref": "#/$defs/repositoryGit" },
253
+ "status": { "type": "null" },
254
+ "readiness": { "type": "null" },
255
+ "aggregate": { "type": "null" }
256
+ },
257
+ "not": {
258
+ "anyOf": [{ "required": ["body"] }, { "required": ["pr"] }]
259
+ }
260
+ }
261
+ },
262
+ {
263
+ "if": { "properties": { "kind": { "const": "execution-unit" } } },
264
+ "then": {
265
+ "properties": {
266
+ "data": { "$ref": "#/$defs/executionData" },
267
+ "workspace": { "$ref": "#/$defs/executionWorkspace" },
268
+ "git": { "$ref": "#/$defs/executionGit" },
269
+ "pr": { "$ref": "#/$defs/pr" },
270
+ "status": { "$ref": "#/$defs/status" },
271
+ "readiness": { "$ref": "#/$defs/readiness" },
272
+ "aggregate": { "$ref": "#/$defs/progress" }
273
+ },
274
+ "not": { "required": ["body"] }
275
+ }
276
+ }
277
+ ]
278
+ },
279
+ "repositoryReference": {
280
+ "type": "object",
281
+ "additionalProperties": false,
282
+ "required": ["repo", "ref"],
283
+ "properties": {
284
+ "repo": { "type": "string" },
285
+ "ref": { "type": "string" }
286
+ }
287
+ },
288
+ "dependency": {
289
+ "type": "object",
290
+ "additionalProperties": false,
291
+ "required": ["id"],
292
+ "properties": {
293
+ "id": { "type": "string" },
294
+ "dependsOn": { "type": "array", "items": { "type": "string" } }
295
+ }
296
+ },
297
+ "epicData": {
298
+ "type": "object",
299
+ "additionalProperties": false,
300
+ "required": ["ticketUrl", "repos", "tasks", "sha256"],
301
+ "properties": {
302
+ "ticketUrl": { "type": "string" },
303
+ "description": { "type": "string" },
304
+ "repos": {
305
+ "type": "array",
306
+ "items": { "$ref": "#/$defs/repositoryReference" }
307
+ },
308
+ "tasks": { "type": "array", "items": { "$ref": "#/$defs/dependency" } },
309
+ "sha256": { "type": "string" }
310
+ }
311
+ },
312
+ "singleTaskData": {
313
+ "type": "object",
314
+ "additionalProperties": false,
315
+ "required": [
316
+ "ticketUrl",
317
+ "repo",
318
+ "branch",
319
+ "base",
320
+ "pr",
321
+ "status",
322
+ "sha256"
323
+ ],
324
+ "properties": {
325
+ "ticketUrl": { "type": ["string", "null"] },
326
+ "description": { "type": "string" },
327
+ "epic": { "type": "string" },
328
+ "repo": { "type": "string" },
329
+ "repos": {
330
+ "type": "array",
331
+ "items": { "$ref": "#/$defs/repositoryReference" }
332
+ },
333
+ "branch": { "type": "string" },
334
+ "base": { "type": "string" },
335
+ "pr": { "type": ["string", "null"] },
336
+ "status": { "$ref": "#/$defs/status" },
337
+ "sha256": { "type": "string" }
338
+ }
339
+ },
340
+ "multiTaskData": {
341
+ "type": "object",
342
+ "additionalProperties": false,
343
+ "required": ["ticketUrl", "phases", "sha256"],
344
+ "properties": {
345
+ "ticketUrl": { "type": ["string", "null"] },
346
+ "description": { "type": "string" },
347
+ "epic": { "type": "string" },
348
+ "phases": {
349
+ "type": "array",
350
+ "items": { "$ref": "#/$defs/dependency" }
351
+ },
352
+ "sha256": { "type": "string" }
353
+ }
354
+ },
355
+ "phaseData": {
356
+ "type": "object",
357
+ "additionalProperties": false,
358
+ "required": ["repo", "branch", "base", "pr", "status", "sha256"],
359
+ "properties": {
360
+ "description": { "type": "string" },
361
+ "repo": { "type": "string" },
362
+ "repos": {
363
+ "type": "array",
364
+ "items": { "$ref": "#/$defs/repositoryReference" }
365
+ },
366
+ "branch": { "type": "string" },
367
+ "base": { "type": "string" },
368
+ "pr": { "type": ["string", "null"] },
369
+ "status": { "$ref": "#/$defs/status" },
370
+ "sha256": { "type": "string" }
371
+ }
372
+ },
373
+ "executionData": {
374
+ "type": "object",
375
+ "additionalProperties": false,
376
+ "required": ["taskId", "repo", "branch", "base", "pr", "status"],
377
+ "properties": {
378
+ "taskId": { "type": "string" },
379
+ "phaseId": { "type": "string" },
380
+ "ticketUrl": { "type": ["string", "null"] },
381
+ "epic": { "type": "string" },
382
+ "description": { "type": "string" },
383
+ "repo": { "type": "string" },
384
+ "repos": {
385
+ "type": "array",
386
+ "items": { "$ref": "#/$defs/repositoryReference" }
387
+ },
388
+ "branch": { "type": "string" },
389
+ "base": { "type": "string" },
390
+ "pr": { "type": ["string", "null"] },
391
+ "status": { "$ref": "#/$defs/status" }
392
+ }
393
+ },
394
+ "repositoryData": {
395
+ "type": "object",
396
+ "additionalProperties": false,
397
+ "required": ["alias"],
398
+ "properties": { "alias": { "type": "string" } }
399
+ },
400
+ "documentWorkspace": {
401
+ "type": "object",
402
+ "additionalProperties": false,
403
+ "required": ["documentPath", "directory"],
404
+ "properties": {
405
+ "documentPath": { "type": "string" },
406
+ "directory": { "type": "string" }
407
+ }
408
+ },
409
+ "repositoryWorkspace": {
410
+ "type": "object",
411
+ "additionalProperties": false,
412
+ "required": ["path", "target"],
413
+ "properties": {
414
+ "path": { "type": "string" },
415
+ "target": { "type": ["string", "null"] }
416
+ }
417
+ },
418
+ "executionWorkspace": {
419
+ "type": "object",
420
+ "additionalProperties": false,
421
+ "required": ["codePath", "checkoutPath", "materialized"],
422
+ "properties": {
423
+ "codePath": { "type": "string" },
424
+ "checkoutPath": { "type": "string" },
425
+ "materialized": { "type": "boolean" }
426
+ }
427
+ },
428
+ "repositoryGit": {
429
+ "type": "object",
430
+ "additionalProperties": false,
431
+ "required": ["kind", "remote", "head", "branch"],
432
+ "properties": {
433
+ "kind": { "enum": ["bare", "repository", null] },
434
+ "remote": { "type": ["string", "null"] },
435
+ "head": { "type": ["string", "null"] },
436
+ "branch": { "type": ["string", "null"] }
437
+ }
438
+ },
439
+ "executionGit": {
440
+ "type": "object",
441
+ "additionalProperties": false,
442
+ "required": [
443
+ "branch",
444
+ "base",
445
+ "branchCommit",
446
+ "baseCommit",
447
+ "checkoutCommit",
448
+ "checkoutBranch",
449
+ "dirty"
450
+ ],
451
+ "properties": {
452
+ "branch": { "type": "string" },
453
+ "base": { "type": "string" },
454
+ "branchCommit": { "type": ["string", "null"] },
455
+ "baseCommit": { "type": ["string", "null"] },
456
+ "checkoutCommit": { "type": ["string", "null"] },
457
+ "checkoutBranch": { "type": ["string", "null"] },
458
+ "dirty": { "type": ["boolean", "null"] }
459
+ }
460
+ },
461
+ "pr": {
462
+ "oneOf": [
463
+ {
464
+ "type": "object",
465
+ "additionalProperties": false,
466
+ "required": ["url", "state"],
467
+ "properties": {
468
+ "url": { "type": "null" },
469
+ "state": { "const": "none" }
470
+ }
471
+ },
472
+ {
473
+ "type": "object",
474
+ "additionalProperties": false,
475
+ "required": ["url", "state"],
476
+ "properties": {
477
+ "url": { "type": "string" },
478
+ "state": { "const": "unavailable" }
479
+ }
480
+ },
481
+ {
482
+ "type": "object",
483
+ "additionalProperties": false,
484
+ "required": [
485
+ "recordedUrl",
486
+ "number",
487
+ "state",
488
+ "title",
489
+ "isDraft",
490
+ "headRefName",
491
+ "baseRefName",
492
+ "url"
493
+ ],
494
+ "properties": {
495
+ "recordedUrl": { "type": "string" },
496
+ "number": { "type": "number" },
497
+ "state": { "type": "string" },
498
+ "title": { "type": "string" },
499
+ "isDraft": { "type": "boolean" },
500
+ "headRefName": { "type": "string" },
501
+ "baseRefName": { "type": "string" },
502
+ "url": { "type": "string" }
503
+ }
504
+ }
505
+ ]
506
+ },
507
+ "edge": {
508
+ "type": "object",
509
+ "additionalProperties": false,
510
+ "required": ["id", "kind", "from", "to"],
511
+ "properties": {
512
+ "id": { "type": "string" },
513
+ "kind": { "enum": ["owns", "depends_on", "writes", "references"] },
514
+ "from": { "type": "string" },
515
+ "to": { "type": "string" }
516
+ }
517
+ }
518
+ }
519
+ }
@@ -203,6 +203,12 @@ Follow these invariants:
203
203
  Commands that print Agency-owned results accept `--json` for machine-readable
204
204
  output, including mutations, entity inspection, status, validation, and PR creation.
205
205
 
206
+ Use `agency graph --json` to load the complete, versioned workbase graph without
207
+ walking directories. Filter it with repeatable `--status`, `--repository`, and
208
+ `--kind` options or with `--ready`/`--blocked`. Add durable prose and observational
209
+ details explicitly with `--include bodies|workspace|git|pr`. For large workbases,
210
+ `--jsonl` streams equivalent versioned metadata, node, edge, and end records.
211
+
206
212
  - Multi-phase task frontmatter owns the phase dependency graph.
207
213
  - Epic frontmatter owns the child-task dependency graph.
208
214
  - `pr` is either a GitHub PR URL string or `null`.
@@ -140,6 +140,7 @@ describe("strict CLI parsing", () => {
140
140
  [["status", "extra"], "agency status"],
141
141
  [["validate", "one", "two"], "agency validate"],
142
142
  [["context", "one", "two"], "agency context"],
143
+ [["graph", "extra"], "agency graph"],
143
144
  ] as const) {
144
145
  expectUsageError([...args], usage)
145
146
  }
@@ -154,6 +155,50 @@ describe("strict CLI parsing", () => {
154
155
  expectUsageError(["status", "--compact"], "agency status")
155
156
  })
156
157
 
158
+ test("accepts repeatable graph filters and rejects output conflicts", () => {
159
+ expect(
160
+ parseCli([
161
+ "graph",
162
+ "--json",
163
+ "--status",
164
+ "open",
165
+ "--status",
166
+ "working",
167
+ "--repository",
168
+ "agency",
169
+ "--kind",
170
+ "task",
171
+ "--kind",
172
+ "phase",
173
+ "--include",
174
+ "bodies",
175
+ "--include",
176
+ "git",
177
+ ]).values,
178
+ ).toMatchObject({
179
+ json: true,
180
+ status: ["open", "working"],
181
+ repository: ["agency"],
182
+ kind: ["task", "phase"],
183
+ include: ["bodies", "git"],
184
+ })
185
+ expect(() => parseCli(["graph", "--json", "--jsonl"])).toThrow(
186
+ "cannot be combined",
187
+ )
188
+ expect(() => parseCli(["graph", "--ready", "--blocked"])).toThrow(
189
+ "cannot be combined",
190
+ )
191
+ for (const [option, value] of [
192
+ ["status", "started"],
193
+ ["kind", "worktree"],
194
+ ["include", "secrets"],
195
+ ] as const) {
196
+ expect(() => parseCli(["graph", `--${option}`, value])).toThrow(
197
+ `Invalid '--${option}' value`,
198
+ )
199
+ }
200
+ })
201
+
157
202
  test("reports unknown subcommands with parent usage", () => {
158
203
  expect(() => parseCli(["task", "crate"])).toThrow(
159
204
  "Unknown subcommand 'crate'",
package/src/cli-parser.ts CHANGED
@@ -351,6 +351,39 @@ const commands = {
351
351
  options: ["json", "compact"],
352
352
  },
353
353
  },
354
+ graph: {
355
+ usage: "agency graph [options]",
356
+ options: {
357
+ ...outputOptions,
358
+ jsonl: { type: "boolean" },
359
+ ready: { type: "boolean" },
360
+ blocked: { type: "boolean" },
361
+ status: { type: "string", multiple: true },
362
+ repository: { type: "string", multiple: true },
363
+ kind: { type: "string", multiple: true },
364
+ include: { type: "string", multiple: true },
365
+ },
366
+ command: {
367
+ usage: "agency graph [options]",
368
+ minArgs: 0,
369
+ maxArgs: 0,
370
+ options: [
371
+ "json",
372
+ "jsonl",
373
+ "ready",
374
+ "blocked",
375
+ "status",
376
+ "repository",
377
+ "kind",
378
+ "include",
379
+ ],
380
+ repeatable: ["status", "repository", "kind", "include"],
381
+ conflicts: [
382
+ ["json", "jsonl"],
383
+ ["ready", "blocked"],
384
+ ],
385
+ },
386
+ },
354
387
  } satisfies Readonly<Record<string, CommandDefinition>>
355
388
 
356
389
  const rootOptions = commonOptions
@@ -462,6 +495,30 @@ function validateTaskCreate(
462
495
  }
463
496
  }
464
497
 
498
+ function validateGraphOptions(values: ParsedCli["values"], spec: LeafCommand) {
499
+ const allowed = {
500
+ status: new Set(["open", "working", "delegated", "done", "dropped"]),
501
+ kind: new Set(["epic", "task", "phase", "repository", "execution-unit"]),
502
+ include: new Set(["bodies", "workspace", "git", "pr"]),
503
+ }
504
+ for (const [name, accepted] of Object.entries(allowed)) {
505
+ const supplied = values[name]
506
+ const entries = Array.isArray(supplied)
507
+ ? supplied
508
+ : supplied === undefined
509
+ ? []
510
+ : [supplied]
511
+ for (const value of entries) {
512
+ if (typeof value !== "string" || !accepted.has(value)) {
513
+ throw usageError(
514
+ `Invalid '${optionLabel(name)}' value '${String(value)}'. Expected one of: ${[...accepted].join(", ")}.`,
515
+ spec.usage,
516
+ )
517
+ }
518
+ }
519
+ }
520
+ }
521
+
465
522
  export function parseCli(args: readonly string[]): ParsedCli {
466
523
  const commandIndex = findCommandIndex(args)
467
524
  if (commandIndex === -1) {
@@ -589,6 +646,9 @@ export function parseCli(args: readonly string[]): ParsedCli {
589
646
  ) {
590
647
  validateTaskCreate(parsed.values, spec, subcommand === "create")
591
648
  }
649
+ if (commandName === "graph") {
650
+ validateGraphOptions(parsed.values, spec)
651
+ }
592
652
 
593
653
  return {
594
654
  commandName: commandName as keyof typeof commands,