@markjaquith/agency 1.11.1 → 2.1.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 (132) hide show
  1. package/README.md +313 -117
  2. package/cli.ts +124 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +266 -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 +326 -399
  24. package/src/commands/work.ts +153 -153
  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/work-target.test.ts +77 -0
  53. package/src/workbase/work-target.ts +180 -0
  54. package/src/workbase/worktree-command.test.ts +61 -0
  55. package/src/workbase/worktree-command.ts +52 -0
  56. package/src/commands/base.test.ts +0 -198
  57. package/src/commands/base.ts +0 -198
  58. package/src/commands/clean.test.ts +0 -299
  59. package/src/commands/clean.ts +0 -274
  60. package/src/commands/completions.test.ts +0 -51
  61. package/src/commands/completions.ts +0 -308
  62. package/src/commands/emit.integration.test.ts +0 -549
  63. package/src/commands/emit.test.ts +0 -576
  64. package/src/commands/emit.ts +0 -700
  65. package/src/commands/emitted.test.ts +0 -226
  66. package/src/commands/emitted.ts +0 -57
  67. package/src/commands/loop.test.ts +0 -621
  68. package/src/commands/loop.ts +0 -406
  69. package/src/commands/merge.integration.test.ts +0 -195
  70. package/src/commands/merge.test.ts +0 -246
  71. package/src/commands/merge.ts +0 -244
  72. package/src/commands/pull.test.ts +0 -395
  73. package/src/commands/pull.ts +0 -205
  74. package/src/commands/push.test.ts +0 -641
  75. package/src/commands/push.ts +0 -482
  76. package/src/commands/rebase.test.ts +0 -521
  77. package/src/commands/rebase.ts +0 -232
  78. package/src/commands/save.test.ts +0 -247
  79. package/src/commands/save.ts +0 -162
  80. package/src/commands/source.test.ts +0 -195
  81. package/src/commands/source.ts +0 -72
  82. package/src/commands/switch.test.ts +0 -194
  83. package/src/commands/switch.ts +0 -84
  84. package/src/commands/task-branching.test.ts +0 -633
  85. package/src/commands/task-continue.test.ts +0 -311
  86. package/src/commands/task-edit.test.ts +0 -235
  87. package/src/commands/task-main.test.ts +0 -1066
  88. package/src/commands/task-squash.test.ts +0 -266
  89. package/src/commands/tasks.test.ts +0 -391
  90. package/src/commands/tasks.ts +0 -161
  91. package/src/commands/template-delete.test.ts +0 -178
  92. package/src/commands/template-delete.ts +0 -98
  93. package/src/commands/template-list.test.ts +0 -135
  94. package/src/commands/template-list.ts +0 -87
  95. package/src/commands/template-view.test.ts +0 -158
  96. package/src/commands/template-view.ts +0 -86
  97. package/src/commands/template.test.ts +0 -32
  98. package/src/commands/template.ts +0 -96
  99. package/src/commands/use.test.ts +0 -87
  100. package/src/commands/use.ts +0 -97
  101. package/src/constants.ts +0 -10
  102. package/src/errors.ts +0 -17
  103. package/src/schemas.ts +0 -33
  104. package/src/services/AgencyMetadataService.ts +0 -311
  105. package/src/services/ClaudeService.test.ts +0 -184
  106. package/src/services/ClaudeService.ts +0 -91
  107. package/src/services/ConfigService.ts +0 -115
  108. package/src/services/FilterRepoService.ts +0 -164
  109. package/src/services/FormatterService.test.ts +0 -432
  110. package/src/services/FormatterService.ts +0 -219
  111. package/src/services/GitService.ts +0 -1187
  112. package/src/services/MockFilterRepoService.ts +0 -140
  113. package/src/services/OpencodeService.ts +0 -263
  114. package/src/services/PromptService.ts +0 -183
  115. package/src/services/TemplateService.ts +0 -81
  116. package/src/types/native-exec.d.ts +0 -8
  117. package/src/utils/colors.ts +0 -178
  118. package/src/utils/git-path.ts +0 -6
  119. package/src/utils/glob.test.ts +0 -161
  120. package/src/utils/glob.ts +0 -78
  121. package/src/utils/paths.ts +0 -51
  122. package/src/utils/pr-branch.test.ts +0 -376
  123. package/src/utils/pr-branch.ts +0 -508
  124. package/src/utils/spinner.test.ts +0 -45
  125. package/src/utils/spinner.ts +0 -96
  126. package/src/utils/task-parser.test.ts +0 -172
  127. package/src/utils/task-parser.ts +0 -93
  128. package/templates/AGENCY.md +0 -22
  129. package/templates/AGENTS.md +0 -11
  130. package/templates/CLAUDE.md +0 -3
  131. package/templates/TASK.md +0 -5
  132. 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
  })