Merge claudedo/89f989e04fcf43f5a80211d63df7a6d1
This commit is contained in:
@@ -18,5 +18,7 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
|
|||||||
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
||||||
builder.Property(c => c.SessionSkills).HasColumnName("session_skills");
|
builder.Property(c => c.SessionSkills).HasColumnName("session_skills");
|
||||||
builder.Property(c => c.VerifyCommand).HasColumnName("verify_command");
|
builder.Property(c => c.VerifyCommand).HasColumnName("verify_command");
|
||||||
|
builder.Property(c => c.SerializeOnFileOverlap).HasColumnName("serialize_on_file_overlap")
|
||||||
|
.IsRequired().HasDefaultValue(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|||||||
builder.Property(t => t.Notes).HasColumnName("notes");
|
builder.Property(t => t.Notes).HasColumnName("notes");
|
||||||
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||||
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
||||||
|
builder.Property(t => t.ScopeGlobs).HasColumnName("scope_globs");
|
||||||
builder.Property(t => t.HandlerBaseCommit).HasColumnName("handler_base_commit");
|
builder.Property(t => t.HandlerBaseCommit).HasColumnName("handler_base_commit");
|
||||||
builder.Property(t => t.HandlerHeadCommit).HasColumnName("handler_head_commit");
|
builder.Property(t => t.HandlerHeadCommit).HasColumnName("handler_head_commit");
|
||||||
builder.Property(t => t.InteractiveSessionId).HasColumnName("interactive_session_id");
|
builder.Property(t => t.InteractiveSessionId).HasColumnName("interactive_session_id");
|
||||||
|
|||||||
+899
@@ -0,0 +1,899 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ClaudeDoDbContext))]
|
||||||
|
[Migration("20260810113643_AddScopeOverlapFields")]
|
||||||
|
partial class AddScopeOverlapFields
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("CentralWorktreeRoot")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("central_worktree_root");
|
||||||
|
|
||||||
|
b.Property<int>("DailyPrepMaxTasks")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(5)
|
||||||
|
.HasColumnName("daily_prep_max_tasks");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultClaudeInstructions")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("default_claude_instructions");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultMaxTurns")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(40)
|
||||||
|
.HasColumnName("default_max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultModel")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sonnet")
|
||||||
|
.HasColumnName("default_model");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultPermissionMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("bypassPermissions")
|
||||||
|
.HasColumnName("default_permission_mode");
|
||||||
|
|
||||||
|
b.Property<int>("MaxParallelExecutions")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(1)
|
||||||
|
.HasColumnName("max_parallel_executions");
|
||||||
|
|
||||||
|
b.Property<int>("MaxTurnsCeiling")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("max_turns_ceiling");
|
||||||
|
|
||||||
|
b.Property<string>("ModelPresets")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model_presets");
|
||||||
|
|
||||||
|
b.Property<string>("RepoImportFolders")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("repo_import_folders");
|
||||||
|
|
||||||
|
b.Property<string>("ReportExcludedPaths")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("report_excluded_paths");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("StandupWeekday")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(3)
|
||||||
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(80)
|
||||||
|
.HasColumnName("usage_gate_five_hour_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageGateSevenDayPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasColumnName("usage_gate_seven_day_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageThrottleFiveHourHardPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(65)
|
||||||
|
.HasColumnName("usage_throttle_five_hour_hard_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageThrottleFiveHourSoftPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(50)
|
||||||
|
.HasColumnName("usage_throttle_five_hour_soft_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageThrottleSevenDayHardPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(65)
|
||||||
|
.HasColumnName("usage_throttle_seven_day_hard_pct");
|
||||||
|
|
||||||
|
b.Property<int>("UsageThrottleSevenDaySoftPct")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(50)
|
||||||
|
.HasColumnName("usage_throttle_seven_day_soft_pct");
|
||||||
|
|
||||||
|
b.Property<int>("WorktreeAutoCleanupDays")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(7)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_days");
|
||||||
|
|
||||||
|
b.Property<bool>("WorktreeAutoCleanupEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("worktree_auto_cleanup_enabled");
|
||||||
|
|
||||||
|
b.Property<string>("WorktreeStrategy")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("sibling")
|
||||||
|
.HasColumnName("worktree_strategy");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("app_settings", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
DailyPrepMaxTasks = 5,
|
||||||
|
DefaultClaudeInstructions = "",
|
||||||
|
DefaultMaxTurns = 40,
|
||||||
|
DefaultModel = "sonnet",
|
||||||
|
DefaultPermissionMode = "auto",
|
||||||
|
MaxParallelExecutions = 1,
|
||||||
|
MaxTurnsCeiling = 80,
|
||||||
|
StandupWeekday = 3,
|
||||||
|
UsageGateFiveHourPct = 80,
|
||||||
|
UsageGateSevenDayPct = 90,
|
||||||
|
UsageThrottleFiveHourHardPct = 65,
|
||||||
|
UsageThrottleFiveHourSoftPct = 50,
|
||||||
|
UsageThrottleSevenDayHardPct = 65,
|
||||||
|
UsageThrottleSevenDaySoftPct = 50,
|
||||||
|
WorktreeAutoCleanupDays = 7,
|
||||||
|
WorktreeAutoCleanupEnabled = false,
|
||||||
|
WorktreeStrategy = "sibling"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("note_date");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.ToTable("daily_notes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<bool>("SerializeOnFileOverlap")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("serialize_on_file_overlap");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("VerifyCommand")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("verify_command");
|
||||||
|
|
||||||
|
b.HasKey("ListId");
|
||||||
|
|
||||||
|
b.ToTable("list_config", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultCommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("default_commit_type");
|
||||||
|
|
||||||
|
b.Property<bool>("FindingsTracked")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("findings_tracked");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<string>("WorkingDir")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("working_dir");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("idx_lists_sort");
|
||||||
|
|
||||||
|
b.ToTable("lists", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("Days")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(31)
|
||||||
|
.HasColumnName("days_of_week");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasColumnName("enabled");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastRunAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("last_run_at");
|
||||||
|
|
||||||
|
b.Property<string>("PromptOverride")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt_override");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("TimeOfDay")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("time_of_day");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("prime_schedules", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("AddedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("added_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("PinnedRef")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("pinned_ref");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("source_url");
|
||||||
|
|
||||||
|
b.Property<string>("Subpath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("subpath");
|
||||||
|
|
||||||
|
b.HasKey("Name");
|
||||||
|
|
||||||
|
b.ToTable("session_skills", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<bool>("Completed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("completed");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("order_num");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_subtasks_task_id");
|
||||||
|
|
||||||
|
b.ToTable("subtasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<long>("ByteSize")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("byte_size");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_attachments_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AgentPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("agent_path");
|
||||||
|
|
||||||
|
b.Property<string>("BlockedByTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("blocked_by_task_id");
|
||||||
|
|
||||||
|
b.Property<string>("CommitType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("chore")
|
||||||
|
.HasColumnName("commit_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_by");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerBaseCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("HandlerHeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("handler_head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("InteractiveSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("interactive_session_id");
|
||||||
|
|
||||||
|
b.Property<bool>("IsManual")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_manual");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMyDay")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_my_day");
|
||||||
|
|
||||||
|
b.Property<bool>("IsStarred")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_starred");
|
||||||
|
|
||||||
|
b.Property<string>("ListId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("list_id");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxTurns")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("max_turns");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("notes");
|
||||||
|
|
||||||
|
b.Property<string>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("parent_task_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlanningFinalizedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_finalized_at");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningPhase")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("none")
|
||||||
|
.HasColumnName("planning_phase");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_id");
|
||||||
|
|
||||||
|
b.Property<string>("PlanningSessionToken")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("planning_session_token");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result");
|
||||||
|
|
||||||
|
b.Property<string>("ReviewFeedback")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("review_feedback");
|
||||||
|
|
||||||
|
b.Property<int>("RoadblockCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("roadblock_count");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledFor")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("ScopeGlobs")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scope_globs");
|
||||||
|
|
||||||
|
b.Property<string>("SessionSkills")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_skills");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0)
|
||||||
|
.HasColumnName("sort_order");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("SystemPrompt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("system_prompt");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BlockedByTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_blocked_by");
|
||||||
|
|
||||||
|
b.HasIndex("ListId")
|
||||||
|
.HasDatabaseName("idx_tasks_list_id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentTaskId")
|
||||||
|
.HasDatabaseName("idx_tasks_parent_task_id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("idx_tasks_status");
|
||||||
|
|
||||||
|
b.HasIndex("ListId", "SortOrder")
|
||||||
|
.HasDatabaseName("idx_tasks_list_sort");
|
||||||
|
|
||||||
|
b.ToTable("tasks", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<int?>("CacheReadTokens")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("cache_read_tokens");
|
||||||
|
|
||||||
|
b.Property<int?>("CacheWriteTokens")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("cache_write_tokens");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("error_markdown");
|
||||||
|
|
||||||
|
b.Property<int?>("ExitCode")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("exit_code");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("finished_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsRetry")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("is_retry");
|
||||||
|
|
||||||
|
b.Property<string>("LogPath")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("log_path");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("Prompt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("prompt");
|
||||||
|
|
||||||
|
b.Property<string>("ResultMarkdown")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("result_markdown");
|
||||||
|
|
||||||
|
b.Property<int>("RunNumber")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("run_number");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("session_id");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("StartedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<string>("StructuredOutputJson")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("structured_output");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensIn")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_in");
|
||||||
|
|
||||||
|
b.Property<int?>("TokensOut")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("tokens_out");
|
||||||
|
|
||||||
|
b.Property<int?>("TurnCount")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("turn_count");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId")
|
||||||
|
.HasDatabaseName("idx_task_runs_task_id");
|
||||||
|
|
||||||
|
b.ToTable("task_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("EndDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
|
b.Property<DateTime>("GeneratedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("generated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Markdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("markdown");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("StartDate")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StartDate", "EndDate")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("week_reports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<string>("BaseCommit")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("base_commit");
|
||||||
|
|
||||||
|
b.Property<string>("BranchName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("branch_name");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<string>("DiffStat")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("diff_stat");
|
||||||
|
|
||||||
|
b.Property<string>("HeadCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("head_commit");
|
||||||
|
|
||||||
|
b.Property<string>("MergeCommit")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("merge_commit");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("active")
|
||||||
|
.HasColumnName("state");
|
||||||
|
|
||||||
|
b.HasKey("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("worktrees", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithOne("Config")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Subtasks")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BlockedByTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
|
||||||
|
.WithMany("Tasks")
|
||||||
|
.HasForeignKey("ListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
|
||||||
|
.WithMany("Children")
|
||||||
|
.HasForeignKey("ParentTaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("List");
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithMany("Runs")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
|
||||||
|
.WithOne("Worktree")
|
||||||
|
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Config");
|
||||||
|
|
||||||
|
b.Navigation("Tasks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Children");
|
||||||
|
|
||||||
|
b.Navigation("Runs");
|
||||||
|
|
||||||
|
b.Navigation("Subtasks");
|
||||||
|
|
||||||
|
b.Navigation("Worktree");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddScopeOverlapFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "scope_globs",
|
||||||
|
table: "tasks",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "serialize_on_file_overlap",
|
||||||
|
table: "list_config",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "scope_globs",
|
||||||
|
table: "tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "serialize_on_file_overlap",
|
||||||
|
table: "list_config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -225,6 +225,12 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("model");
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<bool>("SerializeOnFileOverlap")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasColumnName("serialize_on_file_overlap");
|
||||||
|
|
||||||
b.Property<string>("SessionSkills")
|
b.Property<string>("SessionSkills")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("session_skills");
|
.HasColumnName("session_skills");
|
||||||
@@ -567,6 +573,10 @@ namespace ClaudeDo.Data.Migrations
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("scheduled_for");
|
.HasColumnName("scheduled_for");
|
||||||
|
|
||||||
|
b.Property<string>("ScopeGlobs")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("scope_globs");
|
||||||
|
|
||||||
b.Property<string>("SessionSkills")
|
b.Property<string>("SessionSkills")
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasColumnName("session_skills");
|
.HasColumnName("session_skills");
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ public sealed class ListConfigEntity
|
|||||||
public string? SessionSkills { get; set; }
|
public string? SessionSkills { get; set; }
|
||||||
public string? VerifyCommand { get; set; }
|
public string? VerifyCommand { get; set; }
|
||||||
|
|
||||||
|
// Off by default: tasks in this list run in parallel exactly as before. When on, the queue
|
||||||
|
// picker holds back a queued task whose declared ScopeGlobs overlaps a running or
|
||||||
|
// awaiting-merge sibling in the same list instead of claiming it.
|
||||||
|
public bool SerializeOnFileOverlap { get; set; }
|
||||||
|
|
||||||
// Navigation property
|
// Navigation property
|
||||||
public ListEntity List { get; set; } = null!;
|
public ListEntity List { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ public sealed class TaskEntity
|
|||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
public string? SessionSkills { get; set; }
|
public string? SessionSkills { get; set; }
|
||||||
|
|
||||||
|
// Newline-separated declared file paths/globs this task expects to touch. User-supplied only
|
||||||
|
// (never inferred). Only consulted by the queue picker when the list's
|
||||||
|
// ListConfigEntity.SerializeOnFileOverlap is on, and only for this task's own overlap check --
|
||||||
|
// an unset value means "no basis to hold this task back", not "touches nothing".
|
||||||
|
public string? ScopeGlobs { get; set; }
|
||||||
|
|
||||||
// Review range for a worktree-less task hosting an interactive "list handler" run
|
// Review range for a worktree-less task hosting an interactive "list handler" run
|
||||||
// (Mission Control's "Let Claude handle it"): the handler commits its own changes
|
// (Mission Control's "Let Claude handle it"): the handler commits its own changes
|
||||||
// straight to the list's working dir, so there is no per-task worktree to diff. These
|
// straight to the list's working dir, so there is no per-task worktree to diff. These
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public sealed class ListRepository
|
|||||||
existing.MaxTurns = config.MaxTurns;
|
existing.MaxTurns = config.MaxTurns;
|
||||||
existing.SessionSkills = config.SessionSkills;
|
existing.SessionSkills = config.SessionSkills;
|
||||||
existing.VerifyCommand = config.VerifyCommand;
|
existing.VerifyCommand = config.VerifyCommand;
|
||||||
|
existing.SerializeOnFileOverlap = config.SerializeOnFileOverlap;
|
||||||
}
|
}
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-5
@@ -9,11 +9,14 @@ namespace ClaudeDo.Worker.External;
|
|||||||
|
|
||||||
public sealed record QueueSlotDto(string Slot, string TaskId, DateTime StartedAt);
|
public sealed record QueueSlotDto(string Slot, string TaskId, DateTime StartedAt);
|
||||||
|
|
||||||
|
public sealed record QueueWaitReasonDto(string TaskId, string Reason, string BlockedByTaskId);
|
||||||
|
|
||||||
public sealed record GetQueueStateResult(
|
public sealed record GetQueueStateResult(
|
||||||
int ConfiguredSlots,
|
int ConfiguredSlots,
|
||||||
int EffectiveSlots,
|
int EffectiveSlots,
|
||||||
IReadOnlyList<QueueSlotDto> ActiveSlots,
|
IReadOnlyList<QueueSlotDto> ActiveSlots,
|
||||||
IReadOnlyList<string> WaitingTaskIds);
|
IReadOnlyList<string> WaitingTaskIds,
|
||||||
|
IReadOnlyList<QueueWaitReasonDto> ScopeBlockedTasks);
|
||||||
|
|
||||||
[McpServerToolType]
|
[McpServerToolType]
|
||||||
public sealed class QueueStateMcpTools
|
public sealed class QueueStateMcpTools
|
||||||
@@ -33,7 +36,11 @@ public sealed class QueueStateMcpTools
|
|||||||
"by the usage throttle (lower when the 5h/7d usage window fills up), so comparing the two " +
|
"by the usage throttle (lower when the 5h/7d usage window fills up), so comparing the two " +
|
||||||
"shows whether throttling is currently active. Each active slot is \"queue\" (a normal " +
|
"shows whether throttling is currently active. Each active slot is \"queue\" (a normal " +
|
||||||
"queue slot) or \"override\" (the single run_task_now/continue_task slot). waitingTaskIds " +
|
"queue slot) or \"override\" (the single run_task_now/continue_task slot). waitingTaskIds " +
|
||||||
"lists queued, unblocked, non-manual, due tasks in the order the queue would pick them next.")]
|
"lists queued, unblocked, non-manual, due tasks in the order the queue would pick them next -- " +
|
||||||
|
"including any held back purely by file-scope overlap, which is why a waiting task can " +
|
||||||
|
"outlast a free slot. scopeBlockedTasks explains those: the list opted into " +
|
||||||
|
"serializeOnFileOverlap and this task's declared scope overlaps blockedByTaskId, a running " +
|
||||||
|
"or awaiting-merge sibling in the same list.")]
|
||||||
public async Task<GetQueueStateResult> GetQueueState(CancellationToken cancellationToken = default)
|
public async Task<GetQueueStateResult> GetQueueState(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var (configured, effective) = await _queue.GetSlotCountsAsync(cancellationToken);
|
var (configured, effective) = await _queue.GetSlotCountsAsync(cancellationToken);
|
||||||
@@ -44,15 +51,32 @@ public sealed class QueueStateMcpTools
|
|||||||
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var waitingTaskIds = await ctx.Tasks
|
var waiting = await ctx.Tasks
|
||||||
.Where(t => t.Status == TaskStatus.Queued
|
.Where(t => t.Status == TaskStatus.Queued
|
||||||
&& t.BlockedByTaskId == null
|
&& t.BlockedByTaskId == null
|
||||||
&& !t.IsManual
|
&& !t.IsManual
|
||||||
&& (t.ScheduledFor == null || t.ScheduledFor <= now))
|
&& (t.ScheduledFor == null || t.ScheduledFor <= now))
|
||||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
.Select(t => t.Id)
|
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return new GetQueueStateResult(configured, effective, activeSlots, waitingTaskIds);
|
var serializingListIds = (await ctx.ListConfigs
|
||||||
|
.Where(c => c.SerializeOnFileOverlap)
|
||||||
|
.Select(c => c.ListId)
|
||||||
|
.ToListAsync(cancellationToken))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var scopeBlocked = new List<QueueWaitReasonDto>();
|
||||||
|
if (serializingListIds.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var t in waiting)
|
||||||
|
{
|
||||||
|
if (!serializingListIds.Contains(t.ListId)) continue;
|
||||||
|
var blockerId = await ScopeOverlap.FindBlockingSiblingAsync(ctx, t, cancellationToken);
|
||||||
|
if (blockerId is not null)
|
||||||
|
scopeBlocked.Add(new QueueWaitReasonDto(t.Id, "scope_overlap", blockerId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GetQueueStateResult(configured, effective, activeSlots, waiting.Select(t => t.Id).ToList(), scopeBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,6 +687,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Preserve SerializeOnFileOverlap: it has no UI/hub affordance yet (set via
|
||||||
|
// set_list_config or directly against ListConfigEntity), so a save from this path
|
||||||
|
// must not silently clear it.
|
||||||
|
var existing = await repo.GetConfigAsync(dto.ListId);
|
||||||
await repo.SetConfigAsync(new ListConfigEntity
|
await repo.SetConfigAsync(new ListConfigEntity
|
||||||
{
|
{
|
||||||
ListId = dto.ListId,
|
ListId = dto.ListId,
|
||||||
@@ -696,6 +700,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
MaxTurns = dto.MaxTurns,
|
MaxTurns = dto.MaxTurns,
|
||||||
SessionSkills = sessionSkills,
|
SessionSkills = sessionSkills,
|
||||||
VerifyCommand = verifyCommand,
|
VerifyCommand = verifyCommand,
|
||||||
|
SerializeOnFileOverlap = existing?.SerializeOnFileOverlap ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Queue;
|
namespace ClaudeDo.Worker.Queue;
|
||||||
|
|
||||||
@@ -13,14 +14,30 @@ public sealed class QueuePicker : IQueuePicker
|
|||||||
|
|
||||||
public async Task<TaskEntity?> ClaimNextAsync(DateTime now, CancellationToken ct)
|
public async Task<TaskEntity?> ClaimNextAsync(DateTime now, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Atomic queue claim: UPDATE + RETURNING in a single statement prevents TOCTOU races.
|
|
||||||
// Raw SQL because EF cannot express UPDATE...RETURNING.
|
|
||||||
// Eligible task must be Queued, unblocked, not manual, and due (or unscheduled).
|
|
||||||
// EF SQLite stores DateTime as "yyyy-MM-dd HH:mm:ss.fffffff" — same format used here for comparison.
|
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
var nowStr = now.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss.fffffff");
|
var nowStr = now.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss.fffffff");
|
||||||
var startedAtStr = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
|
var startedAtStr = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
|
||||||
|
|
||||||
|
var serializingListIds = await ctx.ListConfigs
|
||||||
|
.Where(c => c.SerializeOnFileOverlap)
|
||||||
|
.Select(c => c.ListId)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
// Fast path: no list has opted into file-scope serialization, so behavior and cost stay
|
||||||
|
// identical to before that option existed -- single atomic UPDATE...RETURNING.
|
||||||
|
if (serializingListIds.Count == 0)
|
||||||
|
return await ClaimTopEligibleAsync(ctx, nowStr, startedAtStr, ct);
|
||||||
|
|
||||||
|
return await ClaimRespectingScopeAsync(ctx, now, startedAtStr, serializingListIds.ToHashSet(StringComparer.Ordinal), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<TaskEntity?> ClaimTopEligibleAsync(
|
||||||
|
ClaudeDoDbContext ctx, string nowStr, string startedAtStr, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Atomic queue claim: UPDATE + RETURNING in a single statement prevents TOCTOU races.
|
||||||
|
// Raw SQL because EF cannot express UPDATE...RETURNING.
|
||||||
|
// Eligible task must be Queued, unblocked, not manual, and due (or unscheduled).
|
||||||
|
// EF SQLite stores DateTime as "yyyy-MM-dd HH:mm:ss.fffffff" — same format used here for comparison.
|
||||||
var rows = await ctx.Tasks.FromSqlRaw("""
|
var rows = await ctx.Tasks.FromSqlRaw("""
|
||||||
UPDATE tasks SET status = 'running', started_at = {1}
|
UPDATE tasks SET status = 'running', started_at = {1}
|
||||||
WHERE id = (
|
WHERE id = (
|
||||||
@@ -37,4 +54,41 @@ public sealed class QueuePicker : IQueuePicker
|
|||||||
|
|
||||||
return rows.FirstOrDefault();
|
return rows.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// At least one list wants overlapping-scope tasks serialized: walk eligible candidates in the
|
||||||
|
// usual order and skip any whose declared scope overlaps a running or awaiting-merge sibling
|
||||||
|
// in the same list. A candidate with no declared scope, or belonging to a list not in
|
||||||
|
// serializingListIds, is claimed exactly as before -- there is no attempt to predict a scope
|
||||||
|
// that was never declared and no finished sibling to infer it from.
|
||||||
|
private async Task<TaskEntity?> ClaimRespectingScopeAsync(
|
||||||
|
ClaudeDoDbContext ctx, DateTime now, string startedAtStr, HashSet<string> serializingListIds, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var candidates = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.Status == TaskStatus.Queued
|
||||||
|
&& t.BlockedByTaskId == null
|
||||||
|
&& !t.IsManual
|
||||||
|
&& (t.ScheduledFor == null || t.ScheduledFor <= now))
|
||||||
|
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (serializingListIds.Contains(candidate.ListId)
|
||||||
|
&& await ScopeOverlap.FindBlockingSiblingAsync(ctx, candidate, ct) is not null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var rows = await ctx.Tasks.FromSqlRaw("""
|
||||||
|
UPDATE tasks SET status = 'running', started_at = {1}
|
||||||
|
WHERE id = {0} AND status = 'queued'
|
||||||
|
RETURNING *
|
||||||
|
""", candidate.Id, startedAtStr).ToListAsync(ct);
|
||||||
|
|
||||||
|
var claimed = rows.FirstOrDefault();
|
||||||
|
if (claimed is not null) return claimed;
|
||||||
|
// Lost the race for this row to a concurrent picker -- try the next candidate.
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Queue;
|
||||||
|
|
||||||
|
// Conservative overlap check between two sets of declared/observed file scopes. A scope entry may
|
||||||
|
// be an exact path or a glob (e.g. "src/Foo/*.cs"); comparing the literal prefix before the first
|
||||||
|
// wildcard is enough to catch real collisions without a full glob-matching engine, and it never
|
||||||
|
// under-reports -- worst case it serializes two tasks that would not actually have collided.
|
||||||
|
public static class ScopeOverlap
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<string> ParseScopeGlobs(string? scopeGlobs)
|
||||||
|
=> string.IsNullOrWhiteSpace(scopeGlobs)
|
||||||
|
? []
|
||||||
|
: scopeGlobs.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> ParseDiffStatFiles(string? diffStat)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(diffStat)) return [];
|
||||||
|
var files = new List<string>();
|
||||||
|
foreach (var line in diffStat.Split('\n'))
|
||||||
|
{
|
||||||
|
var idx = line.IndexOf('|');
|
||||||
|
if (idx > 0) files.Add(line[..idx].Trim());
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Overlaps(IEnumerable<string> a, IEnumerable<string> b)
|
||||||
|
{
|
||||||
|
var bPrefixes = b.Select(LiteralPrefix).Where(p => p.Length > 0).ToList();
|
||||||
|
if (bPrefixes.Count == 0) return false;
|
||||||
|
|
||||||
|
foreach (var pa in a.Select(LiteralPrefix))
|
||||||
|
{
|
||||||
|
if (pa.Length == 0) continue;
|
||||||
|
foreach (var pb in bPrefixes)
|
||||||
|
{
|
||||||
|
if (PrefixesOverlap(pa, pb)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool PrefixesOverlap(string a, string b)
|
||||||
|
=> a.Length <= b.Length
|
||||||
|
? b.StartsWith(a, StringComparison.OrdinalIgnoreCase)
|
||||||
|
: a.StartsWith(b, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static string LiteralPrefix(string pattern)
|
||||||
|
{
|
||||||
|
var normalized = pattern.Trim().Replace('\\', '/').TrimStart('/');
|
||||||
|
var idx = normalized.IndexOfAny(['*', '?']);
|
||||||
|
return idx < 0 ? normalized : normalized[..idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the first currently-running or awaiting-merge sibling in the candidate's list whose
|
||||||
|
// scope overlaps the candidate's declared ScopeGlobs. Returns null (no basis to hold it back)
|
||||||
|
// when the candidate declares no scope at all -- this never predicts scope for a task that
|
||||||
|
// hasn't run and has none declared.
|
||||||
|
public static async Task<string?> FindBlockingSiblingAsync(ClaudeDoDbContext ctx, TaskEntity candidate, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var scope = ParseScopeGlobs(candidate.ScopeGlobs);
|
||||||
|
if (scope.Count == 0) return null;
|
||||||
|
|
||||||
|
var siblings = await ctx.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(t => t.Worktree)
|
||||||
|
.Where(t => t.ListId == candidate.ListId
|
||||||
|
&& t.Id != candidate.Id
|
||||||
|
&& (t.Status == TaskStatus.Running
|
||||||
|
|| (t.Status == TaskStatus.WaitingForReview && t.Worktree != null && t.Worktree.State == WorktreeState.Active)))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
foreach (var sibling in siblings)
|
||||||
|
{
|
||||||
|
var siblingScope = sibling.Status == TaskStatus.Running
|
||||||
|
? ParseScopeGlobs(sibling.ScopeGlobs)
|
||||||
|
: ParseDiffStatFiles(sibling.Worktree?.DiffStat);
|
||||||
|
|
||||||
|
if (siblingScope.Count > 0 && Overlaps(scope, siblingScope))
|
||||||
|
return sibling.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -172,4 +172,48 @@ public sealed class QueueStateMcpToolsTests : IDisposable
|
|||||||
|
|
||||||
Assert.Equal(new[] { first.Id, second.Id }, result.WaitingTaskIds);
|
Assert.Equal(new[] { first.Id, second.Id }, result.WaitingTaskIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetQueueState_ScopeBlockedTasks_Empty_WhenNoListSerializes()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var (_, sut) = CreateSut();
|
||||||
|
|
||||||
|
var running = await SeedTaskAsync(listId, TaskStatus.Running);
|
||||||
|
running.ScopeGlobs = "src/Foo.cs";
|
||||||
|
await _ctx.SaveChangesAsync();
|
||||||
|
var queued = await SeedTaskAsync(listId, TaskStatus.Queued);
|
||||||
|
queued.ScopeGlobs = "src/Foo.cs";
|
||||||
|
await _ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await sut.GetQueueState(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Contains(queued.Id, result.WaitingTaskIds);
|
||||||
|
Assert.Empty(result.ScopeBlockedTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetQueueState_ScopeBlockedTasks_ReportsReasonAndBlocker_WhenListSerializes()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
await _listRepo.SetConfigAsync(new ListConfigEntity { ListId = listId, SerializeOnFileOverlap = true });
|
||||||
|
|
||||||
|
var (_, sut) = CreateSut();
|
||||||
|
|
||||||
|
var running = await SeedTaskAsync(listId, TaskStatus.Running);
|
||||||
|
running.ScopeGlobs = "src/Foo.cs";
|
||||||
|
await _ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
var queued = await SeedTaskAsync(listId, TaskStatus.Queued);
|
||||||
|
queued.ScopeGlobs = "src/Foo.cs";
|
||||||
|
await _ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await sut.GetQueueState(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Contains(queued.Id, result.WaitingTaskIds);
|
||||||
|
var reason = Assert.Single(result.ScopeBlockedTasks);
|
||||||
|
Assert.Equal(queued.Id, reason.TaskId);
|
||||||
|
Assert.Equal(running.Id, reason.BlockedByTaskId);
|
||||||
|
Assert.Equal("scope_overlap", reason.Reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ public sealed class QueuePickerTests : IDisposable
|
|||||||
string? blockedBy = null,
|
string? blockedBy = null,
|
||||||
bool taskAgentTag = false,
|
bool taskAgentTag = false,
|
||||||
int? sortOrder = null,
|
int? sortOrder = null,
|
||||||
bool isManual = false)
|
bool isManual = false,
|
||||||
|
string? scopeGlobs = null)
|
||||||
{
|
{
|
||||||
var task = new TaskEntity
|
var task = new TaskEntity
|
||||||
{
|
{
|
||||||
@@ -62,6 +63,7 @@ public sealed class QueuePickerTests : IDisposable
|
|||||||
BlockedByTaskId = blockedBy,
|
BlockedByTaskId = blockedBy,
|
||||||
CommitType = "feat",
|
CommitType = "feat",
|
||||||
IsManual = isManual,
|
IsManual = isManual,
|
||||||
|
ScopeGlobs = scopeGlobs,
|
||||||
};
|
};
|
||||||
await _tasks.AddAsync(task);
|
await _tasks.AddAsync(task);
|
||||||
if (sortOrder is not null)
|
if (sortOrder is not null)
|
||||||
@@ -72,6 +74,25 @@ public sealed class QueuePickerTests : IDisposable
|
|||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SetSerializeOnFileOverlapAsync(string listId, bool value)
|
||||||
|
=> await _lists.SetConfigAsync(new ListConfigEntity { ListId = listId, SerializeOnFileOverlap = value });
|
||||||
|
|
||||||
|
private async Task SeedWorktreeAsync(string taskId, WorktreeState state, string? diffStat)
|
||||||
|
{
|
||||||
|
_ctx.Worktrees.Add(new WorktreeEntity
|
||||||
|
{
|
||||||
|
TaskId = taskId,
|
||||||
|
Path = $"C:\\fake\\{taskId}",
|
||||||
|
BranchName = $"claudedo/{taskId[..8]}",
|
||||||
|
BaseCommit = "base",
|
||||||
|
HeadCommit = "head",
|
||||||
|
DiffStat = diffStat,
|
||||||
|
State = state,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
await _ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ClaimNextAsync_Skips_ManualTasks()
|
public async Task ClaimNextAsync_Skips_ManualTasks()
|
||||||
{
|
{
|
||||||
@@ -194,4 +215,86 @@ public sealed class QueuePickerTests : IDisposable
|
|||||||
var nonNull = results.Where(r => r is not null).ToList();
|
var nonNull = results.Where(r => r is not null).ToList();
|
||||||
Assert.Single(nonNull);
|
Assert.Single(nonNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ClaimNextAsync_SerializeOff_ClaimsOverlappingQueuedTask_EvenWithRunningOverlap()
|
||||||
|
{
|
||||||
|
// Default (option off): behavior is unaffected by ScopeGlobs, even when it overlaps a
|
||||||
|
// running sibling's declared scope.
|
||||||
|
var listId = await CreateListAsync();
|
||||||
|
await SeedAsync(listId, status: TaskStatus.Running, scopeGlobs: "src/Foo.cs");
|
||||||
|
var queued = await SeedAsync(listId, scopeGlobs: "src/Foo.cs");
|
||||||
|
|
||||||
|
var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(picked);
|
||||||
|
Assert.Equal(queued.Id, picked!.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ClaimNextAsync_SerializeOn_SkipsQueuedTask_OverlappingRunningSibling()
|
||||||
|
{
|
||||||
|
var listId = await CreateListAsync();
|
||||||
|
await SetSerializeOnFileOverlapAsync(listId, true);
|
||||||
|
|
||||||
|
await SeedAsync(listId, status: TaskStatus.Running, scopeGlobs: "src/Foo.cs");
|
||||||
|
var overlapping = await SeedAsync(listId, scopeGlobs: "src/Foo.cs", sortOrder: 0, createdAt: DateTime.UtcNow.AddMinutes(-5));
|
||||||
|
var clear = await SeedAsync(listId, scopeGlobs: "src/Bar.cs", sortOrder: 1, createdAt: DateTime.UtcNow);
|
||||||
|
|
||||||
|
var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(picked);
|
||||||
|
Assert.Equal(clear.Id, picked!.Id);
|
||||||
|
|
||||||
|
var stillQueued = await _tasks.GetByIdAsync(overlapping.Id);
|
||||||
|
Assert.Equal(TaskStatus.Queued, stillQueued!.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ClaimNextAsync_SerializeOn_NoDeclaredScope_StillClaimed()
|
||||||
|
{
|
||||||
|
var listId = await CreateListAsync();
|
||||||
|
await SetSerializeOnFileOverlapAsync(listId, true);
|
||||||
|
|
||||||
|
await SeedAsync(listId, status: TaskStatus.Running, scopeGlobs: "src/Foo.cs");
|
||||||
|
var queued = await SeedAsync(listId); // no ScopeGlobs declared -- no basis to hold it back
|
||||||
|
|
||||||
|
var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(picked);
|
||||||
|
Assert.Equal(queued.Id, picked!.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ClaimNextAsync_SerializeOn_SkipsQueuedTask_OverlappingUnmergedFinishedSiblingDiff()
|
||||||
|
{
|
||||||
|
var listId = await CreateListAsync();
|
||||||
|
await SetSerializeOnFileOverlapAsync(listId, true);
|
||||||
|
|
||||||
|
var finished = await SeedAsync(listId, status: TaskStatus.WaitingForReview);
|
||||||
|
await SeedWorktreeAsync(finished.Id, WorktreeState.Active, "src/Foo.cs | 3 ++-");
|
||||||
|
await SeedAsync(listId, scopeGlobs: "src/Foo.cs");
|
||||||
|
|
||||||
|
var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Null(picked);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ClaimNextAsync_SerializeOn_IgnoresMergedSiblingDiff()
|
||||||
|
{
|
||||||
|
// A merged sibling's changes are already on the base branch -- not a live conflict risk,
|
||||||
|
// so it must not hold back an overlapping queued task.
|
||||||
|
var listId = await CreateListAsync();
|
||||||
|
await SetSerializeOnFileOverlapAsync(listId, true);
|
||||||
|
|
||||||
|
var merged = await SeedAsync(listId, status: TaskStatus.Done);
|
||||||
|
await SeedWorktreeAsync(merged.Id, WorktreeState.Merged, "src/Foo.cs | 3 ++-");
|
||||||
|
var queued = await SeedAsync(listId, scopeGlobs: "src/Foo.cs");
|
||||||
|
|
||||||
|
var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(picked);
|
||||||
|
Assert.Equal(queued.Id, picked!.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user