@markjaquith/agency 3.2.13 → 3.2.15

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 (69) hide show
  1. package/package.json +7 -2
  2. package/src/workbase/schemas.ts +12 -0
  3. package/fixtures/protocol/orchestration-recipes.json +0 -53
  4. package/pi-extensions/agency.test.ts +0 -117
  5. package/src/check-commits.test.ts +0 -58
  6. package/src/cli-parser.test.ts +0 -945
  7. package/src/cli.test.ts +0 -1889
  8. package/src/commands/act.test.ts +0 -418
  9. package/src/commands/archive.test.ts +0 -236
  10. package/src/commands/context.test.ts +0 -665
  11. package/src/commands/description.test.ts +0 -103
  12. package/src/commands/doctor.test.ts +0 -185
  13. package/src/commands/epic.test.ts +0 -196
  14. package/src/commands/init.test.ts +0 -117
  15. package/src/commands/integration.test.ts +0 -165
  16. package/src/commands/pr.test.ts +0 -248
  17. package/src/commands/push.test.ts +0 -56
  18. package/src/commands/read-only.test.ts +0 -161
  19. package/src/commands/repo.test.ts +0 -199
  20. package/src/commands/restore.test.ts +0 -106
  21. package/src/commands/status.test.ts +0 -113
  22. package/src/commands/sync.test.ts +0 -200
  23. package/src/commands/task-phase.test.ts +0 -410
  24. package/src/commands/task.test.ts +0 -281
  25. package/src/commands/validate.test.ts +0 -186
  26. package/src/commands/work.test.ts +0 -1375
  27. package/src/commands/workbase.test.ts +0 -170
  28. package/src/graph-schema.test.ts +0 -124
  29. package/src/protocol.test.ts +0 -163
  30. package/src/readiness.test.ts +0 -105
  31. package/src/services/ArchiveBulkService.test.ts +0 -384
  32. package/src/services/ArchiveService.test.ts +0 -1092
  33. package/src/services/EpicService.test.ts +0 -74
  34. package/src/services/FileSystemService.test.ts +0 -81
  35. package/src/services/GraphMutationService.test.ts +0 -486
  36. package/src/services/GraphService.test.ts +0 -494
  37. package/src/services/IntegrationService.test.ts +0 -1091
  38. package/src/services/LifecycleTransaction.test.ts +0 -145
  39. package/src/services/PullRequestService.test.ts +0 -716
  40. package/src/services/PushService.test.ts +0 -408
  41. package/src/services/ReadinessService.test.ts +0 -290
  42. package/src/services/RepositoryService.test.ts +0 -789
  43. package/src/services/ReviewService.test.ts +0 -408
  44. package/src/services/SyncService.test.ts +0 -1377
  45. package/src/services/TaskPhaseService.test.ts +0 -852
  46. package/src/services/VersionControlService.test.ts +0 -62
  47. package/src/services/WorkbaseService.test.ts +0 -910
  48. package/src/services/WorktreeLock.test.ts +0 -205
  49. package/src/services/WorktreePerformance.test.ts +0 -71
  50. package/src/services/WorktreeService.test.ts +0 -2292
  51. package/src/test-utils.ts +0 -110
  52. package/src/usage-log.test.ts +0 -188
  53. package/src/utils/chooser.test.ts +0 -192
  54. package/src/utils/effect.test.ts +0 -30
  55. package/src/utils/interactive.pty.test.ts +0 -128
  56. package/src/utils/interactive.test.tsx +0 -860
  57. package/src/utils/process.test.ts +0 -132
  58. package/src/utils/progress.test.ts +0 -37
  59. package/src/work-view.test.ts +0 -151
  60. package/src/workbase/agent-command.test.ts +0 -128
  61. package/src/workbase/checkout-command.test.ts +0 -62
  62. package/src/workbase/delivery-command.test.ts +0 -180
  63. package/src/workbase/dependency-graph.test.ts +0 -50
  64. package/src/workbase/execution-contract.test.ts +0 -161
  65. package/src/workbase/frontmatter.test.ts +0 -67
  66. package/src/workbase/repository-reference.test.ts +0 -25
  67. package/src/workbase/schemas.test.ts +0 -543
  68. package/src/workbase/work-target.test.ts +0 -108
  69. package/src/workbase/worktree-command.test.ts +0 -61
@@ -1,494 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test"
2
- import { Schema } from "@effect/schema"
3
- import { Effect } from "effect"
4
- import { mkdir } from "node:fs/promises"
5
- import { dirname, join } from "node:path"
6
- import { AgencyGraph } from "../graph-schema"
7
- import {
8
- cleanupTempDir,
9
- createTempDir,
10
- runTestEffect,
11
- trackDocumentReadConcurrency,
12
- } from "../test-utils"
13
- import { documentLoadConcurrency } from "../workbase/document-loading"
14
- import { FileSystemService } from "./FileSystemService"
15
- import { GraphService } from "./GraphService"
16
- import {
17
- VersionControlService,
18
- type VersionControlBackend,
19
- } from "./VersionControlService"
20
-
21
- const write = async (root: string, path: string, content: string) => {
22
- const fullPath = join(root, path)
23
- await mkdir(dirname(fullPath), { recursive: true })
24
- await Bun.write(fullPath, content)
25
- }
26
-
27
- const createWorkbase = async () => {
28
- const root = await createTempDir()
29
- await write(root, "agency.json", '{"version":2}\n')
30
- await mkdir(join(root, "repos/agency"), { recursive: true })
31
- await write(
32
- root,
33
- "epics/delivery/EPIC.md",
34
- `---
35
- ticketUrl: https://example.com/delivery
36
- repos:
37
- - repo: agency
38
- ref: main
39
- tasks:
40
- - id: prepare
41
- - id: ship
42
- dependsOn: [prepare]
43
- ---
44
-
45
- # Delivery
46
- Deliver the graph.
47
- `,
48
- )
49
- await write(
50
- root,
51
- "tasks/prepare/TASK.md",
52
- `---
53
- ticketUrl: null
54
- epic: delivery
55
- repo: agency
56
- branch: feat/prepare
57
- base: main
58
- pr: null
59
- status: done
60
- ---
61
-
62
- # Prepare
63
- `,
64
- )
65
- await write(
66
- root,
67
- "tasks/ship/TASK.md",
68
- `---
69
- ticketUrl: null
70
- epic: delivery
71
- phases:
72
- - id: implement
73
- - id: verify
74
- dependsOn: [implement]
75
- ---
76
-
77
- # Ship
78
- `,
79
- )
80
- await write(
81
- root,
82
- "tasks/ship/phases/implement/PHASE.md",
83
- `---
84
- repo: agency
85
- branch: feat/implement
86
- base: main
87
- pr: null
88
- status: open
89
- ---
90
-
91
- # Implement
92
- `,
93
- )
94
- await write(
95
- root,
96
- "tasks/ship/phases/verify/PHASE.md",
97
- `---
98
- repo: agency
99
- branch: feat/verify
100
- base: main
101
- pr: null
102
- status: open
103
- ---
104
-
105
- # Verify
106
- `,
107
- )
108
- return root
109
- }
110
-
111
- const getGraph = (root: string, options: Record<string, unknown> = {}) =>
112
- runTestEffect(
113
- GraphService.pipe(
114
- Effect.flatMap((service) => service.get({ cwd: root, ...options })),
115
- ),
116
- )
117
-
118
- describe("GraphService", () => {
119
- const roots: string[] = []
120
-
121
- afterEach(async () => {
122
- await Promise.all(roots.splice(0).map(cleanupTempDir))
123
- })
124
-
125
- test("exports deterministic nodes, typed edges, and computed state", async () => {
126
- const root = await createWorkbase()
127
- roots.push(root)
128
- const graph = await getGraph(root)
129
-
130
- expect(Schema.decodeUnknownSync(AgencyGraph)(graph)).toEqual(graph)
131
- expect(graph.workbase.vcs).toBe("git")
132
- expect(graph.nodes.map((node) => node.id)).toEqual([
133
- "epic:delivery",
134
- "execution-unit:phase/ship/implement",
135
- "execution-unit:phase/ship/verify",
136
- "execution-unit:task/prepare",
137
- "phase:ship/implement",
138
- "phase:ship/verify",
139
- "repository:agency",
140
- "task:prepare",
141
- "task:ship",
142
- ])
143
- expect(graph.edges).toContainEqual({
144
- id: "depends_on:task:ship->task:prepare",
145
- kind: "depends_on",
146
- from: "task:ship",
147
- to: "task:prepare",
148
- })
149
- expect(graph.edges).toContainEqual({
150
- id: "owns:phase:ship/verify->execution-unit:phase/ship/verify",
151
- kind: "owns",
152
- from: "phase:ship/verify",
153
- to: "execution-unit:phase/ship/verify",
154
- })
155
- expect(graph.edges).toContainEqual({
156
- id: "writes:execution-unit:task/prepare->repository:agency",
157
- kind: "writes",
158
- from: "execution-unit:task/prepare",
159
- to: "repository:agency",
160
- })
161
-
162
- const prepare = graph.nodes.find((node) => node.id === "task:prepare")!
163
- const ship = graph.nodes.find((node) => node.id === "task:ship")!
164
- const implement = graph.nodes.find(
165
- (node) => node.id === "phase:ship/implement",
166
- )!
167
- const verify = graph.nodes.find((node) => node.id === "phase:ship/verify")!
168
- expect(prepare.dependents).toEqual(["task:ship"])
169
- expect(prepare.readiness).toMatchObject({
170
- ready: false,
171
- terminal: true,
172
- })
173
- expect(ship.readiness).toMatchObject({
174
- ready: true,
175
- blocked: false,
176
- terminal: false,
177
- })
178
- expect(ship.aggregate).toMatchObject({ total: 2, open: 2, terminal: 0 })
179
- expect(implement.dependents).toEqual(["phase:ship/verify"])
180
- expect(implement.readiness).toMatchObject({ ready: true, blocked: false })
181
- expect(
182
- graph.nodes.find(
183
- (node) => node.id === "execution-unit:phase/ship/implement",
184
- )?.readiness,
185
- ).toMatchObject({
186
- ready: true,
187
- blocked: false,
188
- blockedBy: [],
189
- terminal: false,
190
- })
191
- expect(verify.readiness).toMatchObject({
192
- ready: false,
193
- blocked: true,
194
- blockedBy: ["phase:ship/implement"],
195
- terminal: false,
196
- blockers: [
197
- {
198
- kind: "dependency",
199
- id: "phase:ship/implement",
200
- status: "open",
201
- },
202
- ],
203
- })
204
- const epic = graph.nodes.find((node) => node.id === "epic:delivery")!
205
- expect(epic.aggregate).toMatchObject({ total: 3, done: 1, open: 2 })
206
- expect(graph.summary).toEqual({
207
- status: "open",
208
- total: 3,
209
- open: 2,
210
- working: 0,
211
- delegated: 0,
212
- done: 1,
213
- dropped: 0,
214
- terminal: 1,
215
- })
216
- expect(await getGraph(root)).toEqual(graph)
217
- })
218
-
219
- test("bounds document reads across nested task and phase traversal", async () => {
220
- const root = await createWorkbase()
221
- roots.push(root)
222
- await Promise.all(
223
- Array.from({ length: documentLoadConcurrency + 8 }, (_, index) =>
224
- write(
225
- root,
226
- `tasks/ship/phases/extra-${index}/PHASE.md`,
227
- `---\nrepo: agency\nbranch: extra-${index}\nbase: main\npr: null\nstatus: open\n---\n`,
228
- ),
229
- ),
230
- )
231
- const fs = await Effect.runPromise(
232
- FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
233
- )
234
- const tracked = trackDocumentReadConcurrency(fs)
235
-
236
- await runTestEffect(
237
- GraphService.pipe(
238
- Effect.flatMap((service) => service.get({ cwd: root })),
239
- Effect.provideService(FileSystemService, tracked.fs),
240
- ),
241
- )
242
-
243
- expect(tracked.maximum()).toBe(documentLoadConcurrency)
244
- })
245
-
246
- test("does not resolve a VCS backend when git details are not requested", async () => {
247
- const root = await createWorkbase()
248
- roots.push(root)
249
- let calls = 0
250
- const graph = await runTestEffect(
251
- GraphService.pipe(
252
- Effect.flatMap((service) => service.get({ cwd: root })),
253
- Effect.provideService(VersionControlService, {
254
- _tag: "VersionControlService",
255
- forWorkbase: () => {
256
- calls += 1
257
- throw new Error("unexpected VCS lookup")
258
- },
259
- }),
260
- ),
261
- )
262
-
263
- expect(graph.nodes.length).toBeGreaterThan(0)
264
- expect(calls).toBe(0)
265
- })
266
-
267
- test("propagates directory discovery failures", async () => {
268
- const root = await createTempDir()
269
- roots.push(root)
270
- await write(root, "agency.json", '{"version":2}\n')
271
- await write(root, "tasks", "not a directory")
272
-
273
- await expect(getGraph(root)).rejects.toThrow(
274
- `Failed to read directory: ${join(root, "tasks")}`,
275
- )
276
- })
277
-
278
- test("never reports active or terminal execution units as ready", async () => {
279
- const root = await createWorkbase()
280
- roots.push(root)
281
- const path = "tasks/ship/phases/implement/PHASE.md"
282
-
283
- for (const status of ["working", "delegated", "done", "dropped"]) {
284
- await write(
285
- root,
286
- path,
287
- `---
288
- repo: agency
289
- branch: feat/implement
290
- base: main
291
- pr: null
292
- status: ${status}
293
- ---
294
-
295
- # Implement
296
- `,
297
- )
298
- const graph = await getGraph(root, {
299
- kinds: ["execution-unit"],
300
- ready: true,
301
- })
302
- expect(graph.nodes.map((node) => node.key)).not.toContain(
303
- "phase/ship/implement",
304
- )
305
- }
306
- })
307
-
308
- test("applies filters after computing graph state", async () => {
309
- const root = await createWorkbase()
310
- roots.push(root)
311
-
312
- const ready = await getGraph(root, {
313
- ready: true,
314
- kinds: ["execution-unit"],
315
- repositories: ["agency"],
316
- statuses: ["open"],
317
- })
318
- expect(ready.nodes.map((node) => node.id)).toEqual([
319
- "execution-unit:phase/ship/implement",
320
- ])
321
- expect(ready.edges).toEqual([])
322
-
323
- const blocked = await getGraph(root, { blocked: true, kinds: ["phase"] })
324
- expect(blocked.nodes.map((node) => node.id)).toEqual(["phase:ship/verify"])
325
- })
326
-
327
- test("does not report structurally invalid phases as ready", async () => {
328
- const root = await createWorkbase()
329
- roots.push(root)
330
- await write(
331
- root,
332
- "tasks/ship/phases/orphan/PHASE.md",
333
- `---
334
- repo: agency
335
- branch: feat/orphan
336
- base: main
337
- pr: null
338
- status: open
339
- ---
340
-
341
- # Orphan
342
- `,
343
- )
344
-
345
- const graph = await getGraph(root, { kinds: ["phase"], ready: true })
346
- expect(graph.nodes.map((node) => node.id)).not.toContain(
347
- "phase:ship/orphan",
348
- )
349
- const complete = await getGraph(root)
350
- expect(
351
- complete.nodes.find((node) => node.id === "phase:ship/orphan")?.readiness,
352
- ).toMatchObject({
353
- ready: false,
354
- blocked: true,
355
- blockers: [
356
- {
357
- kind: "validation",
358
- id: "tasks/ship/TASK.md",
359
- reason: "Unlisted phase 'orphan'",
360
- },
361
- ],
362
- blockedBy: ["tasks/ship/TASK.md"],
363
- })
364
- })
365
-
366
- test("inherits parent epic validation blockers", async () => {
367
- const root = await createWorkbase()
368
- roots.push(root)
369
- await write(
370
- root,
371
- "tasks/unlisted/TASK.md",
372
- `---
373
- ticketUrl: null
374
- epic: delivery
375
- repo: agency
376
- branch: feat/unlisted
377
- base: main
378
- pr: null
379
- status: open
380
- ---
381
-
382
- # Unlisted
383
- `,
384
- )
385
-
386
- const graph = await getGraph(root)
387
- const execution = graph.nodes.find(
388
- (node) => node.id === "execution-unit:task/unlisted",
389
- )
390
-
391
- expect(execution?.readiness).toMatchObject({
392
- ready: false,
393
- blockers: [
394
- {
395
- kind: "validation",
396
- reason: "Epic does not list child task 'unlisted'",
397
- },
398
- ],
399
- })
400
- })
401
-
402
- test("adds body, workspace, git, and PR details only when requested", async () => {
403
- const root = await createWorkbase()
404
- roots.push(root)
405
- const baseline = await getGraph(root)
406
- for (const node of baseline.nodes) {
407
- expect("body" in node).toBe(false)
408
- expect("workspace" in node).toBe(false)
409
- expect("git" in node).toBe(false)
410
- expect("pr" in node).toBe(false)
411
- }
412
- expect(baseline.workbase.root).toBeUndefined()
413
-
414
- const detailed = await getGraph(root, {
415
- include: ["bodies", "workspace", "git", "pr"],
416
- backend: {
417
- kind: "git",
418
- inspectRepository: () => Effect.succeed(null),
419
- listWorkspaces: () => Effect.succeed([]),
420
- resolveRevision: () => Effect.succeed(null),
421
- workspaceHead: () => Effect.succeed(null),
422
- workspaceDirty: () => Effect.succeed(null),
423
- remoteUrl: () => Effect.succeed(null),
424
- } as unknown as VersionControlBackend,
425
- })
426
- expect(detailed.includes).toEqual(["bodies", "git", "pr", "workspace"])
427
- expect(detailed.workbase.root).toBe(root)
428
- const epic = detailed.nodes.find((node) => node.id === "epic:delivery")
429
- const repository = detailed.nodes.find(
430
- (node) => node.id === "repository:agency",
431
- )
432
- const execution = detailed.nodes.find(
433
- (node) => node.id === "execution-unit:phase/ship/implement",
434
- )
435
- if (epic?.kind !== "epic") throw new Error("Missing epic node")
436
- if (repository?.kind !== "repository") {
437
- throw new Error("Missing repository node")
438
- }
439
- if (execution?.kind !== "execution-unit") {
440
- throw new Error("Missing execution-unit node")
441
- }
442
- expect(epic?.body).toContain("Deliver the graph")
443
- expect(repository?.git).toMatchObject({ kind: null })
444
- expect(execution?.pr).toEqual({ url: null, state: "none" })
445
- expect(Schema.decodeUnknownSync(AgencyGraph)(detailed)).toEqual(detailed)
446
- })
447
-
448
- test("reuses repository metadata and revision lookups for git details", async () => {
449
- const root = await createWorkbase()
450
- roots.push(root)
451
- const calls = {
452
- listWorkspaces: 0,
453
- remoteUrl: 0,
454
- resolveRevision: new Map<string, number>(),
455
- }
456
- const backend = {
457
- kind: "git",
458
- inspectRepository: () => Effect.succeed(null),
459
- listWorkspaces: () =>
460
- Effect.sync(() => {
461
- calls.listWorkspaces += 1
462
- return []
463
- }),
464
- remoteUrl: () =>
465
- Effect.sync(() => {
466
- calls.remoteUrl += 1
467
- return null
468
- }),
469
- resolveRevision: (_path: string, revision: string) =>
470
- Effect.sync(() => {
471
- calls.resolveRevision.set(
472
- revision,
473
- (calls.resolveRevision.get(revision) ?? 0) + 1,
474
- )
475
- return revision
476
- }),
477
- workspaceHead: () => Effect.succeed(null),
478
- workspaceDirty: () => Effect.succeed(null),
479
- } as unknown as VersionControlBackend
480
-
481
- await getGraph(root, { include: ["git"], backend })
482
-
483
- expect(calls.listWorkspaces).toBe(0)
484
- expect(calls.remoteUrl).toBe(1)
485
- expect(calls.resolveRevision).toEqual(
486
- new Map([
487
- ["feat/prepare", 1],
488
- ["feat/implement", 1],
489
- ["feat/verify", 1],
490
- ["main", 1],
491
- ]),
492
- )
493
- })
494
- })