feat(data): per-list base branch, preserved by every config writer

This commit is contained in:
mika kuns
2026-10-01 10:39:49 +02:00
parent 8911408bca
commit 3fe1feadf9
10 changed files with 1321 additions and 9 deletions
@@ -21,5 +21,6 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
builder.Property(c => c.PermissionMode).HasColumnName("permission_mode");
builder.Property(c => c.TicketProjectId).HasColumnName("ticket_project_id");
builder.Property(c => c.PowerUseAllowed).HasColumnName("power_use_allowed");
builder.Property(c => c.BaseBranch).HasColumnName("base_branch");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddListBaseBranch : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "base_branch",
table: "list_config",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "base_branch",
table: "list_config");
}
}
}
@@ -268,6 +268,10 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BaseBranch")
.HasColumnType("TEXT")
.HasColumnName("base_branch");
b.Property<int?>("MaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("max_turns");
@@ -23,6 +23,12 @@ public sealed class ListConfigEntity
// false, also opt-in. ⚠️ Gleiche Falle wie TicketProjectId: SetConfigAsync kopiert verbatim.
public bool PowerUseAllowed { get; set; }
/// <summary>
/// Basis neuer Worktrees und Default-Merge-/PR-Ziel (z.B. <c>dev</c>). Null = ausgecheckter Branch
/// (privat) bzw. Default-Branch des Remotes (Firma). ⚠️ SetConfigAsync kopiert verbatim.
/// </summary>
public string? BaseBranch { get; set; }
// Navigation property
public ListEntity List { get; set; } = null!;
}
@@ -175,6 +175,7 @@ public sealed class ListRepository
existing.PermissionMode = config.PermissionMode;
existing.TicketProjectId = config.TicketProjectId;
existing.PowerUseAllowed = config.PowerUseAllowed;
existing.BaseBranch = config.BaseBranch;
}
await _context.SaveChangesAsync(ct);
}
+3 -2
View File
@@ -108,11 +108,12 @@ public record UpdateListDto(string Id, string Name, string? WorkingDir, string D
// verbatim. Its scale: null = keep stored, <= 0 = clear the link, > 0 = set it.
// PowerUseAllowed is nullable for the same reason: null = keep stored, false = revoke the opt-in.
// A non-nullable bool here would let any caller that omits it silently revoke PowerUse for the list.
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool? PowerUseAllowed = null);
// BaseBranch: null = keep stored, "" = clear, value = set.
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool? PowerUseAllowed = null, string? BaseBranch = null);
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? PermissionMode = null);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool PowerUseAllowed = false);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool PowerUseAllowed = false, string? BaseBranch = null);
public record SeedResultDto(int Copied, int Skipped);
+8 -5
View File
@@ -10,7 +10,7 @@ using ModelContextProtocol.Server;
namespace ClaudeDo.Worker.External;
public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns, string? VerifyCommand);
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns, string? VerifyCommand, string? BaseBranch = null);
public sealed record TaskConfigResult(bool Found, TaskConfigDto? Config);
public sealed record ListConfigResult(bool Found, ListConfigDto? Config);
public sealed record SetListConfigResult(bool Ok, string ListId, ListConfigDto? Config);
@@ -55,7 +55,7 @@ public sealed class ConfigMcpTools
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
return cfg is null
? new ListConfigResult(false, null)
: new ListConfigResult(true, new ListConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns, cfg.VerifyCommand));
: new ListConfigResult(true, new ListConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns, cfg.VerifyCommand, cfg.BaseBranch));
}
// clearFields (not a per-field sentinel like UpdateList's "" for workingDir) because maxTurns is an
@@ -81,7 +81,7 @@ public sealed class ConfigMcpTools
"in this list that don't override them (verify command is list-only; tasks cannot override it). Only " +
"the fields you pass are changed; omitted fields keep their current value. To clear a field instead, " +
"name it in clearFields; clearing all five deletes the list's config unless it also carries settings " +
"this tool doesn't expose (session skills, file-scope serialization, permission mode), which are " +
"this tool doesn't expose (session skills, permission mode, ticket link, base branch, PowerUse opt-in), which are " +
"always preserved.")]
public async Task<SetListConfigResult> SetListConfig(
string listId, string? model = null, string? systemPrompt = null, string? agentPath = null,
@@ -110,7 +110,8 @@ public sealed class ConfigMcpTools
// UI-only).
var hasUnrelatedSettings = existing is not null
&& (existing.SessionSkills is not null
|| existing.PermissionMode is not null || existing.TicketProjectId is not null);
|| existing.PermissionMode is not null || existing.TicketProjectId is not null
|| existing.BaseBranch is not null || existing.PowerUseAllowed);
ListConfigDto? config;
var allCleared = m is null && sp is null && ap is null && mt is null && vc is null;
@@ -131,8 +132,10 @@ public sealed class ConfigMcpTools
VerifyCommand = vc, SessionSkills = existing?.SessionSkills,
PermissionMode = existing?.PermissionMode,
TicketProjectId = existing?.TicketProjectId,
BaseBranch = existing?.BaseBranch,
PowerUseAllowed = existing?.PowerUseAllowed ?? false,
}, cancellationToken);
config = allCleared ? null : new ListConfigDto(m, sp, ap, mt, vc);
config = allCleared ? null : new ListConfigDto(m, sp, ap, mt, vc, existing?.BaseBranch);
}
await _broadcaster.ListUpdated(listId);
+6 -2
View File
@@ -600,9 +600,12 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
// Gleiches Tri-State-Muster: null = gespeicherten Wert behalten, false = Opt-in widerrufen.
var powerUseAllowed = dto.PowerUseAllowed ?? existing?.PowerUseAllowed ?? false;
// Tri-State wie oben: null = behalten, "" = löschen.
var baseBranch = dto.BaseBranch is null ? existing?.BaseBranch : dto.BaseBranch.NullIfBlank();
// powerUseAllowed muss mit in die Leer-Prüfung: sonst löscht ein Save, bei dem PowerUse das
// EINZIGE gesetzte Feld ist, die Config-Zeile und damit das Opt-in gleich wieder mit.
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && permissionMode is null && ticketProjectId is null && !powerUseAllowed)
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null && verifyCommand is null && permissionMode is null && ticketProjectId is null && !powerUseAllowed && baseBranch is null)
{
await repo.DeleteConfigAsync(dto.ListId);
}
@@ -620,6 +623,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
PermissionMode = permissionMode,
TicketProjectId = ticketProjectId,
PowerUseAllowed = powerUseAllowed,
BaseBranch = baseBranch,
});
}
@@ -643,7 +647,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
var repo = new ListRepository(ctx);
var config = await repo.GetConfigAsync(listId);
if (config is null) return null;
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.PermissionMode, config.TicketProjectId, config.PowerUseAllowed);
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills), config.VerifyCommand, config.PermissionMode, config.TicketProjectId, config.PowerUseAllowed, config.BaseBranch);
}
public async Task<SetTaskStatusResultDto> SetTaskStatus(string taskId, string status)