@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
@@ -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
+ })