feat(FN-1828): merge fusion/fn-1828
This commit is contained in:
5
.changeset/add-commit-author-setting.md
Normal file
5
.changeset/add-commit-author-setting.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gsxdsm/fusion": minor
|
||||
---
|
||||
|
||||
Add configurable commit author attribution. All commits made by Fusion now include `--author` attribution (default: `Fusion <noreply@runfusion.ai>`). Configure or disable via Settings → Merge → Author attribution.
|
||||
47
AGENTS.md
47
AGENTS.md
@@ -1654,6 +1654,53 @@ Alias for `autoResolveConflicts`. When enabled, enables automatic resolution of:
|
||||
|
||||
This setting is preferred for new configurations. If both settings are present, `smartConflictResolution` takes precedence.
|
||||
|
||||
### `commitAuthorEnabled` (default: `true`)
|
||||
|
||||
When enabled, all git commits made by Fusion (both merger squash commits and executor step-boundary commits) include `--author` attribution to identify them as AI-generated.
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"commitAuthorEnabled": true,
|
||||
"commitAuthorName": "Fusion",
|
||||
"commitAuthorEmail": "noreply@runfusion.ai"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `commitAuthorName` (default: `"Fusion"`)
|
||||
|
||||
The name used in the git `--author` flag for Fusion commits. Only used when `commitAuthorEnabled` is true.
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"commitAuthorName": "MyBot"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `commitAuthorEmail` (default: `"noreply@runfusion.ai"`)
|
||||
|
||||
The email used in the git `--author` flag for Fusion commits. Only used when `commitAuthorEnabled` is true.
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"commitAuthorEmail": "bot@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- When disabled (`commitAuthorEnabled: false`), no `--author` flag is added to commits
|
||||
- The committer identity (who physically makes the commit) remains unchanged — only the author metadata is affected
|
||||
- Configure or disable via Settings → Merge → Author attribution in the dashboard
|
||||
|
||||
### `requirePlanApproval` (default: `false`)
|
||||
|
||||
When enabled, AI-generated task specifications require manual approval before the task can move from "triage" to "todo".
|
||||
|
||||
@@ -49,6 +49,9 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
worktreeNaming: "random",
|
||||
taskPrefix: "FN",
|
||||
includeTaskIdInCommit: true,
|
||||
commitAuthorEnabled: true,
|
||||
commitAuthorName: "Fusion",
|
||||
commitAuthorEmail: "noreply@runfusion.ai",
|
||||
planningProvider: undefined,
|
||||
planningModelId: undefined,
|
||||
planningFallbackProvider: undefined,
|
||||
|
||||
@@ -970,6 +970,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. */
|
||||
commitAuthorEnabled?: boolean;
|
||||
/** Name used in the git --author flag for Fusion commits.
|
||||
* Only used when commitAuthorEnabled is true. Default: "Fusion". */
|
||||
commitAuthorName?: string;
|
||||
/** Email used in the git --author flag for Fusion commits.
|
||||
* Only used when commitAuthorEnabled is true. Default: "noreply@runfusion.ai". */
|
||||
commitAuthorEmail?: string;
|
||||
/** AI model provider for planning/triage (specification) agent.
|
||||
* Must be set together with `planningModelId`. When both are undefined,
|
||||
* falls back to `defaultProvider`/`defaultModelId`. */
|
||||
|
||||
@@ -1620,6 +1620,61 @@ export function SettingsModal({
|
||||
</label>
|
||||
<small>When disabled, merge commit messages omit the task ID from the scope (e.g. <code>feat: ...</code> instead of <code>feat(KB-001): ...</code>)</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="commitAuthorEnabled" className="checkbox-label">
|
||||
<input
|
||||
id="commitAuthorEnabled"
|
||||
type="checkbox"
|
||||
checked={form.commitAuthorEnabled !== false}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, commitAuthorEnabled: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Add author attribution to commits
|
||||
</label>
|
||||
<small>
|
||||
When enabled, all commits made by Fusion include <code>--author</code>{" "}
|
||||
attribution identifying them as AI-generated
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{form.commitAuthorEnabled !== false && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="commitAuthorName">Author Name</label>
|
||||
<input
|
||||
id="commitAuthorName"
|
||||
type="text"
|
||||
value={form.commitAuthorName ?? ""}
|
||||
placeholder="Fusion"
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
commitAuthorName: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<small>Name used in commit author attribution</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="commitAuthorEmail">Author Email</label>
|
||||
<input
|
||||
id="commitAuthorEmail"
|
||||
type="email"
|
||||
value={form.commitAuthorEmail ?? ""}
|
||||
placeholder="noreply@runfusion.ai"
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
commitAuthorEmail: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<small>Email used in commit author attribution</small>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="autoResolveConflicts" className="checkbox-label">
|
||||
<input
|
||||
|
||||
@@ -559,6 +559,102 @@ describe("SettingsModal", () => {
|
||||
expect(payload.includeTaskIdInCommit).toBe(false);
|
||||
});
|
||||
|
||||
it("shows Add author attribution to commits checkbox in Merge section", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Merge"));
|
||||
const checkbox = screen.getByLabelText("Add author attribution to commits");
|
||||
expect(checkbox).toBeTruthy();
|
||||
expect(checkbox.getAttribute("type")).toBe("checkbox");
|
||||
});
|
||||
|
||||
it("toggling commitAuthorEnabled checkbox sends false in save payload when unchecked", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Merge"));
|
||||
const checkbox = screen.getByLabelText("Add author attribution to commits");
|
||||
// Default is checked (true), click to uncheck
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
|
||||
|
||||
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(payload.commitAuthorEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("shows author name and email fields when commitAuthorEnabled is true", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Merge"));
|
||||
// Author name and email fields should be visible when author attribution is enabled
|
||||
expect(screen.getByLabelText("Author Name")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Author Email")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides author name and email fields when commitAuthorEnabled is false", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
commitAuthorEnabled: false,
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Merge"));
|
||||
// Author name and email fields should be hidden
|
||||
expect(screen.queryByLabelText("Author Name")).toBeNull();
|
||||
expect(screen.queryByLabelText("Author Email")).toBeNull();
|
||||
});
|
||||
|
||||
it("sends custom author name and email in save payload", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Merge"));
|
||||
|
||||
// Change author name
|
||||
const nameInput = screen.getByLabelText("Author Name");
|
||||
fireEvent.change(nameInput, { target: { value: "CustomBot" } });
|
||||
|
||||
// Change author email
|
||||
const emailInput = screen.getByLabelText("Author Email");
|
||||
fireEvent.change(emailInput, { target: { value: "bot@example.com" } });
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
|
||||
|
||||
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(payload.commitAuthorName).toBe("CustomBot");
|
||||
expect(payload.commitAuthorEmail).toBe("bot@example.com");
|
||||
});
|
||||
|
||||
it("clears author name to undefined when input is emptied", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
commitAuthorName: "SomeBot",
|
||||
commitAuthorEmail: "some@example.com",
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByText("Merge"));
|
||||
|
||||
// Clear author name
|
||||
const nameInput = screen.getByLabelText("Author Name");
|
||||
fireEvent.change(nameInput, { target: { value: "" } });
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
|
||||
|
||||
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(payload.commitAuthorName).toBeUndefined();
|
||||
});
|
||||
|
||||
it("toggling autoResolveConflicts checkbox sends false in save payload when unchecked", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
@@ -2416,6 +2416,58 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).not.toContain("## Project Memory");
|
||||
});
|
||||
});
|
||||
|
||||
describe("commit author attribution", () => {
|
||||
it("includes default author 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>"');
|
||||
});
|
||||
|
||||
it("includes custom 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>"');
|
||||
});
|
||||
|
||||
it("omits author from commit instruction when commitAuthorEnabled is false", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task, "/project", {
|
||||
commitAuthorEnabled: false,
|
||||
} as any);
|
||||
expect(result).not.toContain("--author");
|
||||
// Should still contain commit instruction without author
|
||||
expect(result).toContain("git commit -m");
|
||||
});
|
||||
|
||||
it("uses default 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>"');
|
||||
});
|
||||
|
||||
it("uses default author when settings is undefined", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task, "/project");
|
||||
expect(result).toContain('--author="Fusion <noreply@runfusion.ai>"');
|
||||
});
|
||||
|
||||
it("uses default author when settings is empty object", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task, "/project", {} as any);
|
||||
expect(result).toContain('--author="Fusion <noreply@runfusion.ai>"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Import the summarizeToolArgs helper directly (not affected by mocks above)
|
||||
|
||||
@@ -4031,6 +4031,11 @@ export function buildExecutionPrompt(task: TaskDetail, rootDir?: string, setting
|
||||
const reviewMatch = prompt.match(/##\s*Review Level[:\s]*(\d)/);
|
||||
const reviewLevel = reviewMatch ? parseInt(reviewMatch[1], 10) : 0;
|
||||
|
||||
// Build author arg for git commits based on settings
|
||||
const authorArg = settings?.commitAuthorEnabled !== false
|
||||
? ` --author="${settings?.commitAuthorName || "Fusion"} <${settings?.commitAuthorEmail || "noreply@runfusion.ai"}>"`
|
||||
: "";
|
||||
|
||||
// Build step progress for resume
|
||||
const hasProgress = task.steps.length > 0 && task.steps.some((s) => s.status !== "pending");
|
||||
let progressSection = "";
|
||||
@@ -4149,7 +4154,7 @@ ${hasProgress
|
||||
Use \`task_update\` to report progress on every step transition.
|
||||
Use \`task_log\` for important actions and decisions.
|
||||
Use \`task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
|
||||
Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N — description"\`
|
||||
Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N — description"${authorArg}\`
|
||||
When all steps are complete: call \`task_done()\`
|
||||
|
||||
If a build command is configured, run that exact command in this worktree before calling \`task_done()\`.
|
||||
|
||||
@@ -3329,6 +3329,66 @@ describe("buildMergePrompt — truncation behavior", () => {
|
||||
// Diff stat should be unchanged
|
||||
expect(prompt).toContain(shortDiffStat);
|
||||
});
|
||||
|
||||
it("includes author arg in no-conflicts commit instruction", async () => {
|
||||
const { buildMergePrompt } = await import("./merger.js");
|
||||
|
||||
const prompt = buildMergePrompt({
|
||||
taskId: "FN-001",
|
||||
branch: "fusion/fn-001",
|
||||
commitLog: "- feat: something",
|
||||
diffStat: "1 file changed",
|
||||
hasConflicts: false,
|
||||
authorArg: ' --author="Fusion <noreply@runfusion.ai>"',
|
||||
});
|
||||
|
||||
expect(prompt).toContain('Be sure to include `--author="Fusion <noreply@runfusion.ai>"` in the commit command');
|
||||
});
|
||||
|
||||
it("includes author arg in conflicts commit instruction", async () => {
|
||||
const { buildMergePrompt } = await import("./merger.js");
|
||||
|
||||
const prompt = buildMergePrompt({
|
||||
taskId: "FN-001",
|
||||
branch: "fusion/fn-001",
|
||||
commitLog: "- feat: something",
|
||||
diffStat: "1 file changed",
|
||||
hasConflicts: true,
|
||||
authorArg: ' --author="CustomBot <bot@example.com>"',
|
||||
});
|
||||
|
||||
expect(prompt).toContain('Be sure to include `--author="CustomBot <bot@example.com>"` in the commit command');
|
||||
});
|
||||
|
||||
it("omits author instruction when authorArg is not provided", async () => {
|
||||
const { buildMergePrompt } = await import("./merger.js");
|
||||
|
||||
const prompt = buildMergePrompt({
|
||||
taskId: "FN-001",
|
||||
branch: "fusion/fn-001",
|
||||
commitLog: "- feat: something",
|
||||
diffStat: "1 file changed",
|
||||
hasConflicts: false,
|
||||
});
|
||||
|
||||
expect(prompt).not.toContain("Be sure to include");
|
||||
expect(prompt).toContain("Write and run the `git commit` command with a good message summarizing the work");
|
||||
});
|
||||
|
||||
it("handles empty authorArg gracefully", async () => {
|
||||
const { buildMergePrompt } = await import("./merger.js");
|
||||
|
||||
const prompt = buildMergePrompt({
|
||||
taskId: "FN-001",
|
||||
branch: "fusion/fn-001",
|
||||
commitLog: "- feat: something",
|
||||
diffStat: "1 file changed",
|
||||
hasConflicts: false,
|
||||
authorArg: "",
|
||||
});
|
||||
|
||||
expect(prompt).not.toContain("Be sure to include");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Context Limit Recovery Tests ─────────────────────────────────────
|
||||
|
||||
@@ -972,15 +972,27 @@ export async function resolveConflicts(
|
||||
return remainingComplex;
|
||||
}
|
||||
|
||||
/** Build the --author flag for git commits based on project settings. */
|
||||
function getCommitAuthorArg(settings: {
|
||||
commitAuthorEnabled?: boolean;
|
||||
commitAuthorName?: string;
|
||||
commitAuthorEmail?: string;
|
||||
}): string {
|
||||
if (settings.commitAuthorEnabled === false) return "";
|
||||
const name = settings.commitAuthorName || "Fusion";
|
||||
const email = settings.commitAuthorEmail || "noreply@runfusion.ai";
|
||||
return ` --author="${name} <${email}>"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the merge system prompt. When `includeTaskId` is true (default),
|
||||
* the commit format uses `<type>(<scope>): <summary>` where scope is the
|
||||
* task ID. When false, it uses `<type>: <summary>` with no scope.
|
||||
*/
|
||||
function buildMergeSystemPrompt(includeTaskId: boolean, agentPrompts?: AgentPromptsConfig): string {
|
||||
function buildMergeSystemPrompt(includeTaskId: boolean, agentPrompts?: AgentPromptsConfig, authorArg?: string): string {
|
||||
const commitFormat = includeTaskId
|
||||
? `\`\`\`
|
||||
git commit -m "<type>(<scope>): <summary>" -m "<body>"
|
||||
git commit -m "<type>(<scope>): <summary>" -m "<body>"${authorArg || ""}
|
||||
\`\`\`
|
||||
|
||||
Message format:
|
||||
@@ -988,6 +1000,7 @@ Message format:
|
||||
- **Scope:** the task ID (e.g., KB-001)
|
||||
- **Summary:** one line describing what the squash brings in (imperative mood)
|
||||
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
|
||||
${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
@@ -995,17 +1008,17 @@ git commit -m "feat(KB-003): add user profile page" -m "- Add /profile route wit
|
||||
- Create ProfileCard and EditProfileForm components
|
||||
- Add profile image resizing via sharp
|
||||
- Update nav bar with profile link
|
||||
- Add profile e2e tests"
|
||||
- Add profile e2e tests"${authorArg || ""}
|
||||
\`\`\``
|
||||
: `\`\`\`
|
||||
git commit -m "<type>: <summary>" -m "<body>"
|
||||
git commit -m "<type>: <summary>" -m "<body>"${authorArg || ""}
|
||||
\`\`\`
|
||||
|
||||
Message format:
|
||||
- **Type:** feat, fix, refactor, docs, test, chore
|
||||
- **Summary:** one line describing what the squash brings in (imperative mood)
|
||||
- **Body:** 2-5 bullet points summarizing the key changes, each starting with "- "
|
||||
|
||||
${authorArg ? `- **Author:** Always include the --author flag as shown in the example above.` : ""}
|
||||
Do NOT include a scope in the commit message type.
|
||||
|
||||
Example:
|
||||
@@ -1014,7 +1027,7 @@ git commit -m "feat: add user profile page" -m "- Add /profile route with avatar
|
||||
- Create ProfileCard and EditProfileForm components
|
||||
- Add profile image resizing via sharp
|
||||
- Update nav bar with profile link
|
||||
- Add profile e2e tests"
|
||||
- Add profile e2e tests"${authorArg || ""}
|
||||
\`\`\``;
|
||||
|
||||
// Resolve the base merger prompt from agent prompts config, falling back to the inline default
|
||||
@@ -1347,6 +1360,7 @@ export async function aiMergeTask(
|
||||
attemptNum,
|
||||
options,
|
||||
result,
|
||||
settings,
|
||||
testCommand: effectiveTestCommand,
|
||||
buildCommand: effectiveBuildCommand,
|
||||
testSource: effectiveTestSource,
|
||||
@@ -1592,6 +1606,11 @@ interface MergeAttemptParams {
|
||||
attemptNum: 1 | 2 | 3;
|
||||
options: MergerOptions;
|
||||
result: MergeResult;
|
||||
settings: {
|
||||
commitAuthorEnabled?: boolean;
|
||||
commitAuthorName?: string;
|
||||
commitAuthorEmail?: string;
|
||||
};
|
||||
testCommand?: string;
|
||||
buildCommand?: string;
|
||||
/** Source of the test command: 'explicit' from settings or 'inferred' from project files */
|
||||
@@ -1626,6 +1645,7 @@ async function executeMergeAttempt(
|
||||
attemptNum,
|
||||
options,
|
||||
result,
|
||||
settings,
|
||||
testCommand,
|
||||
buildCommand,
|
||||
testSource,
|
||||
@@ -1706,8 +1726,9 @@ async function executeMergeAttempt(
|
||||
if (staged !== "0") {
|
||||
const escapedLog = commitLog.replace(/"/g, '\\"');
|
||||
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
||||
const authorArg = getCommitAuthorArg(settings);
|
||||
await execAsync(
|
||||
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"`,
|
||||
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${authorArg}`,
|
||||
{ cwd: rootDir },
|
||||
);
|
||||
mergerLog.log(`${taskId}: committed after auto-resolving all conflicts`);
|
||||
@@ -1851,7 +1872,7 @@ async function executeMergeAttempt(
|
||||
* Attempt 3: Use git merge -X theirs --squash strategy
|
||||
*/
|
||||
async function attemptWithTheirsStrategy(params: MergeAttemptParams): Promise<boolean> {
|
||||
const { rootDir, branch, commitLog, includeTaskId, taskId, store, testCommand, buildCommand, testSource, buildSource } = params;
|
||||
const { rootDir, branch, commitLog, includeTaskId, taskId, store, settings, testCommand, buildCommand, testSource, buildSource } = params;
|
||||
|
||||
mergerLog.log(`${taskId}: attempting merge with -X theirs strategy`);
|
||||
|
||||
@@ -1890,8 +1911,9 @@ async function attemptWithTheirsStrategy(params: MergeAttemptParams): Promise<bo
|
||||
// Commit with fallback message
|
||||
const escapedLog = commitLog.replace(/"/g, '\\"');
|
||||
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
||||
const authorArg = getCommitAuthorArg(settings);
|
||||
await execAsync(
|
||||
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"`,
|
||||
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"${authorArg}`,
|
||||
{ cwd: rootDir },
|
||||
);
|
||||
mergerLog.log(`${taskId}: committed with -X theirs auto-resolution`);
|
||||
@@ -2014,8 +2036,9 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
// Graceful fallback
|
||||
}
|
||||
}
|
||||
const authorArg = getCommitAuthorArg(settings);
|
||||
const mergerSystemPrompt = buildSystemPromptWithInstructions(
|
||||
buildMergeSystemPrompt(includeTaskId, settings.agentPrompts),
|
||||
buildMergeSystemPrompt(includeTaskId, settings.agentPrompts, authorArg),
|
||||
mergerInstructions,
|
||||
);
|
||||
|
||||
@@ -2064,6 +2087,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
simplifiedContext,
|
||||
testCommand,
|
||||
buildCommand,
|
||||
authorArg,
|
||||
});
|
||||
|
||||
// Attempt prompting with fresh session (first attempt).
|
||||
@@ -2123,6 +2147,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
simplifiedContext: true, // Also skip detailed context
|
||||
testCommand,
|
||||
buildCommand,
|
||||
authorArg,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -2173,8 +2198,9 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
||||
mergerLog.log("Agent didn't commit — committing with fallback message");
|
||||
const escapedLog = commitLog.replace(/"/g, '\\"');
|
||||
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
||||
const authorArg = getCommitAuthorArg(settings);
|
||||
await execAsync(
|
||||
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"`,
|
||||
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"${authorArg}`,
|
||||
{ cwd: rootDir },
|
||||
);
|
||||
} else {
|
||||
@@ -2208,10 +2234,11 @@ interface MergePromptParams {
|
||||
simplifiedContext?: boolean;
|
||||
testCommand?: string;
|
||||
buildCommand?: string;
|
||||
authorArg?: string;
|
||||
}
|
||||
|
||||
export function buildMergePrompt(params: MergePromptParams): string {
|
||||
const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, testCommand, buildCommand } = params;
|
||||
const { taskId, branch, commitLog, diffStat, hasConflicts, simplifiedContext, testCommand, buildCommand, authorArg } = params;
|
||||
|
||||
// Apply truncation to prevent context overflow for large branches/diffs
|
||||
const truncatedCommitLog = truncateWithEllipsis(commitLog, MERGE_COMMIT_LOG_MAX_CHARS);
|
||||
@@ -2242,14 +2269,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.",
|
||||
`After resolving all conflicts, write and run the commit command.${authorArg ? ` Be sure to include \`${authorArg.trim()}\` in the commit command.` : ""}`,
|
||||
);
|
||||
} 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.",
|
||||
`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.` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user