@markjaquith/agency 0.5.0 → 0.6.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.
@@ -1,4 +1,4 @@
1
- import { resolve, join } from "path"
1
+ import { resolve, join, dirname } from "path"
2
2
  import { Effect } from "effect"
3
3
  import type { BaseCommandOptions } from "../utils/command"
4
4
  import { GitService } from "../services/GitService"
@@ -8,6 +8,10 @@ import { PromptService } from "../services/PromptService"
8
8
  import { TemplateService } from "../services/TemplateService"
9
9
  import { OpencodeService } from "../services/OpencodeService"
10
10
  import { ClaudeService } from "../services/ClaudeService"
11
+ import {
12
+ AgencyMetadataService,
13
+ AgencyMetadataServiceLive,
14
+ } from "../services/AgencyMetadataService"
11
15
  import { initializeManagedFiles, writeAgencyMetadata } from "../types"
12
16
  import { RepositoryNotInitializedError } from "../errors"
13
17
  import highlight, { done, info, plural } from "../utils/colors"
@@ -17,22 +21,291 @@ import {
17
21
  extractCleanBranch,
18
22
  makeSourceBranchName,
19
23
  } from "../utils/pr-branch"
24
+ import { getTopLevelDir, dirToGlobPattern } from "../utils/glob"
20
25
 
21
26
  interface TaskOptions extends BaseCommandOptions {
22
27
  path?: string
23
28
  task?: string
24
- branch?: string
29
+ emit?: string
30
+ branch?: string // Deprecated: use emit instead
25
31
  from?: string
26
32
  fromCurrent?: boolean
33
+ continue?: boolean
27
34
  }
28
35
 
29
36
  interface TaskEditOptions extends BaseCommandOptions {}
30
37
 
38
+ /**
39
+ * Continue a task by creating a new branch with the same agency files.
40
+ * This is useful after a PR is merged and you want to continue working on the task.
41
+ */
42
+ const taskContinue = (options: TaskOptions) =>
43
+ Effect.gen(function* () {
44
+ const { silent = false } = options
45
+ const { log, verboseLog } = createLoggers(options)
46
+
47
+ const git = yield* GitService
48
+ const configService = yield* ConfigService
49
+ const fs = yield* FileSystemService
50
+ const promptService = yield* PromptService
51
+
52
+ // Determine target path
53
+ let targetPath: string
54
+
55
+ if (options.path) {
56
+ targetPath = resolve(options.path)
57
+ const isRoot = yield* git.isGitRoot(targetPath)
58
+ if (!isRoot) {
59
+ return yield* Effect.fail(
60
+ new Error(
61
+ "The specified path is not the root of a git repository. Please provide a path to the top-level directory of a git checkout.",
62
+ ),
63
+ )
64
+ }
65
+ } else {
66
+ const isRepo = yield* git.isInsideGitRepo(process.cwd())
67
+ if (!isRepo) {
68
+ return yield* Effect.fail(
69
+ new Error(
70
+ "Not in a git repository. Please run this command inside a git repo.",
71
+ ),
72
+ )
73
+ }
74
+ targetPath = yield* git.getGitRoot(process.cwd())
75
+ }
76
+
77
+ // Check if initialized (has template in git config)
78
+ const templateName = yield* getTemplateName(targetPath)
79
+ if (!templateName) {
80
+ return yield* Effect.fail(new RepositoryNotInitializedError())
81
+ }
82
+
83
+ const currentBranch = yield* git.getCurrentBranch(targetPath)
84
+ verboseLog(`Current branch: ${highlight.branch(currentBranch)}`)
85
+
86
+ // Load agency config for branch patterns
87
+ const config = yield* configService.loadConfig()
88
+
89
+ // Verify we're on an agency source branch by checking if agency.json exists
90
+ const agencyJsonPath = resolve(targetPath, "agency.json")
91
+ const hasAgencyJson = yield* fs.exists(agencyJsonPath)
92
+
93
+ if (!hasAgencyJson) {
94
+ return yield* Effect.fail(
95
+ new Error(
96
+ `No agency.json found on current branch ${highlight.branch(currentBranch)}.\n` +
97
+ `The --continue flag requires you to be on an agency source branch with existing agency files.`,
98
+ ),
99
+ )
100
+ }
101
+
102
+ // Read the existing agency.json to get metadata
103
+ const existingMetadata = yield* Effect.gen(function* () {
104
+ const service = yield* AgencyMetadataService
105
+ return yield* service.readFromDisk(targetPath)
106
+ }).pipe(Effect.provide(AgencyMetadataServiceLive))
107
+
108
+ if (!existingMetadata) {
109
+ return yield* Effect.fail(
110
+ new Error(
111
+ `Failed to read agency.json on branch ${highlight.branch(currentBranch)}.`,
112
+ ),
113
+ )
114
+ }
115
+
116
+ verboseLog(
117
+ `Existing metadata: ${JSON.stringify(existingMetadata, null, 2)}`,
118
+ )
119
+
120
+ // Get the list of agency files to copy
121
+ const filesToCopy = [
122
+ "agency.json",
123
+ "TASK.md",
124
+ "AGENCY.md",
125
+ ...existingMetadata.injectedFiles,
126
+ ]
127
+ verboseLog(`Files to copy: ${filesToCopy.join(", ")}`)
128
+
129
+ // Read all the existing files content before switching branches
130
+ const fileContents = new Map<string, string>()
131
+ for (const file of filesToCopy) {
132
+ const filePath = resolve(targetPath, file)
133
+ const exists = yield* fs.exists(filePath)
134
+ if (exists) {
135
+ const content = yield* fs.readFile(filePath)
136
+ fileContents.set(file, content)
137
+ verboseLog(`Read ${file} (${content.length} bytes)`)
138
+ } else {
139
+ verboseLog(`File ${file} not found, skipping`)
140
+ }
141
+ }
142
+
143
+ // Prompt for new branch name if not provided
144
+ let branchName = options.emit || options.branch
145
+ if (!branchName) {
146
+ if (silent) {
147
+ return yield* Effect.fail(
148
+ new Error(
149
+ "Branch name is required with --continue in silent mode. Use --emit to specify one.",
150
+ ),
151
+ )
152
+ }
153
+ branchName = yield* promptService.prompt("New branch name: ")
154
+ if (!branchName) {
155
+ return yield* Effect.fail(
156
+ new Error("Branch name is required to continue the task."),
157
+ )
158
+ }
159
+ }
160
+
161
+ // Apply source pattern to get the full source branch name
162
+ const sourceBranchName = makeSourceBranchName(
163
+ branchName,
164
+ config.sourceBranchPattern,
165
+ )
166
+
167
+ // Check if the new branch already exists
168
+ const branchExists = yield* git.branchExists(targetPath, sourceBranchName)
169
+ if (branchExists) {
170
+ return yield* Effect.fail(
171
+ new Error(
172
+ `Branch ${highlight.branch(sourceBranchName)} already exists.\n` +
173
+ `Choose a different branch name.`,
174
+ ),
175
+ )
176
+ }
177
+
178
+ // Determine base branch to branch from
179
+ let baseBranchToBranchFrom: string | undefined
180
+
181
+ if (options.from) {
182
+ baseBranchToBranchFrom = options.from
183
+ const exists = yield* git.branchExists(targetPath, baseBranchToBranchFrom)
184
+ if (!exists) {
185
+ return yield* Effect.fail(
186
+ new Error(
187
+ `Branch ${highlight.branch(baseBranchToBranchFrom)} does not exist.`,
188
+ ),
189
+ )
190
+ }
191
+ } else {
192
+ // Default: branch from main upstream branch (preferring remote)
193
+ baseBranchToBranchFrom =
194
+ (yield* git.resolveMainBranch(targetPath)) || undefined
195
+ }
196
+
197
+ if (!baseBranchToBranchFrom) {
198
+ return yield* Effect.fail(
199
+ new Error(
200
+ "Could not determine base branch. Use --from to specify one.",
201
+ ),
202
+ )
203
+ }
204
+
205
+ verboseLog(
206
+ `Creating new branch ${highlight.branch(sourceBranchName)} from ${highlight.branch(baseBranchToBranchFrom)}`,
207
+ )
208
+
209
+ // Create the new branch from the base branch
210
+ yield* git.createBranch(
211
+ sourceBranchName,
212
+ targetPath,
213
+ baseBranchToBranchFrom,
214
+ )
215
+ log(
216
+ done(
217
+ `Created and switched to branch ${highlight.branch(sourceBranchName)} based on ${highlight.branch(baseBranchToBranchFrom)}`,
218
+ ),
219
+ )
220
+
221
+ // Calculate the new emit branch name
222
+ const newEmitBranchName = makeEmitBranchName(branchName, config.emitBranch)
223
+ verboseLog(`New emit branch name: ${newEmitBranchName}`)
224
+
225
+ // Write all the files to the new branch
226
+ const createdFiles: string[] = []
227
+
228
+ for (const [file, content] of fileContents) {
229
+ const filePath = resolve(targetPath, file)
230
+
231
+ // Ensure parent directory exists
232
+ const parentDir = resolve(filePath, "..")
233
+ const parentExists = yield* fs.exists(parentDir)
234
+ if (!parentExists) {
235
+ yield* fs.runCommand(["mkdir", "-p", parentDir])
236
+ }
237
+
238
+ let fileContent = content
239
+
240
+ // Update emitBranch in agency.json
241
+ if (file === "agency.json") {
242
+ const metadata = JSON.parse(content)
243
+ metadata.emitBranch = newEmitBranchName
244
+ metadata.createdAt = new Date().toISOString()
245
+ fileContent = JSON.stringify(metadata, null, 2) + "\n"
246
+ verboseLog(
247
+ `Updated agency.json with new emitBranch: ${newEmitBranchName}`,
248
+ )
249
+ }
250
+
251
+ // Update emitBranch in opencode.json (if it has one)
252
+ if (file === "opencode.json" || file.endsWith("/opencode.json")) {
253
+ try {
254
+ const opencodeConfig = JSON.parse(content)
255
+ if (opencodeConfig.emitBranch) {
256
+ opencodeConfig.emitBranch = newEmitBranchName
257
+ fileContent = JSON.stringify(opencodeConfig, null, "\t") + "\n"
258
+ verboseLog(
259
+ `Updated ${file} with new emitBranch: ${newEmitBranchName}`,
260
+ )
261
+ }
262
+ } catch {
263
+ // If we can't parse it, just copy as-is
264
+ }
265
+ }
266
+
267
+ yield* fs.writeFile(filePath, fileContent)
268
+ createdFiles.push(file)
269
+ log(done(`Created ${highlight.file(file)}`))
270
+ }
271
+
272
+ // Git add and commit the created files
273
+ if (createdFiles.length > 0) {
274
+ yield* Effect.gen(function* () {
275
+ yield* git.gitAdd(createdFiles, targetPath)
276
+ // Format: chore: agency task --continue (baseBranch) originalSource => newSource => newEmit
277
+ const commitMessage = `chore: agency task --continue (${baseBranchToBranchFrom}) ${currentBranch} → ${sourceBranchName} → ${newEmitBranchName}`
278
+ yield* git.gitCommit(commitMessage, targetPath, {
279
+ noVerify: true,
280
+ })
281
+ verboseLog(
282
+ `Committed ${createdFiles.length} file${plural(createdFiles.length)}`,
283
+ )
284
+ }).pipe(
285
+ Effect.catchAll((err) => {
286
+ verboseLog(`Failed to commit: ${err}`)
287
+ return Effect.void
288
+ }),
289
+ )
290
+ }
291
+
292
+ log(
293
+ info(
294
+ `Continued task with ${createdFiles.length} file${plural(createdFiles.length)} from ${highlight.branch(currentBranch)}`,
295
+ ),
296
+ )
297
+ })
298
+
31
299
  export const task = (options: TaskOptions = {}) =>
32
300
  Effect.gen(function* () {
33
301
  const { silent = false, verbose = false } = options
34
302
  const { log, verboseLog } = createLoggers(options)
35
303
 
304
+ // Handle --continue flag
305
+ if (options.continue) {
306
+ return yield* taskContinue(options)
307
+ }
308
+
36
309
  const git = yield* GitService
37
310
  const configService = yield* ConfigService
38
311
  const fs = yield* FileSystemService
@@ -72,6 +345,9 @@ export const task = (options: TaskOptions = {}) =>
72
345
 
73
346
  const createdFiles: string[] = []
74
347
  const injectedFiles: string[] = []
348
+ // Track directories that we create during this task
349
+ // These will be converted to glob patterns for filtering
350
+ const createdDirs = new Set<string>()
75
351
 
76
352
  // Check if initialized (has template in git config)
77
353
  const templateName = yield* getTemplateName(targetPath)
@@ -124,32 +400,9 @@ export const task = (options: TaskOptions = {}) =>
124
400
  )
125
401
  }
126
402
  } else {
127
- // Default: determine main upstream branch
403
+ // Default: determine main upstream branch (preferring remote)
128
404
  baseBranchToBranchFrom =
129
- (yield* git.getMainBranchConfig(targetPath)) ||
130
- (yield* git.findMainBranch(targetPath)) ||
131
- undefined
132
-
133
- // If still no base branch, try to auto-detect from remote
134
- if (!baseBranchToBranchFrom) {
135
- const remote = yield* git
136
- .resolveRemote(targetPath)
137
- .pipe(Effect.catchAll(() => Effect.succeed(null)))
138
-
139
- const commonBases: string[] = []
140
- if (remote) {
141
- commonBases.push(`${remote}/main`, `${remote}/master`)
142
- }
143
- commonBases.push("main", "master")
144
-
145
- for (const base of commonBases) {
146
- const exists = yield* git.branchExists(targetPath, base)
147
- if (exists) {
148
- baseBranchToBranchFrom = base
149
- break
150
- }
151
- }
152
- }
405
+ (yield* git.resolveMainBranch(targetPath)) || undefined
153
406
 
154
407
  if (baseBranchToBranchFrom) {
155
408
  verboseLog(
@@ -158,10 +411,12 @@ export const task = (options: TaskOptions = {}) =>
158
411
  }
159
412
  }
160
413
 
414
+ // Load config early for branch pattern operations
415
+ const config = yield* configService.loadConfig()
416
+
161
417
  // Check if the base branch is an agency source branch
162
418
  // If so, we need to emit it first and use the emit branch instead
163
419
  if (baseBranchToBranchFrom) {
164
- const config = yield* configService.loadConfig()
165
420
  const cleanFromBase = extractCleanBranch(
166
421
  baseBranchToBranchFrom,
167
422
  config.sourceBranchPattern,
@@ -200,10 +455,20 @@ export const task = (options: TaskOptions = {}) =>
200
455
  }
201
456
  }
202
457
 
203
- // If on main branch without a branch name, prompt for it (unless in silent mode)
204
- let branchName = options.branch
205
- if (!isFeature && !branchName) {
458
+ // Determine branch name logic
459
+ let branchName = options.emit || options.branch
460
+
461
+ // If on main branch or using --from without a branch name, prompt for it (unless in silent mode)
462
+ if ((!isFeature || options.from) && !branchName) {
206
463
  if (silent) {
464
+ if (options.from) {
465
+ return yield* Effect.fail(
466
+ new Error(
467
+ `Branch name is required when using --from flag.\n` +
468
+ `Use: 'agency task <branch-name> --from ${options.from}'`,
469
+ ),
470
+ )
471
+ }
207
472
  return yield* Effect.fail(
208
473
  new Error(
209
474
  `You're currently on ${highlight.branch(currentBranch)}, which appears to be your main branch.\n` +
@@ -215,9 +480,7 @@ export const task = (options: TaskOptions = {}) =>
215
480
  }
216
481
  branchName = yield* promptService.prompt("Branch name: ")
217
482
  if (!branchName) {
218
- return yield* Effect.fail(
219
- new Error("Branch name is required when on main branch."),
220
- )
483
+ return yield* Effect.fail(new Error("Branch name is required."))
221
484
  }
222
485
  verboseLog(`Branch name from prompt: ${branchName}`)
223
486
  }
@@ -225,7 +488,6 @@ export const task = (options: TaskOptions = {}) =>
225
488
  // If we have a branch name, apply source pattern and check if branch exists
226
489
  let sourceBranchName: string | undefined
227
490
  if (branchName) {
228
- const config = yield* configService.loadConfig()
229
491
  sourceBranchName = makeSourceBranchName(
230
492
  branchName,
231
493
  config.sourceBranchPattern,
@@ -377,6 +639,18 @@ export const task = (options: TaskOptions = {}) =>
377
639
  continue
378
640
  }
379
641
 
642
+ // Check if this file is in a subdirectory that doesn't exist yet
643
+ // If so, we'll track it for glob pattern creation
644
+ const topLevelDir = getTopLevelDir(fileName)
645
+ if (topLevelDir && source === "template") {
646
+ const dirPath = resolve(targetPath, topLevelDir)
647
+ const dirExists = yield* fs.exists(dirPath)
648
+ if (!dirExists) {
649
+ createdDirs.add(topLevelDir)
650
+ verboseLog(`Will create new directory: ${topLevelDir}`)
651
+ }
652
+ }
653
+
380
654
  let content: string
381
655
 
382
656
  // Try to read from template first, fall back to default content
@@ -395,12 +669,24 @@ export const task = (options: TaskOptions = {}) =>
395
669
  verboseLog(`Replaced {task} placeholder with: ${taskDescription}`)
396
670
  }
397
671
 
672
+ // Ensure parent directory exists before writing file
673
+ const parentDir = dirname(targetFilePath)
674
+ const parentExists = yield* fs.exists(parentDir)
675
+ if (!parentExists) {
676
+ yield* fs.createDirectory(parentDir)
677
+ }
678
+
398
679
  yield* fs.writeFile(targetFilePath, content)
399
680
  createdFiles.push(fileName)
400
681
 
401
682
  // Track backpack files (excluding TASK.md and AGENCY.md which are always filtered)
683
+ // For files in new directories, the glob pattern will be added below
402
684
  if (fileName !== "TASK.md" && fileName !== "AGENCY.md") {
403
- injectedFiles.push(fileName)
685
+ // Only track individual files if they're NOT in a newly created directory
686
+ // (directories will be tracked as glob patterns)
687
+ if (!topLevelDir || !createdDirs.has(topLevelDir)) {
688
+ injectedFiles.push(fileName)
689
+ }
404
690
  }
405
691
 
406
692
  log(done(`Created ${highlight.file(fileName)}`))
@@ -423,34 +709,9 @@ export const task = (options: TaskOptions = {}) =>
423
709
  baseBranch =
424
710
  (yield* git.getDefaultBaseBranchConfig(targetPath)) || undefined
425
711
 
426
- // If no repo-level default, try to auto-detect
712
+ // If no repo-level default, try to auto-detect (preferring remote)
427
713
  if (!baseBranch) {
428
- // Try main branch config
429
- baseBranch =
430
- (yield* git.getMainBranchConfig(targetPath)) ||
431
- (yield* git.findMainBranch(targetPath)) ||
432
- undefined
433
-
434
- // Try common base branches with dynamic remote
435
- if (!baseBranch) {
436
- const remote = yield* git
437
- .resolveRemote(targetPath)
438
- .pipe(Effect.catchAll(() => Effect.succeed(null)))
439
-
440
- const commonBases: string[] = []
441
- if (remote) {
442
- commonBases.push(`${remote}/main`, `${remote}/master`)
443
- }
444
- commonBases.push("main", "master")
445
-
446
- for (const base of commonBases) {
447
- const exists = yield* git.branchExists(targetPath, base)
448
- if (exists) {
449
- baseBranch = base
450
- break
451
- }
452
- }
453
- }
714
+ baseBranch = (yield* git.resolveMainBranch(targetPath)) || undefined
454
715
  }
455
716
 
456
717
  if (baseBranch) {
@@ -459,12 +720,19 @@ export const task = (options: TaskOptions = {}) =>
459
720
 
460
721
  // Calculate emitBranch name from current branch
461
722
  const finalBranch = yield* git.getCurrentBranch(targetPath)
462
- const config = yield* configService.loadConfig()
463
723
  // Extract clean branch name from source pattern, or use branch as-is for legacy branches
464
724
  const cleanBranch =
465
725
  extractCleanBranch(finalBranch, config.sourceBranchPattern) || finalBranch
466
726
  const emitBranchName = makeEmitBranchName(cleanBranch, config.emitBranch)
467
727
 
728
+ // Convert created directories to glob patterns and add to injectedFiles
729
+ // This allows filtering all files in new directories during emit
730
+ for (const dir of createdDirs) {
731
+ const globPattern = dirToGlobPattern(dir)
732
+ injectedFiles.push(globPattern)
733
+ verboseLog(`Tracking directory as glob pattern: ${globPattern}`)
734
+ }
735
+
468
736
  // Create agency.json metadata file
469
737
  const metadata = {
470
738
  version: 1 as const,
@@ -489,7 +757,11 @@ export const task = (options: TaskOptions = {}) =>
489
757
  if (createdFiles.length > 0) {
490
758
  yield* Effect.gen(function* () {
491
759
  yield* git.gitAdd(createdFiles, targetPath)
492
- yield* git.gitCommit("chore: agency task", targetPath, {
760
+ // Format: chore: agency task (baseBranch) sourceBranch → emitBranch
761
+ const commitMessage = baseBranch
762
+ ? `chore: agency task (${baseBranch}) ${finalBranch} → ${emitBranchName}`
763
+ : `chore: agency task ${finalBranch} → ${emitBranchName}`
764
+ yield* git.gitCommit(commitMessage, targetPath, {
493
765
  noVerify: true,
494
766
  })
495
767
  verboseLog(
@@ -519,14 +791,11 @@ const createFeatureBranchEffect = (
519
791
  const git = yield* GitService
520
792
  const promptService = yield* PromptService
521
793
 
522
- // Use provided base branch if available, otherwise get or prompt for one
794
+ // Use provided base branch if available, otherwise resolve (preferring remote)
523
795
  let baseBranch: string | undefined = providedBaseBranch
524
796
 
525
797
  if (!baseBranch) {
526
- baseBranch =
527
- (yield* git.getMainBranchConfig(targetPath)) ||
528
- (yield* git.findMainBranch(targetPath)) ||
529
- undefined
798
+ baseBranch = (yield* git.resolveMainBranch(targetPath)) || undefined
530
799
  }
531
800
 
532
801
  // If no base branch is configured and not in silent mode, prompt for it
@@ -538,13 +807,6 @@ const createFeatureBranchEffect = (
538
807
  suggestions,
539
808
  )
540
809
  verboseLog(`Selected base branch: ${baseBranch}`)
541
-
542
- // Save the main branch config if it's not already set
543
- const mainBranchConfig = yield* git.getMainBranchConfig(targetPath)
544
- if (!mainBranchConfig) {
545
- yield* git.setMainBranchConfig(baseBranch, targetPath)
546
- log(done(`Set main branch to ${highlight.branch(baseBranch)}`))
547
- }
548
810
  } else {
549
811
  return yield* Effect.fail(
550
812
  new Error(
@@ -607,7 +869,6 @@ const taskEditEffect = (options: TaskEditOptions = {}) =>
607
869
  Effect.gen(function* () {
608
870
  const { log, verboseLog } = createLoggers(options)
609
871
 
610
- const git = yield* GitService
611
872
  const fs = yield* FileSystemService
612
873
 
613
874
  const gitRoot = yield* ensureGitRepo()
@@ -644,6 +905,22 @@ const taskEditEffect = (options: TaskEditOptions = {}) =>
644
905
  log(done("TASK.md edited"))
645
906
  })
646
907
 
908
+ export const editHelp = `
909
+ Usage: agency edit [options]
910
+
911
+ Open TASK.md in the system editor for editing.
912
+
913
+ Notes:
914
+ - Requires TASK.md to exist (run 'agency task' first)
915
+ - Respects VISUAL and EDITOR environment variables
916
+ - On macOS, defaults to 'open' which uses the default app for .md files
917
+ - On other platforms, defaults to 'vim'
918
+ - The command waits for the editor to close before returning
919
+
920
+ Example:
921
+ agency edit # Open TASK.md in default editor
922
+ `
923
+
647
924
  export const help = `
648
925
  Usage: agency task [branch-name] [options]
649
926
 
@@ -663,9 +940,23 @@ Arguments:
663
940
  branch-name Create and switch to this branch before initializing
664
941
 
665
942
  Options:
666
- -b, --branch Branch name to create (alternative to positional arg)
943
+ --emit Branch name to create (alternative to positional arg)
944
+ --branch (Deprecated: use --emit) Branch name to create
667
945
  --from <branch> Branch to branch from instead of main upstream branch
668
946
  --from-current Branch from the current branch
947
+ --continue Continue a task by copying agency files to a new branch
948
+
949
+ Continue Mode (--continue):
950
+ After a PR is merged, use '--continue' to create a new branch that preserves
951
+ your agency files (TASK.md, AGENCY.md, opencode.json, agency.json, and all
952
+ backpacked files). This allows you to continue working on a task after its
953
+ PR has been merged to main.
954
+
955
+ The continue workflow:
956
+ 1. Be on an agency source branch with agency files
957
+ 2. Run 'agency task --continue <new-branch-name>'
958
+ 3. A new branch is created from main with all your agency files
959
+ 4. The emitBranch in agency.json is updated for the new branch
669
960
 
670
961
  Base Branch Selection:
671
962
  By default, 'agency task' branches from the main upstream branch (e.g., origin/main).
@@ -683,6 +974,7 @@ Examples:
683
974
  agency task --from agency/branch-B # Branch from agency/branch-B's emit branch
684
975
  agency task --from-current # Branch from current branch's emit branch
685
976
  agency task my-feature --from develop # Create 'my-feature' from 'develop'
977
+ agency task --continue my-feature-v2 # Continue task on new branch after PR merge
686
978
 
687
979
  Template Workflow:
688
980
  1. Run 'agency init' to select template (saved to .git/config)
@@ -4,6 +4,7 @@ import { Schema } from "@effect/schema"
4
4
  import { AgencyMetadata } from "../schemas"
5
5
  import { FileSystemService } from "./FileSystemService"
6
6
  import { GitService } from "./GitService"
7
+ import { expandGlobs } from "../utils/glob"
7
8
 
8
9
  /**
9
10
  * Error type for AgencyMetadata operations
@@ -184,7 +185,14 @@ export const AgencyMetadataServiceLive = Layer.succeed(
184
185
  return baseFiles
185
186
  }
186
187
 
187
- return [...baseFiles, ...metadata.injectedFiles]
188
+ // Expand any glob patterns in injectedFiles to actual file paths
189
+ const expandedFiles = yield* Effect.tryPromise({
190
+ try: () =>
191
+ expandGlobs([...baseFiles, ...metadata.injectedFiles], gitRoot),
192
+ catch: () => new Error("Failed to expand glob patterns"),
193
+ })
194
+
195
+ return expandedFiles
188
196
  }).pipe(
189
197
  Effect.catchAll(() =>
190
198
  Effect.succeed(["TASK.md", "AGENCY.md", "agency.json"]),