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