@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,103 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { mkdir } from "node:fs/promises"
3
- import { join } from "node:path"
4
- import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
5
- import { epic } from "./epic"
6
- import { phase } from "./phase"
7
- import { task } from "./task"
8
-
9
- describe("create command descriptions", () => {
10
- let root: string
11
-
12
- beforeEach(async () => {
13
- root = await createTempDir()
14
- await Bun.write(join(root, "agency.json"), '{"version":2}\n')
15
- await mkdir(join(root, "repos/agency"), { recursive: true })
16
- })
17
-
18
- afterEach(async () => cleanupTempDir(root))
19
-
20
- test("persists and exposes descriptions for every body-of-work shape", async () => {
21
- await runTestEffect(
22
- epic({
23
- subcommand: "create",
24
- args: ["rollout"],
25
- ticketUrl: "https://example.com/epic",
26
- description: "Coordinate the rollout.",
27
- repos: ["agency:main"],
28
- cwd: root,
29
- silent: true,
30
- }),
31
- )
32
- await runTestEffect(
33
- task({
34
- subcommand: "create",
35
- args: ["delivery"],
36
- ticketUrl: "https://example.com/task",
37
- description: "Deliver the rollout.",
38
- multiPhase: true,
39
- cwd: root,
40
- silent: true,
41
- }),
42
- )
43
- await runTestEffect(
44
- phase({
45
- subcommand: "create",
46
- args: ["delivery", "contract"],
47
- description: "Introduce the service contract.",
48
- repo: "agency",
49
- branch: "task/contract",
50
- base: "main",
51
- cwd: root,
52
- silent: true,
53
- }),
54
- )
55
-
56
- expect(
57
- await Bun.file(join(root, "epics/rollout/EPIC.md")).text(),
58
- ).toContain("description: Coordinate the rollout.")
59
- expect(
60
- await Bun.file(join(root, "tasks/delivery/TASK.md")).text(),
61
- ).toContain("description: Deliver the rollout.")
62
- expect(
63
- await Bun.file(
64
- join(root, "tasks/delivery/phases/contract/PHASE.md"),
65
- ).text(),
66
- ).toContain("description: Introduce the service contract.")
67
-
68
- const output: string[] = []
69
- const originalLog = console.log
70
- console.log = (message?: unknown) => output.push(String(message))
71
- try {
72
- await runTestEffect(
73
- epic({ subcommand: "list", args: [], cwd: root, json: true }),
74
- )
75
- await runTestEffect(
76
- task({
77
- subcommand: "show",
78
- args: ["delivery"],
79
- cwd: root,
80
- json: true,
81
- }),
82
- )
83
- await runTestEffect(
84
- phase({
85
- subcommand: "show",
86
- args: ["delivery", "contract"],
87
- cwd: root,
88
- json: true,
89
- }),
90
- )
91
- } finally {
92
- console.log = originalLog
93
- }
94
-
95
- expect(JSON.parse(output[0]!)[0].data.description).toBe(
96
- "Coordinate the rollout.",
97
- )
98
- expect(JSON.parse(output[1]!).data.description).toBe("Deliver the rollout.")
99
- expect(JSON.parse(output[2]!).data.description).toBe(
100
- "Introduce the service contract.",
101
- )
102
- })
103
- })
@@ -1,185 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { createHash } from "node:crypto"
3
- import { chmod, mkdir } from "node:fs/promises"
4
- import { dirname, join } from "node:path"
5
- import {
6
- captureLogs,
7
- cleanupTempDir,
8
- createTempDir,
9
- runTestEffect,
10
- } from "../test-utils"
11
- import { doctor } from "./doctor"
12
-
13
- const write = async (root: string, path: string, content: string) => {
14
- const fullPath = join(root, path)
15
- await mkdir(dirname(fullPath), { recursive: true })
16
- await Bun.write(fullPath, content)
17
- }
18
-
19
- const managedAgents = (body: string) =>
20
- `<!-- agency-managed: sha256=${createHash("sha256").update(body).digest("hex")} -->\n\n${body}`
21
-
22
- describe("doctor command", () => {
23
- let root: string
24
- let repository: string
25
-
26
- beforeEach(async () => {
27
- root = await createTempDir()
28
- repository = join(root, "source")
29
- await mkdir(repository)
30
- await Bun.$`git init -q -b main ${repository}`
31
- await Bun.$`git -C ${repository} config user.email test@example.com`
32
- await Bun.$`git -C ${repository} config user.name Test`
33
- await write(repository, "README.md", "test\n")
34
- await Bun.$`git -C ${repository} add README.md`
35
- await Bun.$`git -C ${repository} commit -q -m initial`
36
- await Bun.$`git -C ${repository} remote add origin https://example.com/agency.git`
37
- await write(
38
- root,
39
- "agency.json",
40
- JSON.stringify({
41
- version: 2,
42
- agents: {
43
- missing: {
44
- command: ["definitely-not-installed"],
45
- autoCommand: ["also-not-installed", "{prompt}"],
46
- },
47
- },
48
- }),
49
- )
50
- await mkdir(join(root, "repos"), { recursive: true })
51
- await Bun.$`ln -s ${repository} ${join(root, "repos/agency")}`
52
- await write(
53
- root,
54
- "tasks/example/TASK.md",
55
- `---
56
- ticketUrl: null
57
- repo: agency
58
- branch: feat/example
59
- base: main
60
- pr: null
61
- status: open
62
- ---
63
-
64
- # Example
65
- `,
66
- )
67
- })
68
-
69
- afterEach(async () => cleanupTempDir(root))
70
-
71
- test("returns stable checks, severities, and remediation in JSON", async () => {
72
- const logs = await captureLogs(() =>
73
- runTestEffect(doctor({ cwd: root, json: true })),
74
- )
75
- const report = JSON.parse(logs[0]!)
76
-
77
- expect(report).toMatchObject({
78
- version: 1,
79
- root,
80
- healthy: false,
81
- })
82
- expect(report.checks).toEqual(
83
- expect.arrayContaining([
84
- expect.objectContaining({
85
- id: "tool.git",
86
- level: "error",
87
- status: "pass",
88
- }),
89
- expect.objectContaining({
90
- id: "integration.agent.missing",
91
- level: "error",
92
- status: "fail",
93
- }),
94
- expect.objectContaining({
95
- id: "integration.agent.missing.auto",
96
- level: "error",
97
- status: "fail",
98
- }),
99
- expect.objectContaining({
100
- id: "ref.agency.main",
101
- status: "pass",
102
- }),
103
- ]),
104
- )
105
- for (const check of report.checks) {
106
- if (check.status === "fail") expect(check.remediation).toBeTruthy()
107
- }
108
- })
109
-
110
- test("reports repository, ref, remote, and managed-file failures", async () => {
111
- await Bun.$`git -C ${repository} remote remove origin`
112
- await Bun.$`git -C ${repository} branch -m other`
113
- await write(root, ".agency/AGENTS.md", managedAgents("old\n"))
114
-
115
- const logs = await captureLogs(() =>
116
- runTestEffect(doctor({ cwd: root, json: true })),
117
- )
118
- const checks = JSON.parse(logs[0]!).checks
119
-
120
- expect(checks).toEqual(
121
- expect.arrayContaining([
122
- expect.objectContaining({
123
- id: "repository.agency.remote",
124
- status: "fail",
125
- }),
126
- expect.objectContaining({
127
- id: "ref.agency.main",
128
- status: "fail",
129
- }),
130
- expect.objectContaining({
131
- id: "integration.file.agents",
132
- status: "fail",
133
- }),
134
- ]),
135
- )
136
- })
137
-
138
- test("warns when customized OpenCode config cannot guarantee access", async () => {
139
- const custom = '{"references":{}}\n'
140
- await write(root, ".opencode/opencode.jsonc", custom)
141
-
142
- const logs = await captureLogs(() =>
143
- runTestEffect(doctor({ cwd: root, json: true })),
144
- )
145
- const check = JSON.parse(logs[0]!).checks.find(
146
- (value: { id: string }) => value.id === "integration.file.opencode",
147
- )
148
-
149
- expect(check).toMatchObject({
150
- level: "warning",
151
- status: "fail",
152
- message: expect.stringContaining("cannot guarantee its instructions"),
153
- remediation: expect.stringContaining("global config"),
154
- })
155
- expect(await Bun.file(join(root, ".opencode/opencode.jsonc")).text()).toBe(
156
- custom,
157
- )
158
- })
159
-
160
- test("is safe when the workbase is read-only", async () => {
161
- for (const path of [
162
- join(root, "agency.json"),
163
- join(root, "tasks/example/TASK.md"),
164
- ]) {
165
- await chmod(path, 0o444)
166
- }
167
- await chmod(join(root, "tasks/example"), 0o555)
168
- await chmod(join(root, "tasks"), 0o555)
169
- await chmod(root, 0o555)
170
-
171
- try {
172
- await runTestEffect(doctor({ cwd: root, silent: true }))
173
- } finally {
174
- await chmod(root, 0o755)
175
- await chmod(join(root, "tasks"), 0o755)
176
- await chmod(join(root, "tasks/example"), 0o755)
177
- }
178
-
179
- expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
180
- expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
181
- expect(
182
- await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
183
- ).toBe(false)
184
- })
185
- })
@@ -1,196 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { mkdir } from "node:fs/promises"
3
- import { join } from "node:path"
4
- import { Effect } from "effect"
5
- import {
6
- captureLogs,
7
- cleanupTempDir,
8
- createTempDir,
9
- runTestEffect,
10
- } from "../test-utils"
11
- import { epic } from "./epic"
12
-
13
- describe("epic command", () => {
14
- let root: string
15
-
16
- beforeEach(async () => {
17
- root = await createTempDir()
18
- await Bun.write(join(root, "agency.json"), '{"version":2}\n')
19
- await mkdir(join(root, "repos/agency"), { recursive: true })
20
- })
21
-
22
- afterEach(async () => cleanupTempDir(root))
23
-
24
- test("requires create arguments", async () => {
25
- await expect(
26
- runTestEffect(epic({ subcommand: "create", args: [], silent: true })),
27
- ).rejects.toThrow("Usage: agency epic create")
28
- })
29
-
30
- test("outputs the created epic as JSON", async () => {
31
- const logs = await captureLogs(() =>
32
- runTestEffect(
33
- epic({
34
- subcommand: "create",
35
- args: ["example"],
36
- ticketUrl: "https://example.com/epic",
37
- repos: ["agency:main"],
38
- cwd: root,
39
- json: true,
40
- }),
41
- ),
42
- )
43
-
44
- const created = JSON.parse(logs[0]!)
45
- expect(created.revision).toMatch(/^[a-f0-9]{64}$/)
46
- expect(created).toEqual({
47
- id: "example",
48
- path: join(root, "epics/example/EPIC.md"),
49
- revision: created.revision,
50
- data: {
51
- ticketUrl: "https://example.com/epic",
52
- repos: [{ repo: "agency", ref: "main" }],
53
- tasks: [],
54
- },
55
- })
56
- })
57
-
58
- test("lists and shows stable epic JSON records", async () => {
59
- await runTestEffect(
60
- epic({
61
- subcommand: "create",
62
- args: ["example"],
63
- ticketUrl: "https://example.com/epic",
64
- repos: ["agency:main"],
65
- cwd: root,
66
- silent: true,
67
- }),
68
- )
69
-
70
- const listLogs = await captureLogs(() =>
71
- runTestEffect(
72
- epic({ subcommand: "list", args: [], cwd: root, json: true }),
73
- ),
74
- )
75
- const listed = JSON.parse(listLogs[0]!)
76
- expect(listed[0].revision).toMatch(/^[a-f0-9]{64}$/)
77
- expect(listed).toEqual([
78
- {
79
- id: "example",
80
- path: join(root, "epics/example/EPIC.md"),
81
- revision: listed[0].revision,
82
- data: {
83
- ticketUrl: "https://example.com/epic",
84
- repos: [{ repo: "agency", ref: "main" }],
85
- tasks: [],
86
- },
87
- },
88
- ])
89
-
90
- const showLogs = await captureLogs(() =>
91
- runTestEffect(
92
- epic({
93
- subcommand: "show",
94
- args: ["example"],
95
- cwd: root,
96
- json: true,
97
- }),
98
- ),
99
- )
100
- const shown = JSON.parse(showLogs[0]!)
101
- expect(shown.revision).toBe(listed[0].revision)
102
- expect(shown).toEqual({
103
- id: "example",
104
- path: join(root, "epics/example/EPIC.md"),
105
- revision: shown.revision,
106
- data: {
107
- ticketUrl: "https://example.com/epic",
108
- repos: [{ repo: "agency", ref: "main" }],
109
- tasks: [],
110
- },
111
- })
112
- })
113
-
114
- test("reads each epic document once when listing JSON", async () => {
115
- await runTestEffect(
116
- epic({
117
- subcommand: "create",
118
- args: ["example"],
119
- ticketUrl: "https://example.com/epic",
120
- repos: ["agency:main"],
121
- cwd: root,
122
- silent: true,
123
- }),
124
- )
125
- const original = Bun.file
126
- let reads = 0
127
- Bun.file = ((path: Parameters<typeof Bun.file>[0], ...args: never[]) => {
128
- if (String(path).endsWith("/epics/example/EPIC.md")) reads += 1
129
- return original(path as unknown as string, ...args)
130
- }) as typeof Bun.file
131
- try {
132
- await captureLogs(() =>
133
- runTestEffect(
134
- epic({ subcommand: "list", args: [], cwd: root, json: true }),
135
- ),
136
- )
137
- } finally {
138
- Bun.file = original
139
- }
140
- expect(reads).toBe(2)
141
- })
142
-
143
- test("renders a readable operational table", async () => {
144
- await runTestEffect(
145
- epic({
146
- subcommand: "create",
147
- args: ["example"],
148
- ticketUrl: "https://example.com/epic",
149
- repos: ["agency:main"],
150
- cwd: root,
151
- silent: true,
152
- }),
153
- )
154
-
155
- const logs = await captureLogs(() =>
156
- runTestEffect(epic({ subcommand: "list", args: [], cwd: root })),
157
- )
158
- expect(logs[0]).toContain(
159
- "EPIC STATUS READINESS REPOSITORIES PR WORKTREE",
160
- )
161
- expect(logs[0]).toContain("example open waiting agency")
162
- expect(logs[0]).not.toMatch(/[\u{e000}-\u{f8ff}]/u)
163
- })
164
-
165
- test("starts work on a newly created epic", async () => {
166
- const launches: unknown[] = []
167
-
168
- await runTestEffect(
169
- epic(
170
- {
171
- subcommand: "new",
172
- args: ["immediate"],
173
- ticketUrl: "https://example.com/immediate",
174
- repos: ["agency:main"],
175
- work: true,
176
- auto: true,
177
- cwd: root,
178
- silent: true,
179
- },
180
- (options) =>
181
- Effect.sync(() => {
182
- launches.push(options)
183
- return undefined
184
- }),
185
- ),
186
- )
187
-
188
- expect(launches).toEqual([
189
- expect.objectContaining({
190
- epicId: "immediate",
191
- auto: true,
192
- cwd: root,
193
- }),
194
- ])
195
- })
196
- })
@@ -1,117 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { stat } from "node:fs/promises"
3
- import { join } from "node:path"
4
- import {
5
- captureLogs,
6
- cleanupTempDir,
7
- createTempDir,
8
- runTestEffect,
9
- } from "../test-utils"
10
- import { init } from "./init"
11
-
12
- describe("init command", () => {
13
- let parent: string
14
-
15
- beforeEach(async () => {
16
- parent = await createTempDir()
17
- })
18
-
19
- afterEach(async () => {
20
- await cleanupTempDir(parent)
21
- })
22
-
23
- test("creates a workbase and required directories", async () => {
24
- const root = join(parent, "workbase")
25
- await runTestEffect(init({ path: root, silent: true }))
26
-
27
- expect(await Bun.file(join(root, "agency.json")).json()).toEqual({
28
- version: 2,
29
- vcs: "git",
30
- })
31
- for (const directory of ["repos", "epics", "tasks"]) {
32
- expect((await stat(join(root, directory))).isDirectory()).toBe(true)
33
- }
34
- expect(await Bun.file(join(root, ".gitignore")).text()).toBe(
35
- "/repos/\n/tasks/*/code/\n/tasks/*/phases/*/code/\n",
36
- )
37
- expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
38
- const agents = await Bun.file(join(root, ".agency/AGENTS.md")).text()
39
- expect(agents).toContain("agency context . --json")
40
- expect(agents).toContain("## Consent Boundaries")
41
- expect(agents).toContain("## Execution")
42
- expect(agents).not.toContain("SKILL.md")
43
- const opencode = await Bun.file(
44
- join(root, ".opencode/opencode.jsonc"),
45
- ).text()
46
- const config = JSON.parse(opencode.slice(opencode.indexOf("\n\n") + 2))
47
- expect(config.instructions).toEqual([".agency/AGENTS.md"])
48
- expect(config.agent.agency).toBeUndefined()
49
- expect(config.agent.plan).toEqual({ disable: true })
50
- expect(config.agent["agency-plan"]).toMatchObject({
51
- mode: "primary",
52
- permission: {
53
- bash: {
54
- "agency *": "allow",
55
- },
56
- edit: {
57
- "*": "deny",
58
- "tasks/*/TASK.md": "allow",
59
- "tasks/*/phases/*/PHASE.md": "allow",
60
- "epics/*/EPIC.md": "allow",
61
- },
62
- },
63
- })
64
- expect(config.references).toEqual({
65
- workbase: expect.objectContaining({ path: ".." }),
66
- })
67
- expect(config.permission).toBeUndefined()
68
- expect(
69
- await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
70
- ).toBe(false)
71
- const plugin = await Bun.file(
72
- join(root, ".opencode/plugins/agency-repository-skills.ts"),
73
- ).text()
74
- expect(plugin).toContain("AGENCY_WRITABLE_CHECKOUT")
75
- expect(plugin).toContain("config.skills.paths")
76
- const tui = await Bun.file(join(root, ".opencode/tui.jsonc")).text()
77
- expect(JSON.parse(tui.slice(tui.indexOf("\n\n") + 2))).toEqual({
78
- $schema: "https://opencode.ai/tui.json",
79
- plugin: ["./tui/agency-debug.ts"],
80
- })
81
- const tuiPlugin = await Bun.file(
82
- join(root, ".opencode/tui/agency-debug.ts"),
83
- ).text()
84
- expect(tuiPlugin).toContain('slashName: "agency-debug"')
85
- expect(tuiPlugin).toContain("api.ui.toast")
86
- expect(tuiPlugin).not.toContain("chat.message")
87
- expect(
88
- await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).exists(),
89
- ).toBe(false)
90
- })
91
-
92
- test("preserves existing gitignore entries", async () => {
93
- await Bun.write(join(parent, ".gitignore"), "custom\n")
94
- await runTestEffect(init({ path: parent, silent: true }))
95
-
96
- expect(await Bun.file(join(parent, ".gitignore")).text()).toStartWith(
97
- "custom\n",
98
- )
99
- })
100
-
101
- test("outputs the initialized workbase as JSON", async () => {
102
- const root = join(parent, "json-workbase")
103
- const logs = await captureLogs(() =>
104
- runTestEffect(init({ path: root, json: true })),
105
- )
106
-
107
- expect(JSON.parse(logs[0]!)).toEqual({ root })
108
- })
109
-
110
- test("rejects an existing Agency configuration", async () => {
111
- await Bun.write(join(parent, "agency.json"), '{"version":2}\n')
112
-
113
- await expect(
114
- runTestEffect(init({ path: parent, silent: true })),
115
- ).rejects.toThrow("already exists")
116
- })
117
- })