@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,434 +0,0 @@
1
- import { Effect, Either } from "effect"
2
- import type { BaseCommandOptions } from "../utils/command"
3
- import { GitCommandError, GitService } from "../services/GitService"
4
- import { ConfigService } from "../services/ConfigService"
5
- import { resolveBranchPairWithAgencyJson } from "../utils/pr-branch"
6
- import { emit } from "./emit"
7
- import highlight, { done } from "../utils/colors"
8
- import {
9
- createLoggers,
10
- ensureGitRepo,
11
- getRemoteName,
12
- withBranchProtection,
13
- } from "../utils/effect"
14
- import { withSpinner } from "../utils/spinner"
15
- import { spawnProcess } from "../utils/process"
16
-
17
- interface PushOptions extends BaseCommandOptions {
18
- baseBranch?: string
19
- emit?: string
20
- branch?: string // Deprecated: use emit instead
21
- force?: boolean
22
- noVerify?: boolean
23
- pr?: boolean
24
- skipFilter?: boolean
25
- }
26
-
27
- const getPushFailureStderr = (error: unknown): string => {
28
- if (error instanceof GitCommandError) {
29
- return error.stderr.trim()
30
- }
31
-
32
- if (
33
- typeof error === "object" &&
34
- error !== null &&
35
- "cause" in error &&
36
- (error as { cause?: unknown }).cause instanceof GitCommandError
37
- ) {
38
- return (error as { cause: GitCommandError }).cause.stderr.trim()
39
- }
40
-
41
- if (error instanceof Error) {
42
- return error.message.trim()
43
- }
44
-
45
- return String(error).trim()
46
- }
47
-
48
- const formatProcessOutput = (result: {
49
- readonly stdout?: string
50
- readonly stderr?: string
51
- }): string => [result.stdout, result.stderr].filter(Boolean).join("\n").trim()
52
-
53
- const pushFailureNeedsForce = (stderr: string): boolean => {
54
- const normalized = stderr.toLowerCase()
55
-
56
- return (
57
- normalized.includes("non-fast-forward") ||
58
- normalized.includes(
59
- "updates were rejected because the tip of your current branch is behind",
60
- ) ||
61
- (normalized.includes("[rejected]") && normalized.includes("fetch first"))
62
- )
63
- }
64
-
65
- const formatPushFailure = (error: unknown): Error => {
66
- const stderr = getPushFailureStderr(error)
67
-
68
- if (pushFailureNeedsForce(stderr)) {
69
- return new Error(
70
- "Failed to push branch to remote: the remote rejected a non-fast-forward update, which usually means the branch was rebased or rewritten.\n" +
71
- "Run `agency push --force` to replace the remote branch.",
72
- )
73
- }
74
-
75
- return new Error(`Failed to push branch to remote: ${stderr}`)
76
- }
77
-
78
- export const push = (options: PushOptions = {}) =>
79
- Effect.gen(function* () {
80
- const gitRoot = yield* ensureGitRepo()
81
-
82
- // Wrap the entire push operation with branch protection
83
- // This ensures we return to the original branch on Ctrl-C interrupt
84
- yield* withBranchProtection(gitRoot, pushCore(gitRoot, options))
85
- })
86
-
87
- const pushCore = (gitRoot: string, options: PushOptions) =>
88
- Effect.gen(function* () {
89
- const { log, verboseLog } = createLoggers(options)
90
-
91
- const git = yield* GitService
92
- const configService = yield* ConfigService
93
-
94
- // Check for uncommitted changes
95
- const statusOutput = yield* git.getStatus(gitRoot)
96
-
97
- if (statusOutput && statusOutput.trim().length > 0) {
98
- return yield* Effect.fail(
99
- new Error(
100
- `You have uncommitted changes. Please commit or stash them before pushing.\n` +
101
- `The push command performs internal checkouts that would destroy uncommitted work.\n` +
102
- `Run 'git status' to see the changes.`,
103
- ),
104
- )
105
- }
106
-
107
- verboseLog(`Working directory is clean`)
108
-
109
- // Load config to check emit branch pattern
110
- const config = yield* configService.loadConfig()
111
-
112
- // Get current branch
113
- let sourceBranch = yield* git.getCurrentBranch(gitRoot)
114
-
115
- // Check if we're already on an emit branch using proper branch resolution
116
- const branchInfo = yield* resolveBranchPairWithAgencyJson(
117
- gitRoot,
118
- sourceBranch,
119
- config.sourceBranchPattern,
120
- config.emitBranch,
121
- )
122
-
123
- // If we're on an emit branch, switch to the source branch first
124
- if (branchInfo.isOnEmitBranch) {
125
- const actualSourceBranch = branchInfo.sourceBranch
126
- // Check if the source branch exists
127
- const sourceExists = yield* git.branchExists(gitRoot, actualSourceBranch)
128
- if (sourceExists) {
129
- verboseLog(
130
- `Currently on emit branch ${highlight.branch(sourceBranch)}, switching to source branch ${highlight.branch(actualSourceBranch)}`,
131
- )
132
- yield* git.checkoutBranch(gitRoot, actualSourceBranch)
133
- sourceBranch = actualSourceBranch
134
- }
135
- }
136
-
137
- verboseLog(`Starting push workflow from ${highlight.branch(sourceBranch)}`)
138
-
139
- // Step 1: Create emit branch (agency emit)
140
- verboseLog("Step 1: Emitting...")
141
- // Use the emit command for the entire emit step so push and emit stay in sync.
142
- const prEffectWithOptions = emit({
143
- baseBranch: options.baseBranch,
144
- emit: options.emit || options.branch,
145
- silent: options.silent,
146
- force: options.force,
147
- verbose: options.verbose,
148
- skipFilter: options.skipFilter,
149
- })
150
-
151
- const prResult = yield* Effect.either(prEffectWithOptions)
152
- if (Either.isLeft(prResult)) {
153
- const error =
154
- prResult.left instanceof Error
155
- ? prResult.left
156
- : new Error(String(prResult.left))
157
- const message = error.message || ""
158
- const ignorable =
159
- message === "" ||
160
- message.includes("already exists") ||
161
- message.includes("check if branch exists")
162
-
163
- if (!ignorable) {
164
- return yield* Effect.fail(
165
- new Error(`Failed to create emit branch: ${message}`),
166
- )
167
- }
168
-
169
- verboseLog("Emit reported existing branch; continuing")
170
- }
171
-
172
- // Compute the emit branch name (emit() command now stays on source branch)
173
- // Use the branchInfo we already computed earlier
174
- const emitBranchName =
175
- options.emit || options.branch || branchInfo.emitBranch
176
-
177
- // Step 2: Push to remote (git push)
178
- const remote = yield* getRemoteName(gitRoot)
179
- verboseLog(
180
- `Step 2: Pushing ${highlight.branch(emitBranchName)} to ${highlight.remote(remote)}...`,
181
- )
182
-
183
- // Switch to emit branch before pushing so that pre-push hooks run
184
- // in the context of the emit branch (without backpack files)
185
- yield* git.checkoutBranch(gitRoot, emitBranchName)
186
-
187
- const pushEither = yield* Effect.either(
188
- withSpinner(
189
- pushBranchToRemoteEffect(gitRoot, emitBranchName, remote, {
190
- force: options.force,
191
- noVerify: options.noVerify,
192
- verbose: options.verbose,
193
- }),
194
- {
195
- text: options.force
196
- ? `Pushing to ${highlight.remote(remote)} (forced)`
197
- : `Pushing to ${highlight.remote(remote)}`,
198
- enabled: !options.silent && !options.verbose,
199
- },
200
- ),
201
- )
202
- if (Either.isLeft(pushEither)) {
203
- const error = formatPushFailure(pushEither.left)
204
- // If push failed, switch back to source branch
205
- yield* git.checkoutBranch(gitRoot, sourceBranch)
206
- return yield* Effect.fail(error)
207
- }
208
-
209
- const usedForce = pushEither.right
210
-
211
- if (usedForce) {
212
- log(done(`Pushed to ${highlight.remote(remote)} (forced)`))
213
- } else {
214
- log(done(`Pushed to ${highlight.remote(remote)}`))
215
- }
216
-
217
- // Step 3 (optional): Open GitHub PR if --pr flag is set
218
- if (options.pr) {
219
- verboseLog("Step 3: Opening GitHub PR...")
220
-
221
- const ghEither = yield* Effect.either(
222
- openGitHubPR(gitRoot, emitBranchName, {
223
- verbose: options.verbose,
224
- }),
225
- )
226
-
227
- if (Either.isLeft(ghEither)) {
228
- const error = ghEither.left
229
- // Don't fail the entire command if gh fails, just warn
230
- console.error(
231
- `⚠ Failed to open GitHub PR: ${error instanceof Error ? error.message : String(error)}`,
232
- )
233
- } else {
234
- log(done("Opened GitHub PR in browser"))
235
- }
236
- }
237
-
238
- // Switch back to source branch
239
- yield* git.checkoutBranch(gitRoot, sourceBranch)
240
- })
241
-
242
- // Helper: Push branch to remote with optional force and retry logic
243
- const pushBranchToRemoteEffect = (
244
- gitRoot: string,
245
- branchName: string,
246
- remote: string,
247
- options: {
248
- readonly force?: boolean
249
- readonly noVerify?: boolean
250
- readonly verbose?: boolean
251
- },
252
- ) =>
253
- Effect.gen(function* () {
254
- const git = yield* GitService
255
- const { force = false, noVerify = false } = options
256
-
257
- // Try pushing without force first
258
- const pushResult = yield* git
259
- .push(gitRoot, remote, branchName, { setUpstream: true, noVerify })
260
- .pipe(
261
- Effect.catchAll((error: any) =>
262
- Effect.succeed({
263
- exitCode: error.exitCode ?? -1,
264
- stdout: "",
265
- stderr: error.stderr ?? String(error),
266
- }),
267
- ),
268
- )
269
-
270
- let usedForce = false
271
-
272
- // If push failed, check if we should retry with --force
273
- if (pushResult.exitCode !== 0) {
274
- const stderr = formatProcessOutput(pushResult)
275
-
276
- // Check if this is a force-push-needed error
277
- const needsForce = pushFailureNeedsForce(stderr)
278
-
279
- if (needsForce && force) {
280
- // User provided --force flag, retry with force
281
- const forceResult = yield* git
282
- .push(gitRoot, remote, branchName, {
283
- setUpstream: true,
284
- force: true,
285
- noVerify,
286
- })
287
- .pipe(
288
- Effect.catchAll((error: any) =>
289
- Effect.succeed({
290
- exitCode: error.exitCode ?? -1,
291
- stdout: "",
292
- stderr: error.stderr ?? String(error),
293
- }),
294
- ),
295
- )
296
-
297
- if (forceResult.exitCode !== 0) {
298
- return yield* Effect.fail(
299
- new Error(
300
- `Failed to force push branch to remote: ${formatProcessOutput(forceResult)}`,
301
- ),
302
- )
303
- }
304
-
305
- usedForce = true
306
- } else if (needsForce && !force) {
307
- // User didn't provide --force but it's needed
308
- return yield* Effect.fail(
309
- formatPushFailure(
310
- new GitCommandError({
311
- command: `git push -u ${remote} ${branchName}`,
312
- exitCode: pushResult.exitCode,
313
- stderr,
314
- }),
315
- ),
316
- )
317
- } else {
318
- // Some other error
319
- return yield* Effect.fail(
320
- formatPushFailure(
321
- new GitCommandError({
322
- command: `git push -u ${remote} ${branchName}`,
323
- exitCode: pushResult.exitCode,
324
- stderr,
325
- }),
326
- ),
327
- )
328
- }
329
- }
330
-
331
- return usedForce
332
- })
333
-
334
- // Helper: Open GitHub PR using gh CLI
335
- const openGitHubPR = (
336
- gitRoot: string,
337
- branchName: string,
338
- options: {
339
- readonly verbose?: boolean
340
- },
341
- ) =>
342
- Effect.gen(function* () {
343
- const { verbose = false } = options
344
-
345
- // Run gh pr create --web with --head to specify the emit branch
346
- // Set environment variables to ensure non-interactive mode in CI
347
- // Add a 3 second timeout to prevent hanging in CI environments
348
- const ghEffect = spawnProcess(
349
- ["gh", "pr", "create", "--web", "--head", branchName],
350
- {
351
- cwd: gitRoot,
352
- stdout: verbose ? "inherit" : "pipe",
353
- stderr: "pipe",
354
- env: {
355
- GH_PROMPT_DISABLED: "1",
356
- NO_COLOR: "1",
357
- },
358
- },
359
- ).pipe(
360
- Effect.timeout("3 seconds"),
361
- Effect.catchAll((error) => {
362
- // Handle timeout or process errors
363
- if (error._tag === "TimeoutException") {
364
- return Effect.succeed({
365
- exitCode: -1,
366
- stdout: "",
367
- stderr: "gh command timed out after 3 seconds",
368
- })
369
- }
370
- // Handle ProcessError
371
- return Effect.succeed({
372
- exitCode: (error as any).exitCode ?? -1,
373
- stdout: "",
374
- stderr: (error as any).stderr ?? String(error),
375
- })
376
- }),
377
- )
378
-
379
- const ghResult = yield* ghEffect
380
-
381
- if (ghResult.exitCode !== 0) {
382
- return yield* Effect.fail(
383
- new Error(`gh CLI command failed: ${ghResult.stderr.trim()}`),
384
- )
385
- }
386
- })
387
-
388
- export const help = `
389
- Usage: agency push [base-branch] [options]
390
-
391
- Create a emit branch, push it to remote, and return to the source branch.
392
-
393
- This command is a convenience wrapper that runs operations in sequence:
394
- 1. agency emit [base-branch] - Create emit branch with backpack files reverted
395
- 2. git push -u origin <pr-branch> - Push emit branch to remote
396
- 3. gh pr create --web (optional with --pr) - Open GitHub PR in browser
397
- 4. git checkout <source-branch> - Switch back to source branch
398
-
399
- The command ensures you end up back on your source branch after pushing
400
- the emit branch, making it easy to continue working locally while having
401
- a clean emit branch ready on the remote.
402
-
403
- Base Branch Selection:
404
- Same as 'agency emit' - see 'agency emit --help' for details
405
-
406
- Prerequisites:
407
- - git-filter-repo must be installed: brew install git-filter-repo
408
- - Remote 'origin' must be configured
409
-
410
- Arguments:
411
- base-branch Base branch to compare against (e.g., origin/main)
412
- If not provided, will use saved config or auto-detect
413
-
414
- Options:
415
- --emit Custom name for emit branch (defaults to pattern from config)
416
- --branch (Deprecated: use --emit) Custom name for emit branch
417
- -f, --force Force push to remote if branch has diverged
418
- --no-verify Bypass git pre-push hooks
419
- --pr Open GitHub PR in browser after pushing (requires gh CLI)
420
-
421
- Examples:
422
- agency push # Create PR, push, return to source
423
- agency push origin/main # Explicitly use origin/main as base
424
- agency push --force # Force push if branch has diverged
425
- agency push --no-verify # Push without running pre-push hooks
426
- agency push --pr # Push and open GitHub PR in browser
427
-
428
- Notes:
429
- - Must be run from a source branch (not a emit branch)
430
- - Creates or recreates the emit branch
431
- - Pushes with -u flag to set up tracking
432
- - Automatically returns to source branch after pushing
433
- - If any step fails, the command stops and reports the error
434
- `