diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 6837d849d779..c92649dcf96d 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -2949,6 +2949,93 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { }), ); + it.effect("sends identical AGENTS.md and CLAUDE.md content to a Claude writer once", () => + Effect.gen(function* () { + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* runGit(repoDir, ["init", "--initial-branch=main"]); + yield* runGit(repoDir, ["config", "user.email", "test@example.com"]); + yield* runGit(repoDir, ["config", "user.name", "Test User"]); + const instructions = "Use lowercase source control text."; + // A byte-identical copy stands in for the common `CLAUDE.md -> AGENTS.md` + // symlink: both read to the same string, which is what the dedup compares. + NodeFS.writeFileSync(NodePath.join(repoDir, "AGENTS.md"), instructions); + NodeFS.writeFileSync(NodePath.join(repoDir, "CLAUDE.md"), instructions); + NodeFS.writeFileSync(NodePath.join(repoDir, "README.md"), "hello\n"); + yield* runGit(repoDir, ["add", "README.md"]); + let generatedPolicy: TextGeneration.CommitMessageGenerationInput["policy"] = undefined; + + const { manager } = yield* makeManager({ + serverSettings: { + textGenerationModelSelection: { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-4-6", + }, + sourceControlWritingStyle: { + mode: "repo_conventions" as const, + }, + }, + textGeneration: { + generateCommitMessage: (input) => { + generatedPolicy = input.policy; + return Effect.succeed({ subject: "Create initial commit", body: "" }); + }, + }, + }); + yield* runStackedAction(manager, { + cwd: repoDir, + action: "commit", + }); + + expect(generatedPolicy).toEqual({ + kind: "repo_conventions", + commitInstructions: `Follow the repository's established commit message style when examples are available.\n\nLocal AGENTS.md:\n${instructions}`, + changeRequestInstructions: `Follow the repository's established change request title and body style when examples are available.\n\nLocal AGENTS.md:\n${instructions}`, + inferRepositoryConventions: true, + }); + }), + ); + + it.effect("omits CLAUDE.md from repository conventions for a non-Claude writer", () => + Effect.gen(function* () { + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* runGit(repoDir, ["init", "--initial-branch=main"]); + yield* runGit(repoDir, ["config", "user.email", "test@example.com"]); + yield* runGit(repoDir, ["config", "user.name", "Test User"]); + const agentInstructions = "Use lowercase source control text."; + const claudeInstructions = "Keep pull request bodies brief."; + NodeFS.writeFileSync(NodePath.join(repoDir, "AGENTS.md"), agentInstructions); + NodeFS.writeFileSync(NodePath.join(repoDir, "CLAUDE.md"), claudeInstructions); + NodeFS.writeFileSync(NodePath.join(repoDir, "README.md"), "hello\n"); + yield* runGit(repoDir, ["add", "README.md"]); + let generatedPolicy: TextGeneration.CommitMessageGenerationInput["policy"] = undefined; + + const { manager } = yield* makeManager({ + serverSettings: { + sourceControlWritingStyle: { + mode: "repo_conventions" as const, + }, + }, + textGeneration: { + generateCommitMessage: (input) => { + generatedPolicy = input.policy; + return Effect.succeed({ subject: "Create initial commit", body: "" }); + }, + }, + }); + yield* runStackedAction(manager, { + cwd: repoDir, + action: "commit", + }); + + expect(generatedPolicy).toEqual({ + kind: "repo_conventions", + commitInstructions: `Follow the repository's established commit message style when examples are available.\n\nLocal AGENTS.md:\n${agentInstructions}`, + changeRequestInstructions: `Follow the repository's established change request title and body style when examples are available.\n\nLocal AGENTS.md:\n${agentInstructions}`, + inferRepositoryConventions: true, + }); + }), + ); + it.effect("uses custom commit message when provided", () => Effect.gen(function* () { const repoDir = yield* makeTempDir("t3code-git-manager-"); diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 033cc192ccfe..d8d34a4354e5 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -755,9 +755,14 @@ export const make = Effect.gen(function* () { provider.instanceId === settings.modelSelection.instanceId && provider.driver === "claudeAgent", ); - const claudeInstructions = isClaudeWriter + const claudeInstructionsRead = isClaudeWriter ? yield* readRepositoryInstructions(cwd, "CLAUDE.md") : ""; + // CLAUDE.md is commonly a symlink to, or a copy of, AGENTS.md. Both + // reads then return the same text, and the writer would be sent it + // twice. Comparing content covers symlinks, hardlinks and copies. + const claudeInstructions = + claudeInstructionsRead === agentInstructions ? "" : claudeInstructionsRead; const examples = [ ...(subjects.length > 0 ? [["Recent commit subjects from this repository:", ...subjects].join("\n")]