@markjaquith/agency 1.11.0 → 2.0.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 (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +172 -419
  24. package/src/commands/work.ts +47 -166
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -518
  73. package/src/commands/push.ts +0 -434
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1183
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
@@ -0,0 +1,80 @@
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 {
5
+ captureLogs,
6
+ cleanupTempDir,
7
+ createTempDir,
8
+ runTestEffect,
9
+ } from "../test-utils"
10
+ import { repo } from "./repo"
11
+
12
+ describe("repo command", () => {
13
+ let root: string
14
+
15
+ beforeEach(async () => {
16
+ root = await createTempDir()
17
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
18
+ await mkdir(join(root, "repos/agency"), { recursive: true })
19
+ })
20
+
21
+ afterEach(async () => cleanupTempDir(root))
22
+
23
+ test("requires a subcommand", async () => {
24
+ await expect(
25
+ runTestEffect(repo({ args: [], silent: true })),
26
+ ).rejects.toThrow("Available subcommands: add, link, list")
27
+ })
28
+
29
+ test("requires add arguments", async () => {
30
+ await expect(
31
+ runTestEffect(
32
+ repo({ subcommand: "add", args: ["agency"], silent: true }),
33
+ ),
34
+ ).rejects.toThrow("Usage: agency repo add")
35
+ })
36
+
37
+ test("lists repository metadata as JSON", async () => {
38
+ const logs = await captureLogs(() =>
39
+ runTestEffect(
40
+ repo({ subcommand: "list", args: [], cwd: root, json: true }),
41
+ ),
42
+ )
43
+
44
+ expect(JSON.parse(logs[0]!)).toEqual([
45
+ {
46
+ alias: "agency",
47
+ path: join(root, "repos/agency"),
48
+ kind: "repository",
49
+ remote: null,
50
+ target: null,
51
+ },
52
+ ])
53
+ })
54
+
55
+ test("outputs a linked repository as JSON", async () => {
56
+ const target = join(root, "source")
57
+ await mkdir(target)
58
+ const git = Bun.spawn(["git", "init", target], {
59
+ stdout: "ignore",
60
+ stderr: "ignore",
61
+ })
62
+ expect(await git.exited).toBe(0)
63
+
64
+ const logs = await captureLogs(() =>
65
+ runTestEffect(
66
+ repo({
67
+ subcommand: "link",
68
+ args: ["linked", target],
69
+ cwd: root,
70
+ json: true,
71
+ }),
72
+ ),
73
+ )
74
+
75
+ expect(JSON.parse(logs[0]!)).toEqual({
76
+ alias: "linked",
77
+ path: join(root, "repos/linked"),
78
+ })
79
+ })
80
+ })
@@ -0,0 +1,83 @@
1
+ import { Effect } from "effect"
2
+ import type { BaseCommandOptions } from "../utils/command"
3
+ import { RepositoryService } from "../services/RepositoryService"
4
+ import { createLoggers } from "../utils/effect"
5
+
6
+ interface RepoOptions extends BaseCommandOptions {
7
+ readonly subcommand?: string
8
+ readonly args: readonly string[]
9
+ readonly json?: boolean
10
+ }
11
+
12
+ export const repo = (options: RepoOptions) =>
13
+ Effect.gen(function* () {
14
+ const repositories = yield* RepositoryService
15
+ const { log } = createLoggers(options)
16
+ const cwd = options.cwd ?? process.cwd()
17
+
18
+ switch (options.subcommand) {
19
+ case "add": {
20
+ const [alias, remote] = options.args
21
+ if (!alias || !remote) {
22
+ return yield* Effect.fail(
23
+ new Error("Usage: agency repo add <alias> <remote>"),
24
+ )
25
+ }
26
+ const path = yield* repositories.add(alias, remote, cwd)
27
+ log(
28
+ options.json
29
+ ? JSON.stringify({ alias, path }, null, 2)
30
+ : `Added repository '${alias}'`,
31
+ )
32
+ return
33
+ }
34
+
35
+ case "link": {
36
+ const [alias, path] = options.args
37
+ if (!alias || !path) {
38
+ return yield* Effect.fail(
39
+ new Error("Usage: agency repo link <alias> <path>"),
40
+ )
41
+ }
42
+ const destination = yield* repositories.link(alias, path, cwd)
43
+ log(
44
+ options.json
45
+ ? JSON.stringify({ alias, path: destination }, null, 2)
46
+ : `Linked repository '${alias}'`,
47
+ )
48
+ return
49
+ }
50
+
51
+ case "list": {
52
+ const items = yield* repositories.list(cwd)
53
+ if (options.json) {
54
+ log(JSON.stringify(items, null, 2))
55
+ return
56
+ }
57
+ for (const item of items) {
58
+ const detail = item.target ?? item.remote ?? item.path
59
+ log(`${item.alias}\t${item.kind}\t${detail}`)
60
+ }
61
+ return
62
+ }
63
+
64
+ default:
65
+ return yield* Effect.fail(
66
+ new Error(
67
+ "Subcommand is required. Available subcommands: add, link, list",
68
+ ),
69
+ )
70
+ }
71
+ })
72
+
73
+ export const help = `
74
+ Usage: agency repo <subcommand>
75
+
76
+ Subcommands:
77
+ add <alias> <remote> Create a bare clone
78
+ link <alias> <path> Link an existing Git repository
79
+ list List repository aliases
80
+
81
+ Options:
82
+ --json Output repository aliases as JSON
83
+ `
@@ -1,489 +1,47 @@
1
- import { test, expect, describe, beforeEach, afterEach } from "bun:test"
2
- import { join } from "path"
3
- import { status } from "./status"
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { mkdir } from "node:fs/promises"
3
+ import { join } from "node:path"
4
4
  import {
5
- createTempDir,
5
+ captureLogs,
6
6
  cleanupTempDir,
7
- initGitRepo,
8
- getCurrentBranch,
9
- createCommit,
10
- initAgency,
7
+ createTempDir,
11
8
  runTestEffect,
12
9
  } from "../test-utils"
13
- import { writeAgencyMetadata } from "../types"
14
-
15
- async function createBranch(cwd: string, branchName: string): Promise<void> {
16
- await Bun.spawn(["git", "checkout", "-b", branchName], {
17
- cwd,
18
- stdout: "pipe",
19
- stderr: "pipe",
20
- }).exited
21
- }
22
-
23
- async function checkoutBranch(cwd: string, branchName: string): Promise<void> {
24
- await Bun.spawn(["git", "checkout", branchName], {
25
- cwd,
26
- stdout: "pipe",
27
- stderr: "pipe",
28
- }).exited
29
- }
10
+ import { status } from "./status"
30
11
 
31
12
  describe("status command", () => {
32
- let tempDir: string
33
- let originalCwd: string
13
+ let root: string
34
14
 
35
15
  beforeEach(async () => {
36
- tempDir = await createTempDir()
37
- originalCwd = process.cwd()
38
- process.chdir(tempDir)
39
-
40
- // Set config path to non-existent file to use defaults
41
- process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
42
-
43
- // Initialize git repo
44
- await initGitRepo(tempDir)
45
- await createCommit(tempDir, "Initial commit")
46
-
47
- // Rename to main if needed
48
- const currentBranch = await getCurrentBranch(tempDir)
49
- if (currentBranch === "master") {
50
- await Bun.spawn(["git", "branch", "-m", "main"], {
51
- cwd: tempDir,
52
- stdout: "pipe",
53
- stderr: "pipe",
54
- }).exited
55
- }
56
- })
57
-
58
- afterEach(async () => {
59
- process.chdir(originalCwd)
60
- delete process.env.AGENCY_CONFIG_PATH
61
- await cleanupTempDir(tempDir)
16
+ root = await createTempDir()
17
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
18
+ await mkdir(join(root, "repos/agency"), { recursive: true })
62
19
  })
63
20
 
64
- describe("basic functionality", () => {
65
- test("shows not initialized when agency.json is missing", async () => {
66
- let output = ""
67
- const originalLog = console.log
68
- console.log = (msg: string) => {
69
- output += msg + "\n"
70
- }
71
-
72
- await runTestEffect(status({}))
73
-
74
- console.log = originalLog
75
- expect(output).toContain("Not initialized")
76
- expect(output).toContain("agency task")
77
- })
78
-
79
- test("shows initialized when agency.json exists", async () => {
80
- // Initialize agency
81
- await initAgency(tempDir, "test-template")
82
-
83
- // Create feature branch with agency.json
84
- await createBranch(tempDir, "feature")
85
- await writeAgencyMetadata(tempDir, {
86
- version: 1,
87
- injectedFiles: ["AGENTS.md"],
88
- template: "test-template",
89
- createdAt: new Date().toISOString(),
90
- } as any)
91
-
92
- let output = ""
93
- const originalLog = console.log
94
- console.log = (msg: string) => {
95
- output += msg + "\n"
96
- }
97
-
98
- await runTestEffect(status({}))
99
-
100
- console.log = originalLog
101
- expect(output).toContain("Current branch:")
102
- expect(output).toContain("Branch type: Source branch")
103
- expect(output).toContain("Template:")
104
- })
105
-
106
- test("shows source branch type when on source branch", async () => {
107
- await initAgency(tempDir, "test-template")
108
- await createBranch(tempDir, "feature")
109
- await writeAgencyMetadata(tempDir, {
110
- version: 1,
111
- injectedFiles: [],
112
- template: "test-template",
113
- createdAt: new Date().toISOString(),
114
- } as any)
115
-
116
- let output = ""
117
- const originalLog = console.log
118
- console.log = (msg: string) => {
119
- output += msg + "\n"
120
- }
121
-
122
- await runTestEffect(status({}))
123
-
124
- console.log = originalLog
125
- expect(output).toContain("Branch type:")
126
- expect(output).toContain("Source branch")
127
- })
128
-
129
- test("shows emit branch type when on emit branch", async () => {
130
- await initAgency(tempDir, "test-template")
131
-
132
- // Create source branch with agency.json
133
- await createBranch(tempDir, "agency--feature")
134
- await writeAgencyMetadata(tempDir, {
135
- version: 1,
136
- injectedFiles: [],
137
- template: "test-template",
138
- emitBranch: "feature",
139
- createdAt: new Date().toISOString(),
140
- } as any)
141
- // Stage and commit agency.json
142
- await Bun.spawn(["git", "add", "agency.json"], {
143
- cwd: tempDir,
144
- stdout: "pipe",
145
- stderr: "pipe",
146
- }).exited
147
- await createCommit(tempDir, "Feature work")
148
-
149
- // Create and switch to emit branch
150
- await createBranch(tempDir, "feature")
151
-
152
- let output = ""
153
- const originalLog = console.log
154
- console.log = (msg: string) => {
155
- output += msg + "\n"
156
- }
157
-
158
- await runTestEffect(status({}))
159
-
160
- console.log = originalLog
161
- expect(output).toContain("Branch type:")
162
- expect(output).toContain("Emit branch")
163
- })
164
-
165
- test("shows corresponding branch when it exists", async () => {
166
- await initAgency(tempDir, "test-template")
167
- await createBranch(tempDir, "agency--feature")
168
- await writeAgencyMetadata(tempDir, {
169
- version: 1,
170
- injectedFiles: [],
171
- template: "test-template",
172
- emitBranch: "feature",
173
- createdAt: new Date().toISOString(),
174
- } as any)
175
-
176
- // Stage and commit agency.json
177
- await Bun.spawn(["git", "add", "agency.json"], {
178
- cwd: tempDir,
179
- stdout: "pipe",
180
- stderr: "pipe",
181
- }).exited
182
- await createCommit(tempDir, "Set up branch")
183
-
184
- // Create emit branch
185
- await createBranch(tempDir, "feature")
186
- await checkoutBranch(tempDir, "agency--feature")
187
-
188
- let output = ""
189
- const originalLog = console.log
190
- console.log = (msg: string) => {
191
- output += msg + "\n"
192
- }
193
-
194
- await runTestEffect(status({}))
195
-
196
- console.log = originalLog
197
- expect(output).toContain("Emit branch: ")
198
- expect(output).toContain("feature")
199
- })
200
-
201
- test("shows backpack files", async () => {
202
- await initAgency(tempDir, "test-template")
203
- await createBranch(tempDir, "feature")
204
- await writeAgencyMetadata(tempDir, {
205
- version: 1,
206
- injectedFiles: ["AGENTS.md", "opencode.json"],
207
- template: "test-template",
208
- createdAt: new Date().toISOString(),
209
- } as any)
210
-
211
- let output = ""
212
- const originalLog = console.log
213
- console.log = (msg: string) => {
214
- output += msg + "\n"
215
- }
216
-
217
- await runTestEffect(status({}))
218
-
219
- console.log = originalLog
220
- expect(output).toContain("Backpack:")
221
- // Base backpack files (TASK.md, AGENCY.md, agency.json) + injected files (AGENTS.md, opencode.json)
222
- expect(output).toContain("TASK.md")
223
- expect(output).toContain("AGENCY.md")
224
- expect(output).toContain("agency.json")
225
- expect(output).toContain("AGENTS.md")
226
- expect(output).toContain("opencode.json")
227
- })
228
-
229
- test("shows base branch when set", async () => {
230
- await initAgency(tempDir, "test-template")
231
- await createBranch(tempDir, "feature")
232
- await writeAgencyMetadata(tempDir, {
233
- version: 1,
234
- injectedFiles: [],
235
- template: "test-template",
236
- baseBranch: "main",
237
- createdAt: new Date().toISOString(),
238
- } as any)
239
-
240
- let output = ""
241
- const originalLog = console.log
242
- console.log = (msg: string) => {
243
- output += msg + "\n"
244
- }
245
-
246
- await runTestEffect(status({}))
247
-
248
- console.log = originalLog
249
- expect(output).toContain("Base branch:")
250
- expect(output).toContain("main")
251
- })
252
-
253
- test("shows template name", async () => {
254
- await initAgency(tempDir, "my-custom-template")
255
- await createBranch(tempDir, "feature")
256
- await writeAgencyMetadata(tempDir, {
257
- version: 1,
258
- injectedFiles: [],
259
- template: "my-custom-template",
260
- createdAt: new Date().toISOString(),
261
- } as any)
262
-
263
- let output = ""
264
- const originalLog = console.log
265
- console.log = (msg: string) => {
266
- output += msg + "\n"
267
- }
268
-
269
- await runTestEffect(status({}))
270
-
271
- console.log = originalLog
272
- expect(output).toContain("Template:")
273
- expect(output).toContain("my-custom-template")
274
- })
275
- })
276
-
277
- describe("JSON output", () => {
278
- test("outputs valid JSON with --json flag", async () => {
279
- await initAgency(tempDir, "test-template")
280
- await createBranch(tempDir, "feature")
281
- await writeAgencyMetadata(tempDir, {
282
- version: 1,
283
- injectedFiles: ["AGENTS.md"],
284
- baseBranch: "main",
285
- template: "test-template",
286
- createdAt: new Date().toISOString(),
287
- } as any)
288
-
289
- let output = ""
290
- const originalLog = console.log
291
- console.log = (msg: string) => {
292
- output += msg + "\n"
293
- }
294
-
295
- await runTestEffect(status({ json: true }))
296
-
297
- console.log = originalLog
298
-
299
- const data = JSON.parse(output.trim())
300
- expect(data.initialized).toBe(true)
301
- expect(data.branchType).toBe("source")
302
- expect(data.currentBranch).toBe("feature")
303
- expect(data.template).toBe("test-template")
304
- // managedFiles includes base backpack files + injected files
305
- expect(data.managedFiles).toContain("TASK.md")
306
- expect(data.managedFiles).toContain("AGENCY.md")
307
- expect(data.managedFiles).toContain("agency.json")
308
- expect(data.managedFiles).toContain("AGENTS.md")
309
- expect(data.baseBranch).toBe("main")
310
- })
311
-
312
- test("JSON output contains all expected fields", async () => {
313
- await initAgency(tempDir, "test-template")
314
- await createBranch(tempDir, "feature")
315
- await writeAgencyMetadata(tempDir, {
316
- version: 1,
317
- injectedFiles: [],
318
- template: "test-template",
319
- createdAt: new Date().toISOString(),
320
- } as any)
321
-
322
- let output = ""
323
- const originalLog = console.log
324
- console.log = (msg: string) => {
325
- output += msg + "\n"
326
- }
327
-
328
- await runTestEffect(status({ json: true }))
329
-
330
- console.log = originalLog
331
-
332
- const data = JSON.parse(output.trim())
333
- expect(data).toHaveProperty("initialized")
334
- expect(data).toHaveProperty("branchType")
335
- expect(data).toHaveProperty("currentBranch")
336
- expect(data).toHaveProperty("sourceBranch")
337
- expect(data).toHaveProperty("emitBranch")
338
- expect(data).toHaveProperty("correspondingBranchExists")
339
- expect(data).toHaveProperty("template")
340
- expect(data).toHaveProperty("managedFiles")
341
- expect(data).toHaveProperty("baseBranch")
342
- expect(data).toHaveProperty("createdAt")
343
- })
344
-
345
- test("JSON output shows not initialized state", async () => {
346
- let output = ""
347
- const originalLog = console.log
348
- console.log = (msg: string) => {
349
- output += msg + "\n"
350
- }
351
-
352
- await runTestEffect(status({ json: true }))
353
-
354
- console.log = originalLog
355
-
356
- const data = JSON.parse(output.trim())
357
- expect(data.initialized).toBe(false)
358
- expect(data.branchType).toBe("neither")
359
- })
360
- })
361
-
362
- describe("error handling", () => {
363
- test("throws error when not in a git repository", async () => {
364
- const nonGitDir = await createTempDir()
365
- process.chdir(nonGitDir)
366
-
367
- await expect(runTestEffect(status({}))).rejects.toThrow(
368
- "Not in a git repository",
369
- )
370
-
371
- await cleanupTempDir(nonGitDir)
372
- })
373
- })
374
-
375
- describe("silent mode", () => {
376
- test("silent flag suppresses output", async () => {
377
- const originalLog = console.log
378
- let logCalled = false
379
- console.log = () => {
380
- logCalled = true
381
- }
382
-
383
- await runTestEffect(status({ silent: true }))
384
-
385
- console.log = originalLog
386
- expect(logCalled).toBe(false)
387
- })
388
- })
389
-
390
- describe("emit branch behavior", () => {
391
- test("reads metadata from source branch when on emit branch without agency.json on disk", async () => {
392
- await initAgency(tempDir, "test-template")
393
-
394
- // Create source branch (agency--feature) with agency.json committed
395
- await createBranch(tempDir, "agency--feature")
396
- await writeAgencyMetadata(tempDir, {
397
- version: 1,
398
- injectedFiles: ["AGENTS.md", "opencode.json"],
399
- template: "test-template",
400
- baseBranch: "main",
401
- emitBranch: "feature",
402
- createdAt: new Date().toISOString(),
403
- } as any)
404
- // Stage and commit agency.json
405
- await Bun.spawn(["git", "add", "agency.json"], {
406
- cwd: tempDir,
407
- stdout: "pipe",
408
- stderr: "pipe",
409
- }).exited
410
- await createCommit(tempDir, "Feature work")
411
-
412
- // Create emit branch and remove agency.json from working tree
413
- await createBranch(tempDir, "feature")
414
- await Bun.spawn(["rm", "agency.json"], {
415
- cwd: tempDir,
416
- stdout: "pipe",
417
- stderr: "pipe",
418
- }).exited
419
-
420
- let output = ""
421
- const originalLog = console.log
422
- console.log = (msg: string) => {
423
- output += msg + "\n"
424
- }
425
-
426
- await runTestEffect(status({ json: true }))
427
-
428
- console.log = originalLog
429
-
430
- const data = JSON.parse(output.trim())
431
- // Verify that we're recognized as on an emit branch
432
- expect(data.branchType).toBe("emit")
433
- expect(data.initialized).toBe(true)
434
- // Verify metadata is correctly read from source branch
435
- expect(data.baseBranch).toBe("main")
436
- expect(data.managedFiles).toContain("AGENTS.md")
437
- expect(data.managedFiles).toContain("opencode.json")
438
- expect(data.sourceBranch).toBe("agency--feature")
439
- })
440
-
441
- test("shows correct backpack when on emit branch", async () => {
442
- await initAgency(tempDir, "test-template")
443
-
444
- // Create source branch (agency--feature) with agency.json committed
445
- await createBranch(tempDir, "agency--feature")
446
- await writeAgencyMetadata(tempDir, {
447
- version: 1,
448
- injectedFiles: ["AGENTS.md"],
449
- template: "test-template",
450
- emitBranch: "feature",
451
- createdAt: new Date().toISOString(),
452
- } as any)
453
- // Stage and commit agency.json
454
- await Bun.spawn(["git", "add", "agency.json"], {
455
- cwd: tempDir,
456
- stdout: "pipe",
457
- stderr: "pipe",
458
- }).exited
459
- await createCommit(tempDir, "Feature work")
460
-
461
- // Create emit branch and remove agency.json from disk to simulate clean emit branch
462
- await createBranch(tempDir, "feature")
463
- await Bun.spawn(["rm", "agency.json"], {
464
- cwd: tempDir,
465
- stdout: "pipe",
466
- stderr: "pipe",
467
- }).exited
468
-
469
- let output = ""
470
- const originalLog = console.log
471
- console.log = (msg: string) => {
472
- output += msg + "\n"
473
- }
474
-
475
- await runTestEffect(status({ json: true }))
476
-
477
- console.log = originalLog
478
-
479
- const data = JSON.parse(output.trim())
480
- expect(data.branchType).toBe("emit")
481
- expect(data.initialized).toBe(true)
482
- // Backpack should include injected files from source branch
483
- expect(data.managedFiles).toContain("AGENTS.md")
484
- expect(data.managedFiles).toContain("TASK.md")
485
- expect(data.managedFiles).toContain("AGENCY.md")
486
- expect(data.managedFiles).toContain("agency.json")
487
- })
21
+ afterEach(async () => cleanupTempDir(root))
22
+
23
+ test("outputs status and repository metadata as JSON", async () => {
24
+ const logs = await captureLogs(() =>
25
+ runTestEffect(status({ cwd: root, json: true })),
26
+ )
27
+ const output = JSON.parse(logs[0]!)
28
+
29
+ expect(output).toMatchObject({
30
+ root,
31
+ issues: [],
32
+ epicCount: 0,
33
+ taskCount: 0,
34
+ phaseCount: 0,
35
+ valid: true,
36
+ })
37
+ expect(output.repositories).toEqual([
38
+ {
39
+ alias: "agency",
40
+ path: join(root, "repos/agency"),
41
+ kind: "repository",
42
+ remote: null,
43
+ target: null,
44
+ },
45
+ ])
488
46
  })
489
47
  })