@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
@@ -1,232 +1,43 @@
1
- import { Effect, DateTime } from "effect"
2
- import { join } from "node:path"
1
+ import { Effect } from "effect"
3
2
  import type { BaseCommandOptions } from "../utils/command"
4
- import { GitService } from "../services/GitService"
5
- import { ConfigService } from "../services/ConfigService"
6
- import { FileSystemService } from "../services/FileSystemService"
7
- import {
8
- AgencyMetadataService,
9
- parseAgencyMetadata,
10
- } from "../services/AgencyMetadataService"
11
- import { resolveBranchPairWithAgencyJson } from "../utils/pr-branch"
12
- import highlight, { plural } from "../utils/colors"
13
- import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
3
+ import { WorkbaseService } from "../services/WorkbaseService"
4
+ import { RepositoryService } from "../services/RepositoryService"
5
+ import { createLoggers } from "../utils/effect"
14
6
 
15
7
  interface StatusOptions extends BaseCommandOptions {
16
- json?: boolean
8
+ readonly json?: boolean
17
9
  }
18
10
 
19
- /**
20
- * Branch type for status output
21
- */
22
- type BranchType = "source" | "emit" | "neither"
23
-
24
- /**
25
- * Base files that are always in the backpack
26
- */
27
- const BASE_BACKPACK_FILES = ["TASK.md", "AGENCY.md", "agency.json"]
28
-
29
- /**
30
- * Status data structure returned by the status command
31
- */
32
- interface StatusData {
33
- initialized: boolean
34
- branchType: BranchType
35
- currentBranch: string
36
- sourceBranch: string | null
37
- emitBranch: string | null
38
- correspondingBranchExists: boolean
39
- template: string | null
40
- managedFiles: string[]
41
- baseBranch: string | null
42
- createdAt: string | null
43
- }
44
-
45
- /**
46
- * Read agency.json metadata from the current working directory.
47
- */
48
- const readAgencyMetadataFromDisk = (gitRoot: string) =>
49
- Effect.gen(function* () {
50
- const fs = yield* FileSystemService
51
- const metadataPath = join(gitRoot, "agency.json")
52
-
53
- const exists = yield* fs.exists(metadataPath)
54
- if (!exists) {
55
- return null
56
- }
57
-
58
- const content = yield* fs.readFile(metadataPath)
59
- return yield* parseAgencyMetadata(content)
60
- }).pipe(Effect.catchAll(() => Effect.succeed(null)))
61
-
62
- /**
63
- * Read agency.json metadata from a specific branch using git show.
64
- */
65
- const readAgencyMetadataFromBranch = (gitRoot: string, branch: string) =>
66
- Effect.gen(function* () {
67
- const git = yield* GitService
68
-
69
- // Try to read agency.json from the branch using git show
70
- const content = yield* git.getFileAtRef(gitRoot, branch, "agency.json")
71
-
72
- if (!content) {
73
- return null
74
- }
75
-
76
- return yield* parseAgencyMetadata(content)
77
- }).pipe(Effect.catchAll(() => Effect.succeed(null)))
78
-
79
11
  export const status = (options: StatusOptions = {}) =>
80
12
  Effect.gen(function* () {
81
- const { json = false } = options
13
+ const workbase = yield* WorkbaseService
14
+ const repositories = yield* RepositoryService
82
15
  const { log } = createLoggers(options)
83
-
84
- const git = yield* GitService
85
- const configService = yield* ConfigService
86
-
87
- const gitRoot = yield* ensureGitRepo()
88
-
89
- // Load config for emit branch pattern
90
- const config = yield* configService.loadConfig()
91
-
92
- // Get current branch
93
- const currentBranch = yield* git.getCurrentBranch(gitRoot)
94
-
95
- // Resolve branch pair to determine if we're on source or emit branch
96
- const branches = yield* resolveBranchPairWithAgencyJson(
97
- gitRoot,
98
- currentBranch,
99
- config.sourceBranchPattern,
100
- config.emitBranch,
101
- )
102
- const { sourceBranch, emitBranch, isOnEmitBranch } = branches
103
-
104
- // Check if agency is initialized
105
- // If on emit branch, read agency.json from source branch; otherwise read from disk
106
- const metadata = isOnEmitBranch
107
- ? yield* readAgencyMetadataFromBranch(gitRoot, sourceBranch)
108
- : yield* readAgencyMetadataFromDisk(gitRoot)
109
- const initialized = metadata !== null
110
-
111
- // Determine branch type
112
- let branchType: BranchType = "neither"
113
- if (initialized) {
114
- branchType = isOnEmitBranch ? "emit" : "source"
16
+ const cwd = options.cwd ?? process.cwd()
17
+ const report = yield* workbase.validate(cwd)
18
+ const repos = yield* repositories.list(report.root)
19
+ const data = { ...report, repositories: repos }
20
+
21
+ if (options.json) {
22
+ log(JSON.stringify(data, null, 2))
23
+ return
115
24
  }
116
25
 
117
- // Check if the corresponding branch exists
118
- const correspondingBranch = isOnEmitBranch ? sourceBranch : emitBranch
119
- const correspondingBranchExists = yield* git.branchExists(
120
- gitRoot,
121
- correspondingBranch,
26
+ log(`Workbase: ${report.root}`)
27
+ log(`Repositories: ${repos.length}`)
28
+ log(`Epics: ${report.epicCount}`)
29
+ log(`Tasks: ${report.taskCount}`)
30
+ log(`Phases: ${report.phaseCount}`)
31
+ log(
32
+ `Validation: ${report.valid ? "valid" : `${report.issues.length} issues`}`,
122
33
  )
123
-
124
- // Get template name from git config
125
- const template = yield* getTemplateName(gitRoot)
126
-
127
- // Build the complete backpack (all files carried in/out)
128
- const backpackFiles = metadata
129
- ? [...BASE_BACKPACK_FILES, ...metadata.injectedFiles]
130
- : []
131
-
132
- // Build status data
133
- const statusData: StatusData = {
134
- initialized,
135
- branchType,
136
- currentBranch,
137
- sourceBranch: isOnEmitBranch ? sourceBranch : currentBranch,
138
- emitBranch: isOnEmitBranch ? currentBranch : emitBranch,
139
- correspondingBranchExists,
140
- template,
141
- managedFiles: backpackFiles,
142
- baseBranch: metadata?.baseBranch ?? null,
143
- createdAt: metadata?.createdAt
144
- ? DateTime.toDateUtc(metadata.createdAt).toISOString()
145
- : null,
146
- }
147
-
148
- if (json) {
149
- // Output JSON format
150
- log(JSON.stringify(statusData, null, 2))
151
- } else {
152
- // Output human-readable format with highlighting
153
- log("")
154
-
155
- if (!initialized) {
156
- // Not initialized - show minimal info
157
- log(
158
- `Not initialized (run ${highlight.value("agency task")} to initialize)`,
159
- )
160
- log(`Current branch: ${highlight.branch(currentBranch)}`)
161
- if (template) {
162
- log(`Template: ${highlight.template(template)}`)
163
- }
164
- } else {
165
- // Initialized - show full status
166
- log(`Current branch: ${highlight.branch(currentBranch)}`)
167
-
168
- // Branch type
169
- const branchTypeDisplay = isOnEmitBranch
170
- ? "Emit branch"
171
- : "Source branch"
172
- log(`Branch type: ${branchTypeDisplay}`)
173
-
174
- // Show corresponding branch only if it exists
175
- if (correspondingBranchExists) {
176
- if (isOnEmitBranch) {
177
- log(`Source branch: ${highlight.branch(statusData.sourceBranch!)}`)
178
- } else {
179
- log(`Emit branch: ${highlight.branch(statusData.emitBranch!)}`)
180
- }
181
- }
182
-
183
- // Template
184
- if (template) {
185
- log(`Template: ${highlight.template(template)}`)
186
- }
187
-
188
- // Base branch
189
- if (metadata?.baseBranch) {
190
- log(`Base branch: ${highlight.branch(metadata.baseBranch)}`)
191
- }
192
-
193
- // Backpack (files carried into the job and will leave with)
194
- if (backpackFiles.length > 0) {
195
- log(`Backpack:`)
196
- for (const file of backpackFiles) {
197
- log(` ${highlight.file(file)}`)
198
- }
199
- }
200
-
201
- // Created at
202
- if (metadata?.createdAt) {
203
- const date = DateTime.toDateUtc(metadata.createdAt)
204
- log(`Created: ${date.toLocaleString()}`)
205
- }
206
- }
207
-
208
- log("")
209
- }
210
34
  })
211
35
 
212
36
  export const help = `
213
37
  Usage: agency status [options]
214
38
 
215
- Display the current status of the agency setup in this repository.
216
-
217
- Information shown:
218
- - Whether agency is initialized (agency.json exists)
219
- - Current branch and branch type (source, emit, or neither)
220
- - Source and emit branch names
221
- - Whether the corresponding branch exists
222
- - Configured template name
223
- - Backpack (files carried in and filtered during emit)
224
- - Base branch and creation timestamp
39
+ Show workbase repository, entity, and validation status.
225
40
 
226
41
  Options:
227
- --json Output status as JSON for scripting
228
-
229
- Example:
230
- agency status # Show human-readable status
231
- agency status --json # Output as JSON
42
+ --json Output status as JSON
232
43
  `
@@ -0,0 +1,206 @@
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 { phase } from "./phase"
11
+ import { task } from "./task"
12
+
13
+ describe("task and phase command JSON output", () => {
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
+ await runTestEffect(
21
+ task({
22
+ subcommand: "create",
23
+ args: ["multi"],
24
+ ticketUrl: "https://example.com/task",
25
+ multiPhase: true,
26
+ cwd: root,
27
+ silent: true,
28
+ }),
29
+ )
30
+ await runTestEffect(
31
+ phase({
32
+ subcommand: "create",
33
+ args: ["multi", "first"],
34
+ repo: "agency",
35
+ branch: "task/first",
36
+ base: "main",
37
+ cwd: root,
38
+ silent: true,
39
+ }),
40
+ )
41
+ })
42
+
43
+ afterEach(async () => cleanupTempDir(root))
44
+
45
+ test("lists and shows task metadata without Markdown content", async () => {
46
+ const listLogs = await captureLogs(() =>
47
+ runTestEffect(
48
+ task({ subcommand: "list", args: [], cwd: root, json: true }),
49
+ ),
50
+ )
51
+ expect(JSON.parse(listLogs[0]!)).toEqual([
52
+ {
53
+ id: "multi",
54
+ path: join(root, "tasks/multi/TASK.md"),
55
+ data: {
56
+ ticketUrl: "https://example.com/task",
57
+ phases: [{ id: "first" }],
58
+ },
59
+ },
60
+ ])
61
+
62
+ const showLogs = await captureLogs(() =>
63
+ runTestEffect(
64
+ task({
65
+ subcommand: "show",
66
+ args: ["multi"],
67
+ cwd: root,
68
+ json: true,
69
+ }),
70
+ ),
71
+ )
72
+ expect(JSON.parse(showLogs[0]!)).toEqual({
73
+ id: "multi",
74
+ path: join(root, "tasks/multi/TASK.md"),
75
+ data: {
76
+ ticketUrl: "https://example.com/task",
77
+ phases: [{ id: "first" }],
78
+ },
79
+ })
80
+ })
81
+
82
+ test("lists and shows phase metadata without Markdown content", async () => {
83
+ const listLogs = await captureLogs(() =>
84
+ runTestEffect(
85
+ phase({ subcommand: "list", args: ["multi"], cwd: root, json: true }),
86
+ ),
87
+ )
88
+ expect(JSON.parse(listLogs[0]!)).toEqual([
89
+ {
90
+ taskId: "multi",
91
+ id: "first",
92
+ path: join(root, "tasks/multi/phases/first/PHASE.md"),
93
+ data: {
94
+ repo: "agency",
95
+ branch: "task/first",
96
+ base: "main",
97
+ pr: null,
98
+ },
99
+ },
100
+ ])
101
+
102
+ const showLogs = await captureLogs(() =>
103
+ runTestEffect(
104
+ phase({
105
+ subcommand: "show",
106
+ args: ["multi", "first"],
107
+ cwd: root,
108
+ json: true,
109
+ }),
110
+ ),
111
+ )
112
+ expect(JSON.parse(showLogs[0]!)).toEqual({
113
+ taskId: "multi",
114
+ id: "first",
115
+ path: join(root, "tasks/multi/phases/first/PHASE.md"),
116
+ data: {
117
+ repo: "agency",
118
+ branch: "task/first",
119
+ base: "main",
120
+ pr: null,
121
+ },
122
+ })
123
+ })
124
+
125
+ test("outputs created task and phase records as JSON", async () => {
126
+ const taskLogs = await captureLogs(() =>
127
+ runTestEffect(
128
+ task({
129
+ subcommand: "create",
130
+ args: ["another"],
131
+ ticketUrl: "https://example.com/another",
132
+ multiPhase: true,
133
+ cwd: root,
134
+ json: true,
135
+ }),
136
+ ),
137
+ )
138
+ expect(JSON.parse(taskLogs[0]!)).toMatchObject({
139
+ id: "another",
140
+ data: { ticketUrl: "https://example.com/another", phases: [] },
141
+ })
142
+
143
+ const phaseLogs = await captureLogs(() =>
144
+ runTestEffect(
145
+ phase({
146
+ subcommand: "create",
147
+ args: ["another", "first"],
148
+ repo: "agency",
149
+ branch: "task/another-first",
150
+ base: "main",
151
+ cwd: root,
152
+ json: true,
153
+ }),
154
+ ),
155
+ )
156
+ expect(JSON.parse(phaseLogs[0]!)).toMatchObject({
157
+ taskId: "another",
158
+ id: "first",
159
+ data: { repo: "agency", branch: "task/another-first", base: "main" },
160
+ })
161
+ })
162
+
163
+ test("converts a single-phase task with an explicit first phase ID", async () => {
164
+ await runTestEffect(
165
+ task({
166
+ subcommand: "create",
167
+ args: ["single"],
168
+ ticketUrl: "https://example.com/single",
169
+ repo: "agency",
170
+ branch: "task/single",
171
+ base: "main",
172
+ cwd: root,
173
+ silent: true,
174
+ }),
175
+ )
176
+
177
+ await runTestEffect(
178
+ phase({
179
+ subcommand: "create",
180
+ args: ["single", "follow-up"],
181
+ firstPhase: "implementation",
182
+ repo: "agency",
183
+ branch: "task/follow-up",
184
+ base: "main",
185
+ dependsOn: ["implementation"],
186
+ cwd: root,
187
+ silent: true,
188
+ }),
189
+ )
190
+
191
+ const logs = await captureLogs(() =>
192
+ runTestEffect(
193
+ task({
194
+ subcommand: "show",
195
+ args: ["single"],
196
+ cwd: root,
197
+ json: true,
198
+ }),
199
+ ),
200
+ )
201
+ expect(JSON.parse(logs[0]!).data.phases).toEqual([
202
+ { id: "implementation" },
203
+ { id: "follow-up", dependsOn: ["implementation"] },
204
+ ])
205
+ })
206
+ })