feat: attribute Fusion as Co-authored-by trailer instead of primary author

Switch commits Fusion produces (both executor step commits and merger squash
commits) from setting `--author="Fusion <noreply@runfusion.ai>"` to appending
`-m "Co-authored-by: Fusion <noreply@runfusion.ai>"`. The user's configured
git identity now stays as the primary author/committer, and Fusion is recorded
as a co-author (recognized by GitHub for shared attribution). The
`commitAuthorEnabled` toggle and `commitAuthorName`/`commitAuthorEmail`
settings keep their existing keys; the dashboard settings UI relabels them
from "Author" to "Co-author" to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-22 20:38:11 -07:00
parent f58fb8955a
commit e291d86444
7 changed files with 53 additions and 38 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Attribute Fusion as a `Co-authored-by` trailer on managed commits instead of overriding the primary author. The user's configured git identity now remains the author/committer of every commit Fusion produces, and the configured commit author (default `Fusion <noreply@runfusion.ai>`) is appended as a `Co-authored-by:` trailer that GitHub recognizes for shared attribution. The `commitAuthorEnabled` toggle and `commitAuthorName`/`commitAuthorEmail` settings keep their existing keys; the dashboard settings UI relabels them from "Author" to "Co-author" to match the new behavior.

View File

@@ -2855,13 +2855,15 @@ export interface ProjectSettings {
* commit scope (e.g. `feat(KB-001): ...`). When false, the scope is
* omitted (e.g. `feat: ...`). Default: true. */
includeTaskIdInCommit?: boolean;
/** When true, fusion adds --author attribution to all commits it creates.
* When false, no author attribution is added. Default: true. */
/** When true, fusion appends a `Co-authored-by` trailer to all commits it
* creates so Fusion is credited alongside the user's git identity (which
* remains the primary author/committer). When false, no co-author trailer
* is added. Default: true. */
commitAuthorEnabled?: boolean;
/** Name used in the git --author flag for Fusion commits.
/** Name used in the `Co-authored-by` trailer for Fusion commits.
* Only used when commitAuthorEnabled is true. Default: "Fusion". */
commitAuthorName?: string;
/** Email used in the git --author flag for Fusion commits.
/** Email used in the `Co-authored-by` trailer for Fusion commits.
* Only used when commitAuthorEnabled is true. Default: "noreply@runfusion.ai". */
commitAuthorEmail?: string;
/** AI model provider for planning/triage (specification) agent.

View File

@@ -4752,13 +4752,14 @@ export function SettingsModal({
setForm((f) => ({ ...f, commitAuthorEnabled: e.target.checked }))
}
/>
Add author attribution to commits
Add Fusion as co-author on commits
</label>
<details className="settings-option-details">
<summary>More details</summary>
<small>
When enabled, all commits made by Fusion include <code>--author</code>{" "}
attribution identifying them as AI-generated
When enabled, commits made by Fusion keep your git identity as the
primary author and append a <code>Co-authored-by</code> trailer crediting
Fusion (recognized by GitHub for shared attribution).
</small>
</details>
</div>
@@ -4766,7 +4767,7 @@ export function SettingsModal({
{form.commitAuthorEnabled !== false && (
<>
<div className="form-group">
<label htmlFor="commitAuthorName">Author Name</label>
<label htmlFor="commitAuthorName">Co-author Name</label>
<input
id="commitAuthorName"
type="text"
@@ -4779,10 +4780,10 @@ export function SettingsModal({
}))
}
/>
<small>Name used in commit author attribution</small>
<small>Name used in the <code>Co-authored-by</code> trailer</small>
</div>
<div className="form-group">
<label htmlFor="commitAuthorEmail">Author Email</label>
<label htmlFor="commitAuthorEmail">Co-author Email</label>
<input
id="commitAuthorEmail"
type="email"
@@ -4795,7 +4796,7 @@ export function SettingsModal({
}))
}
/>
<small>Email used in commit author attribution</small>
<small>Email used in the <code>Co-authored-by</code> trailer</small>
</div>
</>
)}

View File

@@ -580,55 +580,57 @@ describe("buildExecutionPrompt", () => {
});
});
describe("commit author attribution", () => {
it("includes default author in commit instruction when commitAuthorEnabled is true", () => {
describe("commit co-author attribution", () => {
it("includes default co-author trailer in commit instruction when commitAuthorEnabled is true", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
commitAuthorEnabled: true,
} as any);
expect(result).toContain('--author="Fusion <noreply@runfusion.ai>"');
expect(result).toContain('-m "Co-authored-by: Fusion <noreply@runfusion.ai>"');
expect(result).not.toContain("--author=");
});
it("includes custom author name and email in commit instruction", () => {
it("includes custom co-author name and email in commit instruction", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
commitAuthorEnabled: true,
commitAuthorName: "CustomBot",
commitAuthorEmail: "bot@example.com",
} as any);
expect(result).toContain('--author="CustomBot <bot@example.com>"');
expect(result).toContain('-m "Co-authored-by: CustomBot <bot@example.com>"');
});
it("omits author from commit instruction when commitAuthorEnabled is false", () => {
it("omits co-author trailer from commit instruction when commitAuthorEnabled is false", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
commitAuthorEnabled: false,
} as any);
expect(result).not.toContain("Co-authored-by");
expect(result).not.toContain("--author");
// Should still contain commit instruction without author
// Should still contain commit instruction without co-author
expect(result).toContain("git commit -m");
});
it("uses default author when commitAuthorEnabled is true but name/email are undefined", () => {
it("uses default co-author when commitAuthorEnabled is true but name/email are undefined", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {
commitAuthorEnabled: true,
commitAuthorName: undefined,
commitAuthorEmail: undefined,
} as any);
expect(result).toContain('--author="Fusion <noreply@runfusion.ai>"');
expect(result).toContain('-m "Co-authored-by: Fusion <noreply@runfusion.ai>"');
});
it("uses default author when settings is undefined", () => {
it("uses default co-author when settings is undefined", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project");
expect(result).toContain('--author="Fusion <noreply@runfusion.ai>"');
expect(result).toContain('-m "Co-authored-by: Fusion <noreply@runfusion.ai>"');
});
it("uses default author when settings is empty object", () => {
it("uses default co-author when settings is empty object", () => {
const task = createMockTaskDetail();
const result = buildExecutionPrompt(task, "/project", {} as any);
expect(result).toContain('--author="Fusion <noreply@runfusion.ai>"');
expect(result).toContain('-m "Co-authored-by: Fusion <noreply@runfusion.ai>"');
});
});
});

View File

@@ -789,7 +789,7 @@ describe("buildMergePrompt — truncation behavior", () => {
expect(prompt).toContain(shortDiffStat);
});
it("includes author arg in no-conflicts commit instruction", async () => {
it("includes co-author trailer arg in no-conflicts commit instruction", async () => {
const { buildMergePrompt } = await import("../merger.js");
const prompt = buildMergePrompt({
@@ -798,13 +798,13 @@ describe("buildMergePrompt — truncation behavior", () => {
commitLog: "- feat: something",
diffStat: "1 file changed",
hasConflicts: false,
authorArg: ' --author="Fusion <noreply@runfusion.ai>"',
authorArg: ' -m "Co-authored-by: Fusion <noreply@runfusion.ai>"',
});
expect(prompt).toContain('Be sure to include `--author="Fusion <noreply@runfusion.ai>"` in the commit command');
expect(prompt).toContain('Be sure to append `-m "Co-authored-by: Fusion <noreply@runfusion.ai>"` to the commit command');
});
it("includes author arg in conflicts commit instruction", async () => {
it("includes co-author trailer arg in conflicts commit instruction", async () => {
const { buildMergePrompt } = await import("../merger.js");
const prompt = buildMergePrompt({
@@ -813,10 +813,10 @@ describe("buildMergePrompt — truncation behavior", () => {
commitLog: "- feat: something",
diffStat: "1 file changed",
hasConflicts: true,
authorArg: ' --author="CustomBot <bot@example.com>"',
authorArg: ' -m "Co-authored-by: CustomBot <bot@example.com>"',
});
expect(prompt).toContain('Be sure to include `--author="CustomBot <bot@example.com>"` in the commit command');
expect(prompt).toContain('Be sure to append `-m "Co-authored-by: CustomBot <bot@example.com>"` to the commit command');
});
it("omits author instruction when authorArg is not provided", async () => {

View File

@@ -10389,9 +10389,11 @@ export function buildExecutionPrompt(
const prompt = scopePromptToWorktree(task.prompt, rootDir, worktreePath);
const reviewLevel = parseReviewLevelFromPrompt(prompt);
// Build author arg for git commits based on settings
// Build co-author trailer arg for git commits based on settings. The user's
// configured git identity remains the primary author; Fusion is appended as
// a `Co-authored-by` trailer for shared credit (recognized by GitHub).
const authorArg = settings?.commitAuthorEnabled !== false
? ` --author="${settings?.commitAuthorName || "Fusion"} <${settings?.commitAuthorEmail || "noreply@runfusion.ai"}>"`
? ` -m "Co-authored-by: ${settings?.commitAuthorName || "Fusion"} <${settings?.commitAuthorEmail || "noreply@runfusion.ai"}>"`
: "";
const sourceIssueRef = buildSourceIssueRef(task.sourceIssue);

View File

@@ -5112,7 +5112,10 @@ async function applyBranchCommitsPreservingHistory(params: {
};
}
/** Build the --author flag for git commits based on project settings. */
/** Build the `-m "Co-authored-by: ..."` trailer arg for git commits based on
* project settings. The user's configured git identity remains the primary
* author/committer; Fusion (or whatever name/email is configured) is appended
* as a co-author trailer that GitHub recognizes for shared attribution. */
function getCommitAuthorArg(settings: {
commitAuthorEnabled?: boolean;
commitAuthorName?: string;
@@ -5121,7 +5124,7 @@ function getCommitAuthorArg(settings: {
if (settings.commitAuthorEnabled === false) return "";
const name = settings.commitAuthorName || "Fusion";
const email = settings.commitAuthorEmail || "noreply@runfusion.ai";
return ` --author="${name} <${email}>"`;
return ` -m "Co-authored-by: ${name} <${email}>"`;
}
export function buildSourceIssueRef(sourceIssue?: TaskSourceIssue | null): string {
@@ -5151,7 +5154,7 @@ Message format:
- **Summary:** one line describing what the squash brings in (imperative mood)
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
- **GitHub reference:** when the prompt includes a source issue reference, add \`Ref: owner/repo#N\` to the commit body
${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
${authorArg ? `- **Co-author:** Always include the \`Co-authored-by\` trailer as shown in the example above so Fusion is credited alongside your git identity.` : ""}
Example:
\`\`\`
@@ -5170,7 +5173,7 @@ Message format:
- **Summary:** one line describing what the squash brings in (imperative mood)
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
- **GitHub reference:** when the prompt includes a source issue reference, add \`Ref: owner/repo#N\` to the commit body
${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
${authorArg ? `- **Co-author:** Always include the \`Co-authored-by\` trailer as shown in the example above so Fusion is credited alongside your git identity.` : ""}
Do NOT include a scope in the commit message type.
Example:
@@ -10764,14 +10767,14 @@ export function buildMergePrompt(params: MergePromptParams): string {
"## ⚠️ There are merge conflicts",
"Run `git diff --name-only --diff-filter=U` to see which files.",
"Resolve each conflict, then `git add` the resolved files.",
`After resolving all conflicts, write and run the commit command.${authorArg ? ` Be sure to include \`${authorArg.trim()}\` in the commit command.` : ""}`,
`After resolving all conflicts, write and run the commit command.${authorArg ? ` Be sure to append \`${authorArg.trim()}\` to the commit command so Fusion is recorded as a co-author.` : ""}`,
);
} else {
parts.push(
"",
"## No conflicts",
"The merge applied cleanly. All changes are staged.",
`Write and run the \`git commit\` command with a good message summarizing the work.${authorArg ? ` Be sure to include \`${authorArg.trim()}\` in the commit command.` : ""}`,
`Write and run the \`git commit\` command with a good message summarizing the work.${authorArg ? ` Be sure to append \`${authorArg.trim()}\` to the commit command so Fusion is recorded as a co-author.` : ""}`,
);
}