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) => {...@@ -1967,18 +1967,26 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1967 }),1967 }),
1968 );1968 );
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", () =>
1971 Effect.gen(function* () {1971 Effect.gen(function* () {
1972 const repoDir = yield* makeTempDir("t3code-git-manager-");1972 const repoDir = yield* makeTempDir("t3code-git-manager-");
1973 yield* runGit(repoDir, ["init", "--initial-branch=main"]);1973 yield* runGit(repoDir, ["init", "--initial-branch=main"]);
1974 yield* runGit(repoDir, ["config", "user.email", "test@example.com"]);1974 yield* runGit(repoDir, ["config", "user.email", "test@example.com"]);
1975 yield* runGit(repoDir, ["config", "user.name", "Test User"]);1975 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);
1976 NodeFS.writeFileSync(NodePath.join(repoDir, "README.md"), "hello\n");1980 NodeFS.writeFileSync(NodePath.join(repoDir, "README.md"), "hello\n");
1977 yield* runGit(repoDir, ["add", "README.md"]);1981 yield* runGit(repoDir, ["add", "README.md"]);
1978 let generatedPolicy: TextGeneration.CommitMessageGenerationInput["policy"] = undefined;1982 let generatedPolicy: TextGeneration.CommitMessageGenerationInput["policy"] = undefined;
19791983
1980 const { manager } = yield* makeManager({1984 const { manager } = yield* makeManager({
1981 serverSettings: {1985 serverSettings: {
1986 textGenerationModelSelection: {
1987 instanceId: ProviderInstanceId.make("claudeAgent"),
1988 model: "claude-sonnet-4-6",
1989 },
1982 sourceControlWritingStyle: {1990 sourceControlWritingStyle: {
1983 mode: "repo_conventions" as const,1991 mode: "repo_conventions" as const,
1984 },1992 },
...@@ -1997,10 +2005,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1997,10 +2005,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
19972005
1998 expect(generatedPolicy).toEqual({2006 expect(generatedPolicy).toEqual({
1999 kind: "repo_conventions",2007 kind: "repo_conventions",
2000 commitInstructions: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}`,
2001 "Follow the repository's established commit message style when examples are available.",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}`,
2002 changeRequestInstructions:
2003 "Follow the repository's established change request title and body style when examples are available.",
2004 inferRepositoryConventions: true,2010 inferRepositoryConventions: true,
2005 });2011 });
2006 }),2012 }),
apps/server/src/git/GitManager.ts+41-12
...@@ -606,9 +606,24 @@ export const make = Effect.gen(function* () {...@@ -606,9 +606,24 @@ export const make = Effect.gen(function* () {
606 const providerRegistry = yield* ProviderRegistry.ProviderRegistry;606 const providerRegistry = yield* ProviderRegistry.ProviderRegistry;
607 const projectSetupScriptRunner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;607 const projectSetupScriptRunner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
608 const crypto = yield* Crypto.Crypto;608 const crypto = yield* Crypto.Crypto;
609 const fileSystem = yield* FileSystem.FileSystem;
610 const path = yield* Path.Path;
609611
610 const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd });612 const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd });
611 const serverSettingsService = yield* ServerSettings.ServerSettingsService;613 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
613 const readRecentCommitSubjects = (cwd: string) =>628 const readRecentCommitSubjects = (cwd: string) =>
614 gitCore629 gitCore
...@@ -627,26 +642,43 @@ export const make = Effect.gen(function* () {...@@ -627,26 +642,43 @@ export const make = Effect.gen(function* () {
627 Effect.orElseSucceed(() => []),642 Effect.orElseSucceed(() => []),
628 );643 );
629644
630 const resolveStylePolicy = (cwd: string, style: SourceControlWritingStyleSettings) =>645 const resolveStylePolicy = (cwd: string, settings: SourceControlTextGenerationSettings) =>
631 Effect.gen(function* () {646 Effect.gen(function* () {
632 switch (style.mode) {647 switch (settings.style.mode) {
633 case "conventional_commits":648 case "conventional_commits":
634 return conventionalCommitsTextGenerationPolicy;649 return conventionalCommitsTextGenerationPolicy;
635 case "custom":650 case "custom":
636 return customTextGenerationPolicy(651 return customTextGenerationPolicy(
637 style.customInstructions652 settings.style.customInstructions
638 ? {653 ? {
639 commitInstructions: style.customInstructions,654 commitInstructions: settings.style.customInstructions,
640 changeRequestInstructions: style.customInstructions,655 changeRequestInstructions: settings.style.customInstructions,
641 }656 }
642 : {},657 : {},
643 );658 );
644 case "repo_conventions": {659 case "repo_conventions": {
645 const subjects = yield* readRecentCommitSubjects(cwd);660 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) {
647 return repositoryConventionsTextGenerationPolicy;680 return repositoryConventionsTextGenerationPolicy;
648 }681 }
649 const examples = ["Recent commit subjects from this repository:", ...subjects].join("\n");
650 return {682 return {
651 ...repositoryConventionsTextGenerationPolicy,683 ...repositoryConventionsTextGenerationPolicy,
652 commitInstructions: `${repositoryConventionsTextGenerationPolicy.commitInstructions}\n\n${examples}`,684 commitInstructions: `${repositoryConventionsTextGenerationPolicy.commitInstructions}\n\n${examples}`,
...@@ -848,9 +880,6 @@ export const make = Effect.gen(function* () {...@@ -848,9 +880,6 @@ export const make = Effect.gen(function* () {
848 ),880 ),
849 ),881 ),
850 );882 );
851 const fileSystem = yield* FileSystem.FileSystem;
852 const path = yield* Path.Path;
853
854 const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp";883 const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp";
855 const canonicalizeExistingPath = (value: string) =>884 const canonicalizeExistingPath = (value: string) =>
856 fileSystem.realPath(value).pipe(Effect.orElseSucceed(() => value));885 fileSystem.realPath(value).pipe(Effect.orElseSucceed(() => value));
...@@ -1565,7 +1594,7 @@ export const make = Effect.gen(function* () {...@@ -1565,7 +1594,7 @@ export const make = Effect.gen(function* () {
1565 };1594 };
1566 }1595 }
15671596
1568 const policy = yield* resolveStylePolicy(input.cwd, input.settings.style);1597 const policy = yield* resolveStylePolicy(input.cwd, input.settings);
15691598
1570 const generated = yield* textGeneration1599 const generated = yield* textGeneration
1571 .generateCommitMessage({1600 .generateCommitMessage({
...@@ -1751,7 +1780,7 @@ export const make = Effect.gen(function* () {...@@ -1751,7 +1780,7 @@ export const make = Effect.gen(function* () {
1751 });1780 });
1752 const baseRangeRef = yield* resolveBaseRangeRef(cwd, baseBranch);1781 const baseRangeRef = yield* resolveBaseRangeRef(cwd, baseBranch);
1753 const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef);1782 const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef);
1754 const policy = yield* resolveStylePolicy(cwd, settings.style);1783 const policy = yield* resolveStylePolicy(cwd, settings);
1755 const changeRequestTemplate =1784 const changeRequestTemplate =
1756 settings.style.followChangeRequestTemplates && provider.kind === "github"1785 settings.style.followChangeRequestTemplates && provider.kind === "github"
1757 ? Option.getOrUndefined(yield* detectPrTemplate(cwd, baseRangeRef, gitCore.execute))1786 ? 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";...@@ -16,7 +16,7 @@ const EARLIER_CONTENT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";
1616
17function policyInstruction(instruction: string | undefined): ReadonlyArray<string> {17function policyInstruction(instruction: string | undefined): ReadonlyArray<string> {
18 const trimmed = instruction?.trim();18 const trimmed = instruction?.trim();
19 return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 4_000)] : [];19 return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 20_000)] : [];
20}20}
2121
22// ---------------------------------------------------------------------------22// ---------------------------------------------------------------------------
docs/user/source-control.md+2
...@@ -33,6 +33,8 @@ T3 Code works with the platforms your team already uses:...@@ -33,6 +33,8 @@ T3 Code works with the platforms your team already uses:
3333
34- Push a branch and create a pull request from the Git actions controls in the toolbar34- Push a branch and create a pull request from the Git actions controls in the toolbar
35- T3 Code can suggest titles and descriptions based on your commits35- 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`
36- Supports GitHub Pull Requests, GitLab Merge Requests, Bitbucket Pull Requests, and Azure DevOps Pull Requests38- Supports GitHub Pull Requests, GitLab Merge Requests, Bitbucket Pull Requests, and Azure DevOps Pull Requests
3739
38**Stay on top of open reviews**40**Stay on top of open reviews**