authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-08-30 18:02:10-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-30 18:02:10-04:00
log60f2ce0279d524bd70a573f6e0b6e9fab56e4b3e
treefc4574ab7b40de5baedbd5d02bd9dce5e664decd
parent8f525af5afa93dff58775030d9b503d3bda36b7b
signature Signed by PGP key B5690EEEBB952194

fix(git): follow repository instructions in generated source control text (#8804)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

4 files changed, 55 insertions(+), 18 deletions(-)

apps/server/src/git/GitManager.test.ts+11-5
......@@ -1967,18 +1967,26 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
19671967 }),
19681968 );
19691969
1970 it.effect("preserves repository conventions style when recent history is empty", () =>
1970 it.effect("includes local agent instructions when recent history is empty", () =>
19711971 Effect.gen(function* () {
19721972 const repoDir = yield* makeTempDir("t3code-git-manager-");
19731973 yield* runGit(repoDir, ["init", "--initial-branch=main"]);
19741974 yield* runGit(repoDir, ["config", "user.email", "test@example.com"]);
19751975 yield* runGit(repoDir, ["config", "user.name", "Test User"]);
1976 const agentInstructions = "Use lowercase source control text.";
1977 const claudeInstructions = "Keep pull request bodies brief.";
1978 NodeFS.writeFileSync(NodePath.join(repoDir, "AGENTS.md"), agentInstructions);
1979 NodeFS.writeFileSync(NodePath.join(repoDir, "CLAUDE.md"), claudeInstructions);
19761980 NodeFS.writeFileSync(NodePath.join(repoDir, "README.md"), "hello\n");
19771981 yield* runGit(repoDir, ["add", "README.md"]);
19781982 let generatedPolicy: TextGeneration.CommitMessageGenerationInput["policy"] = undefined;
19791983
19801984 const { manager } = yield* makeManager({
19811985 serverSettings: {
1986 textGenerationModelSelection: {
1987 instanceId: ProviderInstanceId.make("claudeAgent"),
1988 model: "claude-sonnet-4-6",
1989 },
19821990 sourceControlWritingStyle: {
19831991 mode: "repo_conventions" as const,
19841992 },
......@@ -1997,10 +2005,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
19972005
19982006 expect(generatedPolicy).toEqual({
19992007 kind: "repo_conventions",
2000 commitInstructions:
2001 "Follow the repository's established commit message style when examples are available.",
2002 changeRequestInstructions:
2003 "Follow the repository's established change request title and body style when examples are available.",
2008 commitInstructions: `Follow the repository's established commit message style when examples are available.\n\nLocal AGENTS.md:\n${agentInstructions}\n\nLocal CLAUDE.md:\n${claudeInstructions}`,
2009 changeRequestInstructions: `Follow the repository's established change request title and body style when examples are available.\n\nLocal AGENTS.md:\n${agentInstructions}\n\nLocal CLAUDE.md:\n${claudeInstructions}`,
20042010 inferRepositoryConventions: true,
20052011 });
20062012 }),
apps/server/src/git/GitManager.ts+41-12
......@@ -606,9 +606,24 @@ export const make = Effect.gen(function* () {
606606 const providerRegistry = yield* ProviderRegistry.ProviderRegistry;
607607 const projectSetupScriptRunner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
608608 const crypto = yield* Crypto.Crypto;
609 const fileSystem = yield* FileSystem.FileSystem;
610 const path = yield* Path.Path;
609611
610612 const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd });
611613 const serverSettingsService = yield* ServerSettings.ServerSettingsService;
614 const readRepositoryInstructions = (cwd: string, fileName: string) =>
615 Effect.gen(function* () {
616 const root = yield* fileSystem.realPath(cwd);
617 const instructionPath = yield* fileSystem.realPath(path.join(root, fileName));
618 if (!instructionPath.startsWith(`${root}${path.sep}`)) {
619 return "";
620 }
621 const info = yield* fileSystem.stat(instructionPath);
622 if (info.type !== "File" || info.size > FileSystem.Size(20_000)) {
623 return "";
624 }
625 return (yield* fileSystem.readFileString(instructionPath)).trim();
626 }).pipe(Effect.orElseSucceed(() => ""));
612627
613628 const readRecentCommitSubjects = (cwd: string) =>
614629 gitCore
......@@ -627,26 +642,43 @@ export const make = Effect.gen(function* () {
627642 Effect.orElseSucceed(() => []),
628643 );
629644
630 const resolveStylePolicy = (cwd: string, style: SourceControlWritingStyleSettings) =>
645 const resolveStylePolicy = (cwd: string, settings: SourceControlTextGenerationSettings) =>
631646 Effect.gen(function* () {
632 switch (style.mode) {
647 switch (settings.style.mode) {
633648 case "conventional_commits":
634649 return conventionalCommitsTextGenerationPolicy;
635650 case "custom":
636651 return customTextGenerationPolicy(
637 style.customInstructions
652 settings.style.customInstructions
638653 ? {
639 commitInstructions: style.customInstructions,
640 changeRequestInstructions: style.customInstructions,
654 commitInstructions: settings.style.customInstructions,
655 changeRequestInstructions: settings.style.customInstructions,
641656 }
642657 : {},
643658 );
644659 case "repo_conventions": {
645660 const subjects = yield* readRecentCommitSubjects(cwd);
646 if (subjects.length === 0) {
661 const agentInstructions = yield* readRepositoryInstructions(cwd, "AGENTS.md");
662 const isClaudeWriter =
663 settings.modelSelection.instanceId === "claudeAgent" ||
664 (yield* providerRegistry.getProviders).some(
665 (provider) =>
666 provider.instanceId === settings.modelSelection.instanceId &&
667 provider.driver === "claudeAgent",
668 );
669 const claudeInstructions = isClaudeWriter
670 ? yield* readRepositoryInstructions(cwd, "CLAUDE.md")
671 : "";
672 const examples = [
673 ...(subjects.length > 0
674 ? [["Recent commit subjects from this repository:", ...subjects].join("\n")]
675 : []),
676 ...(agentInstructions ? [`Local AGENTS.md:\n${agentInstructions}`] : []),
677 ...(claudeInstructions ? [`Local CLAUDE.md:\n${claudeInstructions}`] : []),
678 ].join("\n\n");
679 if (!examples) {
647680 return repositoryConventionsTextGenerationPolicy;
648681 }
649 const examples = ["Recent commit subjects from this repository:", ...subjects].join("\n");
650682 return {
651683 ...repositoryConventionsTextGenerationPolicy,
652684 commitInstructions: `${repositoryConventionsTextGenerationPolicy.commitInstructions}\n\n${examples}`,
......@@ -848,9 +880,6 @@ export const make = Effect.gen(function* () {
848880 ),
849881 ),
850882 );
851 const fileSystem = yield* FileSystem.FileSystem;
852 const path = yield* Path.Path;
853
854883 const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp";
855884 const canonicalizeExistingPath = (value: string) =>
856885 fileSystem.realPath(value).pipe(Effect.orElseSucceed(() => value));
......@@ -1565,7 +1594,7 @@ export const make = Effect.gen(function* () {
15651594 };
15661595 }
15671596
1568 const policy = yield* resolveStylePolicy(input.cwd, input.settings.style);
1597 const policy = yield* resolveStylePolicy(input.cwd, input.settings);
15691598
15701599 const generated = yield* textGeneration
15711600 .generateCommitMessage({
......@@ -1751,7 +1780,7 @@ export const make = Effect.gen(function* () {
17511780 });
17521781 const baseRangeRef = yield* resolveBaseRangeRef(cwd, baseBranch);
17531782 const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef);
1754 const policy = yield* resolveStylePolicy(cwd, settings.style);
1783 const policy = yield* resolveStylePolicy(cwd, settings);
17551784 const changeRequestTemplate =
17561785 settings.style.followChangeRequestTemplates && provider.kind === "github"
17571786 ? Option.getOrUndefined(yield* detectPrTemplate(cwd, baseRangeRef, gitCore.execute))
apps/server/src/textGeneration/TextGenerationPrompts.ts+1-1
......@@ -16,7 +16,7 @@ const EARLIER_CONTENT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";
1616
1717function policyInstruction(instruction: string | undefined): ReadonlyArray<string> {
1818 const trimmed = instruction?.trim();
19 return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 4_000)] : [];
19 return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 20_000)] : [];
2020}
2121
2222// ---------------------------------------------------------------------------
docs/user/source-control.md+2
......@@ -33,6 +33,8 @@ T3 Code works with the platforms your team already uses:
3333
3434- Push a branch and create a pull request from the Git actions controls in the toolbar
3535- T3 Code can suggest titles and descriptions based on your commits
36- With **Repository conventions** selected, generated source control text follows the project's
37 `AGENTS.md` along with recent commit subjects. Claude writers also follow `CLAUDE.md`
3638- Supports GitHub Pull Requests, GitLab Merge Requests, Bitbucket Pull Requests, and Azure DevOps Pull Requests
3739
3840**Stay on top of open reviews**