@markjaquith/agency 0.6.0 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Manages personal agents files",
5
5
  "license": "MIT",
6
6
  "author": "Mark Jaquith",
@@ -14,7 +14,7 @@
14
14
  "agency": "cli.ts"
15
15
  },
16
16
  "scripts": {
17
- "test": "bun test",
17
+ "test": "find src -name '*.test.ts' -print0 | xargs -0 -n 1 -P 4 bun test",
18
18
  "format": "prettier --write .",
19
19
  "format:check": "prettier --check .",
20
20
  "knip": "knip --production",
@@ -377,6 +377,76 @@ describe("emit command", () => {
377
377
  expect(logOutput).toContain("Add feature file")
378
378
  expect(logOutput).not.toContain("Add AGENTS.md")
379
379
  })
380
+
381
+ test("filters pre-existing CLAUDE.md that gets edited by agency", async () => {
382
+ if (!hasGitFilterRepo) {
383
+ console.log("Skipping test: git-filter-repo not installed")
384
+ return
385
+ }
386
+
387
+ // Start fresh on main branch
388
+ await checkoutBranch(tempDir, "main")
389
+
390
+ // Create CLAUDE.md on main branch (simulating pre-existing file)
391
+ await Bun.write(
392
+ join(tempDir, "CLAUDE.md"),
393
+ "# Original Claude Instructions\n\nSome content here.\n",
394
+ )
395
+ await addAndCommit(tempDir, "CLAUDE.md", "Add CLAUDE.md")
396
+
397
+ // Create a new feature branch
398
+ await createBranch(tempDir, "agency/claude-test")
399
+
400
+ // Initialize agency on this branch (this will modify CLAUDE.md)
401
+ await Bun.write(
402
+ join(tempDir, "agency.json"),
403
+ JSON.stringify({
404
+ version: 1,
405
+ injectedFiles: ["AGENTS.md"],
406
+ template: "test",
407
+ createdAt: new Date().toISOString(),
408
+ }),
409
+ )
410
+ await Bun.write(join(tempDir, "AGENTS.md"), "# Test AGENTS\n")
411
+
412
+ // Simulate what agency task does - inject into CLAUDE.md
413
+ const originalClaude = await Bun.file(join(tempDir, "CLAUDE.md")).text()
414
+ const modifiedClaude = `${originalClaude}\n# Agency References\n@AGENTS.md\n@TASK.md\n`
415
+ await Bun.write(join(tempDir, "CLAUDE.md"), modifiedClaude)
416
+
417
+ await addAndCommit(
418
+ tempDir,
419
+ "agency.json AGENTS.md CLAUDE.md",
420
+ "Initialize agency files",
421
+ )
422
+
423
+ // Add a feature file
424
+ await createCommit(tempDir, "Feature commit")
425
+
426
+ // Create emit branch (this should filter CLAUDE.md)
427
+ await runTestEffect(emit({ silent: true, baseBranch: "main" }))
428
+
429
+ // Should still be on source branch
430
+ const currentBranch = await getCurrentBranch(tempDir)
431
+ expect(currentBranch).toBe("agency/claude-test")
432
+
433
+ // Switch to emit branch to verify CLAUDE.md is reverted to main's version
434
+ await checkoutBranch(tempDir, "claude-test")
435
+
436
+ const files = await getGitOutput(tempDir, ["ls-files"])
437
+ expect(files).toContain("CLAUDE.md") // File should exist (from main)
438
+ expect(files).not.toContain("AGENTS.md") // Should be filtered
439
+ expect(files).not.toContain("TASK.md") // Should be filtered
440
+ expect(files).toContain("test.txt") // Feature file should exist
441
+
442
+ // Verify CLAUDE.md was reverted to original (no agency references)
443
+ const claudeContent = await Bun.file(join(tempDir, "CLAUDE.md")).text()
444
+ expect(claudeContent).toBe(
445
+ "# Original Claude Instructions\n\nSome content here.\n",
446
+ )
447
+ expect(claudeContent).not.toContain("@AGENTS.md")
448
+ expect(claudeContent).not.toContain("@TASK.md")
449
+ })
380
450
  })
381
451
 
382
452
  describe("error handling", () => {
@@ -278,23 +278,41 @@ const openGitHubPR = (
278
278
  const { verbose = false } = options
279
279
 
280
280
  // Run gh pr create --web with --head to specify the emit branch
281
- const ghResult = yield* spawnProcess(
281
+ // Set environment variables to ensure non-interactive mode in CI
282
+ // Add a 3 second timeout to prevent hanging in CI environments
283
+ const ghEffect = spawnProcess(
282
284
  ["gh", "pr", "create", "--web", "--head", branchName],
283
285
  {
284
286
  cwd: gitRoot,
285
287
  stdout: verbose ? "inherit" : "pipe",
286
288
  stderr: "pipe",
289
+ env: {
290
+ GH_PROMPT_DISABLED: "1",
291
+ NO_COLOR: "1",
292
+ },
287
293
  },
288
294
  ).pipe(
289
- Effect.catchAll((error) =>
290
- Effect.succeed({
291
- exitCode: error.exitCode,
295
+ Effect.timeout("3 seconds"),
296
+ Effect.catchAll((error) => {
297
+ // Handle timeout or process errors
298
+ if (error._tag === "TimeoutException") {
299
+ return Effect.succeed({
300
+ exitCode: -1,
301
+ stdout: "",
302
+ stderr: "gh command timed out after 3 seconds",
303
+ })
304
+ }
305
+ // Handle ProcessError
306
+ return Effect.succeed({
307
+ exitCode: (error as any).exitCode ?? -1,
292
308
  stdout: "",
293
- stderr: error.stderr,
294
- }),
295
- ),
309
+ stderr: (error as any).stderr ?? String(error),
310
+ })
311
+ }),
296
312
  )
297
313
 
314
+ const ghResult = yield* ghEffect
315
+
298
316
  if (ghResult.exitCode !== 0) {
299
317
  return yield* Effect.fail(
300
318
  new Error(`gh CLI command failed: ${ghResult.stderr.trim()}`),
@@ -759,6 +759,49 @@ describe("task command", () => {
759
759
 
760
760
  expect(await fileExists(join(tempDir, "AGENTS.md"))).toBe(true)
761
761
  })
762
+
763
+ test("fails when on agency source branch without branch name in silent mode", async () => {
764
+ await initGitRepo(tempDir)
765
+ process.chdir(tempDir)
766
+
767
+ await initAgency(tempDir, "test")
768
+
769
+ // Create a task branch with agency files
770
+ await runTestEffect(task({ silent: true, emit: "original-feature" }))
771
+
772
+ // Verify we're on the source branch with agency.json
773
+ const currentBranch = await getCurrentBranch(tempDir)
774
+ expect(currentBranch).toBe("agency/original-feature")
775
+ expect(await fileExists(join(tempDir, "agency.json"))).toBe(true)
776
+
777
+ // Try to run agency task again without providing a branch name
778
+ // This should fail because we're on an agency source branch
779
+ await expect(runTestEffect(task({ silent: true }))).rejects.toThrow(
780
+ "agency source branch",
781
+ )
782
+ })
783
+
784
+ test("prompts for branch name when on agency source branch", async () => {
785
+ await initGitRepo(tempDir)
786
+ process.chdir(tempDir)
787
+
788
+ await initAgency(tempDir, "test")
789
+
790
+ // Create a task branch with agency files
791
+ await runTestEffect(task({ silent: true, emit: "original-feature" }))
792
+
793
+ // Verify we're on the source branch with agency.json
794
+ const currentBranch = await getCurrentBranch(tempDir)
795
+ expect(currentBranch).toBe("agency/original-feature")
796
+ expect(await fileExists(join(tempDir, "agency.json"))).toBe(true)
797
+
798
+ // With a branch name provided, it should work
799
+ await runTestEffect(task({ silent: true, emit: "new-feature" }))
800
+
801
+ // Verify we're now on the new branch
802
+ const newBranch = await getCurrentBranch(tempDir)
803
+ expect(newBranch).toBe("agency/new-feature")
804
+ })
762
805
  })
763
806
 
764
807
  describe("template-based source files", () => {
@@ -212,11 +212,21 @@ const taskContinue = (options: TaskOptions) =>
212
212
  targetPath,
213
213
  baseBranchToBranchFrom,
214
214
  )
215
+ // Calculate the emit branch name for display
216
+ const cleanBranchForDisplay =
217
+ extractCleanBranch(sourceBranchName, config.sourceBranchPattern) ||
218
+ sourceBranchName
219
+ const emitBranchForDisplay = makeEmitBranchName(
220
+ cleanBranchForDisplay,
221
+ config.emitBranch,
222
+ )
223
+
215
224
  log(
216
- done(
217
- `Created and switched to branch ${highlight.branch(sourceBranchName)} based on ${highlight.branch(baseBranchToBranchFrom)}`,
225
+ info(
226
+ `(${highlight.branch(baseBranchToBranchFrom)}) ${highlight.branch(sourceBranchName)} ${highlight.branch(emitBranchForDisplay)}`,
218
227
  ),
219
228
  )
229
+ log(done(`Created and switched to ${highlight.branch(sourceBranchName)}`))
220
230
 
221
231
  // Calculate the new emit branch name
222
232
  const newEmitBranchName = makeEmitBranchName(branchName, config.emitBranch)
@@ -367,6 +377,11 @@ export const task = (options: TaskOptions = {}) =>
367
377
  const isFeature = yield* git.isFeatureBranch(currentBranch, targetPath)
368
378
  verboseLog(`Is feature branch: ${isFeature}`)
369
379
 
380
+ // Check if we're on an agency source branch (has agency.json with backpacked files)
381
+ const agencyJsonPath = resolve(targetPath, "agency.json")
382
+ const hasAgencyJson = yield* fs.exists(agencyJsonPath)
383
+ verboseLog(`Has agency.json: ${hasAgencyJson}`)
384
+
370
385
  // Determine base branch to branch from
371
386
  let baseBranchToBranchFrom: string | undefined
372
387
 
@@ -458,8 +473,8 @@ export const task = (options: TaskOptions = {}) =>
458
473
  // Determine branch name logic
459
474
  let branchName = options.emit || options.branch
460
475
 
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) {
476
+ // If on main branch, using --from, or on an agency source branch without a branch name, prompt for it (unless in silent mode)
477
+ if ((!isFeature || options.from || hasAgencyJson) && !branchName) {
463
478
  if (silent) {
464
479
  if (options.from) {
465
480
  return yield* Effect.fail(
@@ -469,6 +484,15 @@ export const task = (options: TaskOptions = {}) =>
469
484
  ),
470
485
  )
471
486
  }
487
+ if (hasAgencyJson) {
488
+ return yield* Effect.fail(
489
+ new Error(
490
+ `You're currently on ${highlight.branch(currentBranch)}, which is an agency source branch.\n` +
491
+ `Branch name is required when re-importing backpacked files.\n` +
492
+ `Use: 'agency task <branch-name>' or 'agency task --continue <branch-name>'`,
493
+ ),
494
+ )
495
+ }
472
496
  return yield* Effect.fail(
473
497
  new Error(
474
498
  `You're currently on ${highlight.branch(currentBranch)}, which appears to be your main branch.\n` +
@@ -823,11 +847,27 @@ const createFeatureBranchEffect = (
823
847
  }
824
848
 
825
849
  yield* git.createBranch(branchName, targetPath, baseBranch)
850
+
851
+ // Load config for emit pattern calculation
852
+ const configService = yield* ConfigService
853
+ const config = yield* configService.loadConfig()
854
+
855
+ // Calculate the emit branch name for display
856
+ const cleanBranchForDisplay =
857
+ extractCleanBranch(branchName, config.sourceBranchPattern) || branchName
858
+ const emitBranchForDisplay = makeEmitBranchName(
859
+ cleanBranchForDisplay,
860
+ config.emitBranch,
861
+ )
862
+
826
863
  log(
827
- done(
828
- `Created and switched to branch ${highlight.branch(branchName)}${baseBranch ? ` based on ${highlight.branch(baseBranch)}` : ""}`,
864
+ info(
865
+ baseBranch
866
+ ? `(${highlight.branch(baseBranch)}) ${highlight.branch(branchName)} → ${highlight.branch(emitBranchForDisplay)}`
867
+ : `${highlight.branch(branchName)} → ${highlight.branch(emitBranchForDisplay)}`,
829
868
  ),
830
869
  )
870
+ log(done(`Created and switched to ${highlight.branch(branchName)}`))
831
871
  })
832
872
 
833
873
  // Helper: Discover template files
@@ -49,7 +49,7 @@ export class AgencyMetadataService extends Context.Tag("AgencyMetadataService")<
49
49
 
50
50
  /**
51
51
  * Get list of files to filter during PR/merge operations.
52
- * Always includes TASK.md, AGENCY.md, and agency.json, plus any injectedFiles from metadata.
52
+ * Always includes TASK.md, AGENCY.md, CLAUDE.md, and agency.json, plus any injectedFiles from metadata.
53
53
  */
54
54
  readonly getFilesToFilter: (
55
55
  gitRoot: string,
@@ -165,7 +165,7 @@ export const AgencyMetadataServiceLive = Layer.succeed(
165
165
  const metadataPath = join(gitRoot, "agency.json")
166
166
 
167
167
  const exists = yield* fs.exists(metadataPath)
168
- const baseFiles = ["TASK.md", "AGENCY.md", "agency.json"]
168
+ const baseFiles = ["TASK.md", "AGENCY.md", "CLAUDE.md", "agency.json"]
169
169
 
170
170
  if (!exists) {
171
171
  return baseFiles
@@ -195,7 +195,7 @@ export const AgencyMetadataServiceLive = Layer.succeed(
195
195
  return expandedFiles
196
196
  }).pipe(
197
197
  Effect.catchAll(() =>
198
- Effect.succeed(["TASK.md", "AGENCY.md", "agency.json"]),
198
+ Effect.succeed(["TASK.md", "AGENCY.md", "CLAUDE.md", "agency.json"]),
199
199
  ),
200
200
  ),
201
201
 
package/src/types.ts CHANGED
@@ -160,7 +160,7 @@ export async function writeAgencyMetadata(
160
160
 
161
161
  /**
162
162
  * Get list of files to filter during PR/merge operations.
163
- * Always includes TASK.md, AGENCY.md, and agency.json, plus any backpack files from metadata.
163
+ * Always includes TASK.md, AGENCY.md, CLAUDE.md, and agency.json, plus any backpack files from metadata.
164
164
  * @deprecated Use AgencyMetadataService.getFilesToFilter instead
165
165
  */
166
166
  export async function getFilesToFilter(gitRoot: string): Promise<string[]> {
@@ -1,4 +1,4 @@
1
1
  {
2
- "$schema": "https://opencode.ai/config.json",
3
- "instructions": ["AGENCY.md", "TASK.md"]
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "instructions": ["AGENCY.md", "TASK.md"]
4
4
  }