@markjaquith/agency 3.2.14 → 3.3.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 (75) hide show
  1. package/README.md +20 -3
  2. package/cli-main.ts +1 -0
  3. package/package.json +7 -2
  4. package/src/cli-parser.ts +13 -0
  5. package/src/commands/work.ts +70 -2
  6. package/src/services/ReadinessService.ts +20 -0
  7. package/src/workbase/execution-contract.ts +10 -1
  8. package/src/workbase/schemas.ts +12 -0
  9. package/fixtures/protocol/orchestration-recipes.json +0 -53
  10. package/pi-extensions/agency.test.ts +0 -117
  11. package/src/check-commits.test.ts +0 -58
  12. package/src/cli-parser.test.ts +0 -945
  13. package/src/cli.test.ts +0 -1889
  14. package/src/commands/act.test.ts +0 -418
  15. package/src/commands/archive.test.ts +0 -236
  16. package/src/commands/context.test.ts +0 -711
  17. package/src/commands/description.test.ts +0 -103
  18. package/src/commands/doctor.test.ts +0 -185
  19. package/src/commands/epic.test.ts +0 -196
  20. package/src/commands/init.test.ts +0 -117
  21. package/src/commands/integration.test.ts +0 -165
  22. package/src/commands/pr.test.ts +0 -248
  23. package/src/commands/push.test.ts +0 -56
  24. package/src/commands/read-only.test.ts +0 -161
  25. package/src/commands/repo.test.ts +0 -199
  26. package/src/commands/restore.test.ts +0 -106
  27. package/src/commands/status.test.ts +0 -113
  28. package/src/commands/sync.test.ts +0 -200
  29. package/src/commands/task-phase.test.ts +0 -410
  30. package/src/commands/task.test.ts +0 -281
  31. package/src/commands/validate.test.ts +0 -186
  32. package/src/commands/work.test.ts +0 -1375
  33. package/src/commands/workbase.test.ts +0 -170
  34. package/src/graph-schema.test.ts +0 -124
  35. package/src/protocol.test.ts +0 -163
  36. package/src/readiness.test.ts +0 -105
  37. package/src/services/ArchiveBulkService.test.ts +0 -384
  38. package/src/services/ArchiveService.test.ts +0 -1092
  39. package/src/services/EpicService.test.ts +0 -74
  40. package/src/services/FileSystemService.test.ts +0 -81
  41. package/src/services/GraphMutationService.test.ts +0 -486
  42. package/src/services/GraphService.test.ts +0 -494
  43. package/src/services/IntegrationService.test.ts +0 -1091
  44. package/src/services/LifecycleTransaction.test.ts +0 -145
  45. package/src/services/PullRequestService.test.ts +0 -716
  46. package/src/services/PushService.test.ts +0 -408
  47. package/src/services/ReadinessService.test.ts +0 -290
  48. package/src/services/RepositoryService.test.ts +0 -789
  49. package/src/services/ReviewService.test.ts +0 -408
  50. package/src/services/SyncService.test.ts +0 -1377
  51. package/src/services/TaskPhaseService.test.ts +0 -852
  52. package/src/services/VersionControlService.test.ts +0 -62
  53. package/src/services/WorkbaseService.test.ts +0 -910
  54. package/src/services/WorktreeLock.test.ts +0 -205
  55. package/src/services/WorktreePerformance.test.ts +0 -71
  56. package/src/services/WorktreeService.test.ts +0 -2292
  57. package/src/test-utils.ts +0 -110
  58. package/src/usage-log.test.ts +0 -188
  59. package/src/utils/chooser.test.ts +0 -192
  60. package/src/utils/effect.test.ts +0 -30
  61. package/src/utils/interactive.pty.test.ts +0 -128
  62. package/src/utils/interactive.test.tsx +0 -860
  63. package/src/utils/process.test.ts +0 -132
  64. package/src/utils/progress.test.ts +0 -37
  65. package/src/work-view.test.ts +0 -151
  66. package/src/workbase/agent-command.test.ts +0 -128
  67. package/src/workbase/checkout-command.test.ts +0 -62
  68. package/src/workbase/delivery-command.test.ts +0 -180
  69. package/src/workbase/dependency-graph.test.ts +0 -50
  70. package/src/workbase/execution-contract.test.ts +0 -161
  71. package/src/workbase/frontmatter.test.ts +0 -67
  72. package/src/workbase/repository-reference.test.ts +0 -25
  73. package/src/workbase/schemas.test.ts +0 -543
  74. package/src/workbase/work-target.test.ts +0 -108
  75. package/src/workbase/worktree-command.test.ts +0 -61
@@ -1,711 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
2
- import { mkdir, readFile as nodeReadFile, rename, rm } from "node:fs/promises"
3
- import { dirname, join } from "node:path"
4
- import { Effect } from "effect"
5
- import {
6
- captureLogs,
7
- cleanupTempDir,
8
- createTempDir,
9
- runTestEffect,
10
- trackDocumentReadConcurrency,
11
- } from "../test-utils"
12
- import { documentLoadConcurrency } from "../workbase/document-loading"
13
- import { ContextService } from "../services/ContextService"
14
- import { FileSystemService } from "../services/FileSystemService"
15
- import { context } from "./context"
16
-
17
- const write = async (root: string, path: string, content: string) => {
18
- const fullPath = join(root, path)
19
- await mkdir(dirname(fullPath), { recursive: true })
20
- await Bun.write(fullPath, content)
21
- }
22
-
23
- const run = async (cwd: string, args: string[]) => {
24
- const process = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" })
25
- const [exitCode, stderr] = await Promise.all([
26
- process.exited,
27
- new Response(process.stderr).text(),
28
- ])
29
- if (exitCode !== 0) throw new Error(`${args.join(" ")}: ${stderr}`)
30
- }
31
-
32
- const readContext = async (
33
- root: string,
34
- target: string | undefined,
35
- compact = false,
36
- full = false,
37
- ) => {
38
- const logs = await captureLogs(() =>
39
- runTestEffect(context({ cwd: root, target, compact, full, json: true })),
40
- )
41
- expect(logs).toHaveLength(1)
42
- return JSON.parse(logs[0]!)
43
- }
44
-
45
- describe("context", () => {
46
- let root: string
47
-
48
- beforeEach(async () => {
49
- root = await createTempDir()
50
- await write(root, "agency.json", '{"version":2}\n')
51
- for (const repo of ["agency", "docs"]) {
52
- const path = join(root, "repos", repo)
53
- await mkdir(path, { recursive: true })
54
- await run(root, ["git", "init", "--initial-branch=main", path])
55
- await run(path, ["git", "config", "user.email", "test@example.com"])
56
- await run(path, ["git", "config", "user.name", "Test"])
57
- await Bun.write(join(path, "README.md"), `${repo}\n`)
58
- await run(path, ["git", "add", "README.md"])
59
- await run(path, ["git", "commit", "-m", "initial"])
60
- }
61
-
62
- await write(
63
- root,
64
- "epics/contract/EPIC.md",
65
- `---
66
- ticketUrl: https://example.com/contract
67
- repos:
68
- - repo: agency
69
- ref: main
70
- tasks:
71
- - id: foundations
72
- - id: agent-contract
73
- dependsOn: [foundations]
74
- ---
75
-
76
- # Contract
77
- `,
78
- )
79
- await write(
80
- root,
81
- "tasks/foundations/TASK.md",
82
- `---
83
- ticketUrl: null
84
- epic: contract
85
- repo: agency
86
- branch: foundations
87
- base: main
88
- pr: https://github.com/example/agency/pull/1
89
- status: done
90
- ---
91
-
92
- # Foundations
93
- `,
94
- )
95
- await write(
96
- root,
97
- "tasks/agent-contract/TASK.md",
98
- `---
99
- ticketUrl: null
100
- epic: contract
101
- phases:
102
- - id: schema
103
- - id: context-command
104
- dependsOn: [schema]
105
- ---
106
-
107
- # Agent contract
108
- Task prose.
109
- `,
110
- )
111
- await write(
112
- root,
113
- "tasks/agent-contract/phases/schema/PHASE.md",
114
- `---
115
- repo: agency
116
- branch: schema
117
- base: main
118
- pr: https://github.com/example/agency/pull/2
119
- status: done
120
- ---
121
-
122
- # Schema
123
- `,
124
- )
125
- await write(
126
- root,
127
- "tasks/agent-contract/phases/context-command/PHASE.md",
128
- `---
129
- repo: agency
130
- repos:
131
- - repo: docs
132
- ref: main
133
- branch: feat/context
134
- base: main
135
- pr: null
136
- status: open
137
- ---
138
-
139
- # Context command
140
- Phase prose.
141
- `,
142
- )
143
-
144
- const code = join(root, "tasks/agent-contract/phases/context-command/code")
145
- await mkdir(code, { recursive: true })
146
- await run(join(root, "repos/agency"), [
147
- "git",
148
- "worktree",
149
- "add",
150
- "-b",
151
- "feat/context",
152
- join(code, "agency"),
153
- "main",
154
- ])
155
- await run(join(root, "repos/docs"), [
156
- "git",
157
- "worktree",
158
- "add",
159
- "--detach",
160
- join(code, "docs"),
161
- "main",
162
- ])
163
- })
164
-
165
- test("bounds document reads across nested task and phase traversal", async () => {
166
- await Promise.all(
167
- Array.from({ length: documentLoadConcurrency + 8 }, (_, index) =>
168
- write(
169
- root,
170
- `tasks/agent-contract/phases/extra-${index}/PHASE.md`,
171
- `---\nrepo: agency\nbranch: extra-${index}\nbase: main\npr: null\nstatus: open\n---\n`,
172
- ),
173
- ),
174
- )
175
- const fs = await Effect.runPromise(
176
- FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
177
- )
178
- const tracked = trackDocumentReadConcurrency(fs)
179
-
180
- await runTestEffect(
181
- ContextService.pipe(
182
- Effect.flatMap((service) =>
183
- service.get({ cwd: root, target: "foundations" }),
184
- ),
185
- Effect.provideService(FileSystemService, tracked.fs),
186
- ),
187
- )
188
-
189
- expect(tracked.maximum()).toBe(documentLoadConcurrency)
190
- })
191
-
192
- afterEach(async () => {
193
- await cleanupTempDir(root)
194
- })
195
-
196
- test("returns full phase context with graph, authority, Git, and validation state", async () => {
197
- const target = "tasks/agent-contract/phases/context-command"
198
- const result = await readContext(root, target, false, true)
199
-
200
- expect(result).toMatchObject({
201
- projection: "complete",
202
- workbase: { root, version: 2 },
203
- target: {
204
- kind: "phase",
205
- taskId: "agent-contract",
206
- phaseId: "context-command",
207
- },
208
- graph: {
209
- parent: { kind: "task", id: "agent-contract" },
210
- dependencies: ["schema"],
211
- readiness: { ready: true, blocked: false, blockers: [] },
212
- aggregate: { status: "open", total: 1, open: 1 },
213
- },
214
- authority: {
215
- mode: "execution",
216
- writable: { repo: "agency", branch: "feat/context", base: "main" },
217
- references: [{ repo: "docs", ref: "main" }],
218
- },
219
- workspace: {
220
- materialization: "complete",
221
- writable: {
222
- materialized: true,
223
- registered: true,
224
- checkoutBranch: "feat/context",
225
- detached: false,
226
- },
227
- references: [
228
- {
229
- repo: "docs",
230
- materialized: true,
231
- registered: true,
232
- detached: true,
233
- },
234
- ],
235
- },
236
- pr: { url: null, state: "none" },
237
- validation: { valid: true, warnings: [] },
238
- })
239
- expect(result.documents.epic.body).toContain("# Contract")
240
- expect(result.documents.task.body).toContain("Task prose.")
241
- expect(result.documents.phase.body).toContain("Phase prose.")
242
- expect(result.documents.phase.sha256).toMatch(/^[a-f0-9]{64}$/)
243
- expect(result.authority.documents.writable).toEqual([
244
- join(root, "tasks/agent-contract/TASK.md"),
245
- join(root, "tasks/agent-contract/phases/context-command/PHASE.md"),
246
- ])
247
- expect(result.workspace.writable.branchCommit).toMatch(/^[a-f0-9]{40}$/)
248
- expect(result.workspace.writable.baseCommit).toMatch(/^[a-f0-9]{40}$/)
249
- expect(result.workspace.references[0].resolvedCommit).toMatch(
250
- /^[a-f0-9]{40}$/,
251
- )
252
- })
253
-
254
- test("defaults to compact while preserving explicit compact compatibility", async () => {
255
- const result = await readContext(
256
- root,
257
- "tasks/agent-contract/phases/context-command/code/agency",
258
- )
259
- const explicit = await readContext(
260
- root,
261
- "tasks/agent-contract/phases/context-command/code/agency",
262
- true,
263
- )
264
-
265
- expect(result.projection).toBe("compact")
266
- expect(explicit).toEqual(result)
267
- expect(result.target.kind).toBe("phase")
268
- expect(result.documents.phase.body).toBeUndefined()
269
- expect(result.documents.phase.data.branch).toBe("feat/context")
270
- expect(result.documents.phase.sha256).toMatch(/^[a-f0-9]{64}$/)
271
- expect(result.workspace.writable).toEqual({
272
- materialized: true,
273
- registered: true,
274
- })
275
- expect(result.authority.writable.checkoutPath).toContain("code/agency")
276
- expect(result.authority.documents.writable).toEqual([
277
- join(root, "tasks/agent-contract/TASK.md"),
278
- join(root, "tasks/agent-contract/phases/context-command/PHASE.md"),
279
- ])
280
- })
281
-
282
- test("reports dependency and validation blockers deterministically", async () => {
283
- await write(
284
- root,
285
- "tasks/agent-contract/phases/schema/PHASE.md",
286
- `---
287
- repo: missing
288
- branch: schema
289
- base: main
290
- pr: null
291
- status: dropped
292
- ---
293
-
294
- # Schema
295
- `,
296
- )
297
- const result = await readContext(
298
- root,
299
- "tasks/agent-contract/phases/context-command",
300
- )
301
-
302
- expect(result.graph.readiness.ready).toBe(false)
303
- expect(result.graph.readiness.blockers).toContainEqual({
304
- kind: "dependency",
305
- id: "schema",
306
- status: "dropped",
307
- reason: "Phase dependency is dropped",
308
- })
309
- expect(result.validation.valid).toBe(false)
310
- expect(result.validation.warnings).toContainEqual({
311
- path: "tasks/agent-contract/phases/schema/PHASE.md",
312
- message: "Unknown repository alias 'missing'",
313
- })
314
- })
315
-
316
- test("reads each workbase document at most once per invocation", async () => {
317
- const unrelatedArchive = join(root, "archive/tasks/unrelated/TASK.md")
318
- await write(
319
- root,
320
- "archive/tasks/unrelated/TASK.md",
321
- `---
322
- ticketUrl: null
323
- repo: agency
324
- branch: unrelated
325
- base: main
326
- pr: null
327
- status: done
328
- ---
329
- `,
330
- )
331
- const reads = new Map<string, number>()
332
- const originalFile = Bun.file
333
- Bun.file = mock((path: string) => {
334
- const file = originalFile(path)
335
- return Object.assign(file, {
336
- text: async () => {
337
- reads.set(path, (reads.get(path) ?? 0) + 1)
338
- return nodeReadFile(path, "utf8")
339
- },
340
- })
341
- }) as unknown as typeof Bun.file
342
- try {
343
- await readContext(root, "tasks/agent-contract/phases/context-command")
344
- } finally {
345
- Bun.file = originalFile
346
- }
347
-
348
- const documents = [...reads].filter(([path]) =>
349
- /(?:EPIC|TASK|PHASE)\.md$/.test(path),
350
- )
351
- expect(documents.length).toBeGreaterThan(0)
352
- expect(documents.every(([, count]) => count === 1)).toBe(true)
353
- expect(reads.has(unrelatedArchive)).toBe(false)
354
- })
355
-
356
- test("limits repository inspection and overlaps independent Git observations", async () => {
357
- const commands: string[][] = []
358
- let activeGitCommands = 0
359
- let maxActiveGitCommands = 0
360
- const originalSpawn = Bun.spawn
361
- ;(Bun as { spawn: unknown }).spawn = mock(((
362
- args: string[],
363
- options: Parameters<typeof Bun.spawn>[1],
364
- ) => {
365
- const command = [...args]
366
- const child = originalSpawn(command, options)
367
- if (command[0] !== "git") return child
368
- commands.push(command)
369
- activeGitCommands += 1
370
- maxActiveGitCommands = Math.max(maxActiveGitCommands, activeGitCommands)
371
- const exited = child.exited.finally(() => {
372
- activeGitCommands -= 1
373
- })
374
- return new Proxy(child, {
375
- get(target, property) {
376
- return property === "exited"
377
- ? exited
378
- : Reflect.get(target, property, target)
379
- },
380
- })
381
- }) as unknown as typeof Bun.spawn)
382
- try {
383
- await readContext(root, "tasks/agent-contract/phases/context-command")
384
- } finally {
385
- ;(Bun as { spawn: unknown }).spawn = originalSpawn
386
- }
387
-
388
- expect(
389
- commands.some(
390
- (command) =>
391
- command.includes("config") && command.includes("--show-origin"),
392
- ),
393
- ).toBe(false)
394
- expect(maxActiveGitCommands).toBeGreaterThanOrEqual(3)
395
- })
396
-
397
- test("does not load unrelated archived documents for an active target", async () => {
398
- await write(
399
- root,
400
- "archive/tasks/old-task/TASK.md",
401
- `---
402
- ticketUrl: null
403
- repo: agency
404
- branch: old-task
405
- base: main
406
- pr: null
407
- status: done
408
- ---
409
-
410
- # Old task
411
- `,
412
- )
413
- await write(
414
- root,
415
- "archive/tasks/old-task/phases/old-phase/PHASE.md",
416
- `---
417
- repo: agency
418
- branch: old-phase
419
- base: main
420
- pr: null
421
- status: done
422
- ---
423
-
424
- # Old phase
425
- `,
426
- )
427
-
428
- const archiveReads: string[] = []
429
- const originalFile = Bun.file
430
- Bun.file = mock((path: string) => {
431
- if (path.includes(join(root, "archive", "tasks"))) archiveReads.push(path)
432
- return originalFile(path)
433
- }) as unknown as typeof Bun.file
434
- try {
435
- await readContext(root, "tasks/agent-contract/phases/context-command")
436
- } finally {
437
- Bun.file = originalFile
438
- }
439
-
440
- expect(archiveReads).toEqual([])
441
- })
442
-
443
- test("resolves a bare task ID and returns root discovery context", async () => {
444
- const task = await readContext(root, "agent-contract")
445
- expect(task.target).toMatchObject({
446
- kind: "task",
447
- taskId: "agent-contract",
448
- })
449
- expect(task.graph.parent).toEqual({ kind: "epic", id: "contract" })
450
- expect(task.authority.documents.writable).toEqual([])
451
-
452
- const executionTask = await readContext(root, "foundations")
453
- expect(executionTask.authority.documents.writable).toEqual([
454
- join(root, "tasks/foundations/TASK.md"),
455
- ])
456
-
457
- const workbase = await readContext(root, ".")
458
- expect(workbase).toMatchObject({
459
- projection: "compact",
460
- workbase: { root, version: 2 },
461
- target: { kind: "workbase", path: root },
462
- hint: "Run agency context . --full --json to include document prose.",
463
- authority: {
464
- mode: "orchestration",
465
- writable: null,
466
- references: [],
467
- },
468
- validation: { valid: true, warnings: [] },
469
- })
470
- expect(
471
- workbase.discovery.epics.map((item: { id: string }) => item.id),
472
- ).toEqual(["contract"])
473
- expect(
474
- workbase.discovery.tasks.map((item: { id: string }) => item.id),
475
- ).toEqual(["agent-contract", "foundations"])
476
- expect(
477
- workbase.discovery.phases.map(
478
- (item: { taskId: string; id: string }) => `${item.taskId}/${item.id}`,
479
- ),
480
- ).toEqual(["agent-contract/context-command", "agent-contract/schema"])
481
- expect(workbase.discovery.tasks[0].body).toBeUndefined()
482
- expect(workbase.discovery.phases[0].data.status).toBe("open")
483
- expect(workbase.discovery.phases[0].sha256).toMatch(/^[a-f0-9]{64}$/)
484
- })
485
-
486
- test("includes discovery prose only in full root context", async () => {
487
- const workbase = await readContext(root, ".", false, true)
488
- expect(workbase.projection).toBe("complete")
489
- expect(workbase.hint).toBeNull()
490
- expect(workbase.discovery.epics[0].body).toContain("# Contract")
491
- expect(workbase.discovery.tasks[0].body).toContain("Task prose.")
492
- expect(workbase.discovery.tasks[0].data.phases).toBeDefined()
493
- expect(workbase.discovery.phases[0].taskId).toBe("agent-contract")
494
- })
495
-
496
- test("resolves bare task IDs from inside another target", async () => {
497
- const cwd = join(
498
- root,
499
- "tasks/agent-contract/phases/context-command/code/agency",
500
- )
501
- const logs = await captureLogs(() =>
502
- runTestEffect(context({ cwd, target: "foundations", json: true })),
503
- )
504
- const result = JSON.parse(logs[0]!)
505
- expect(result.target).toMatchObject({ kind: "task", taskId: "foundations" })
506
- expect(result.authority.writable.branch).toBe("foundations")
507
- })
508
-
509
- test("resolves archived tasks without granting mutation or execution authority", async () => {
510
- await mkdir(join(root, "archive/tasks"), { recursive: true })
511
- await rename(
512
- join(root, "tasks/foundations"),
513
- join(root, "archive/tasks/foundations"),
514
- )
515
-
516
- const result = await readContext(root, "foundations", false, true)
517
-
518
- expect(result.target).toMatchObject({
519
- kind: "task",
520
- taskId: "foundations",
521
- archived: true,
522
- path: join(root, "archive/tasks/foundations/TASK.md"),
523
- })
524
- expect(result.documents.task.body).toContain("# Foundations")
525
- expect(result.authority).toMatchObject({
526
- mode: "orchestration",
527
- writable: null,
528
- references: [],
529
- documents: { writable: [] },
530
- })
531
- expect(result.workspace).toMatchObject({
532
- materialization: "absent",
533
- writable: null,
534
- references: [],
535
- })
536
- expect(result.graph.readiness).toMatchObject({
537
- ready: false,
538
- blocked: true,
539
- })
540
- expect(result.graph.readiness.blockers).toContainEqual({
541
- kind: "status",
542
- id: "foundations",
543
- reason: "Target is archived; restore it before mutation or execution",
544
- })
545
- expect(result.pr).toMatchObject({
546
- url: "https://github.com/example/agency/pull/1",
547
- state: "open",
548
- })
549
- })
550
-
551
- test("resolves archived phases with their active parent context", async () => {
552
- await mkdir(join(root, "archive/tasks/agent-contract/phases"), {
553
- recursive: true,
554
- })
555
- await rename(
556
- join(root, "tasks/agent-contract/phases/context-command"),
557
- join(root, "archive/tasks/agent-contract/phases/context-command"),
558
- )
559
-
560
- const result = await readContext(
561
- root,
562
- "archive/tasks/agent-contract/phases/context-command",
563
- false,
564
- true,
565
- )
566
-
567
- expect(result.target).toMatchObject({
568
- kind: "phase",
569
- taskId: "agent-contract",
570
- phaseId: "context-command",
571
- archived: true,
572
- path: join(
573
- root,
574
- "archive/tasks/agent-contract/phases/context-command/PHASE.md",
575
- ),
576
- })
577
- expect(result.documents.task.body).toContain("Task prose.")
578
- expect(result.documents.phase.body).toContain("Phase prose.")
579
- expect(result.graph.parent).toEqual({ kind: "task", id: "agent-contract" })
580
- expect(result.graph.readiness).toMatchObject({
581
- ready: false,
582
- blocked: true,
583
- })
584
- expect(result.authority).toMatchObject({
585
- mode: "orchestration",
586
- writable: null,
587
- references: [],
588
- documents: { writable: [] },
589
- })
590
- })
591
-
592
- test("resolves archived epics with archived descendant graph context", async () => {
593
- await mkdir(join(root, "archive/epics"), { recursive: true })
594
- await mkdir(join(root, "archive/tasks"), { recursive: true })
595
- await rename(
596
- join(root, "epics/contract"),
597
- join(root, "archive/epics/contract"),
598
- )
599
- for (const taskId of ["agent-contract", "foundations"]) {
600
- await rename(
601
- join(root, "tasks", taskId),
602
- join(root, "archive/tasks", taskId),
603
- )
604
- }
605
-
606
- const result = await readContext(root, "epics/contract", false, true)
607
-
608
- expect(result.target).toMatchObject({
609
- kind: "epic",
610
- epicId: "contract",
611
- archived: true,
612
- path: join(root, "archive/epics/contract/EPIC.md"),
613
- })
614
- expect(result.documents.epic.body).toContain("# Contract")
615
- expect(result.graph.aggregate).toMatchObject({ total: 3, done: 2, open: 1 })
616
- expect(result.graph.readiness).toMatchObject({
617
- ready: false,
618
- blocked: true,
619
- })
620
- expect(result.authority).toMatchObject({
621
- mode: "orchestration",
622
- writable: null,
623
- references: [],
624
- documents: { writable: [] },
625
- })
626
- })
627
-
628
- test("computes orchestration readiness from runnable descendants", async () => {
629
- await write(
630
- root,
631
- "tasks/agent-contract/TASK.md",
632
- `---
633
- ticketUrl: null
634
- epic: contract
635
- phases:
636
- - id: schema
637
- - id: context-command
638
- dependsOn: [schema]
639
- - id: parallel
640
- ---
641
-
642
- # Agent contract
643
- `,
644
- )
645
- await write(
646
- root,
647
- "tasks/agent-contract/phases/schema/PHASE.md",
648
- `---
649
- repo: agency
650
- branch: schema
651
- base: main
652
- pr: null
653
- status: working
654
- ---
655
-
656
- # Schema
657
- `,
658
- )
659
- await write(
660
- root,
661
- "tasks/agent-contract/phases/parallel/PHASE.md",
662
- `---
663
- repo: docs
664
- branch: parallel
665
- base: main
666
- pr: null
667
- status: open
668
- ---
669
-
670
- # Parallel
671
- `,
672
- )
673
-
674
- const task = await readContext(root, "agent-contract")
675
- expect(task.graph.readiness).toMatchObject({ ready: true, blocked: false })
676
- expect(task.graph.aggregate).toMatchObject({
677
- status: "working",
678
- total: 3,
679
- open: 2,
680
- working: 1,
681
- })
682
-
683
- const epic = await readContext(root, "epics/contract")
684
- expect(epic.graph.readiness).toMatchObject({ ready: true, blocked: false })
685
- expect(epic.graph.aggregate).toMatchObject({
686
- status: "working",
687
- total: 4,
688
- done: 1,
689
- open: 2,
690
- working: 1,
691
- })
692
- })
693
-
694
- test("reports stale worktree registration independently of materialization", async () => {
695
- const checkout = join(
696
- root,
697
- "tasks/agent-contract/phases/context-command/code/agency",
698
- )
699
- await rm(checkout, { recursive: true, force: true })
700
-
701
- const result = await readContext(
702
- root,
703
- "tasks/agent-contract/phases/context-command",
704
- )
705
- expect(result.workspace.writable).toMatchObject({
706
- materialized: false,
707
- registered: true,
708
- })
709
- expect(result.workspace.materialization).toBe("partial")
710
- })
711
- })