diff --git a/src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs index fcb26df0..a931d59a 100644 --- a/src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs @@ -18,6 +18,7 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration c.MaxTurns).HasColumnName("max_turns"); builder.Property(c => c.SessionSkills).HasColumnName("session_skills"); builder.Property(c => c.VerifyCommand).HasColumnName("verify_command"); + builder.Property(c => c.PermissionMode).HasColumnName("permission_mode"); builder.Property(c => c.SerializeOnFileOverlap).HasColumnName("serialize_on_file_overlap") .IsRequired().HasDefaultValue(false); } diff --git a/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs index 64e79d9f..d3e99e76 100644 --- a/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs @@ -93,6 +93,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt"); builder.Property(t => t.AgentPath).HasColumnName("agent_path"); builder.Property(t => t.MaxTurns).HasColumnName("max_turns"); + builder.Property(t => t.PermissionMode).HasColumnName("permission_mode"); builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false); builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false); builder.Property(t => t.IsManual).HasColumnName("is_manual").HasDefaultValue(false); diff --git a/src/ClaudeDo.Data/Migrations/20260827081703_AddPermissionModeOverrides.Designer.cs b/src/ClaudeDo.Data/Migrations/20260827081703_AddPermissionModeOverrides.Designer.cs new file mode 100644 index 00000000..f0c4257b --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260827081703_AddPermissionModeOverrides.Designer.cs @@ -0,0 +1,976 @@ +// +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("20260827081703_AddPermissionModeOverrides")] + partial class AddPermissionModeOverrides + { + /// + 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("Id") + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AutoContinueOnUsageLimit") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("auto_continue_on_usage_limit"); + + b.Property("CentralWorktreeRoot") + .HasColumnType("TEXT") + .HasColumnName("central_worktree_root"); + + b.Property("DailyPrepMaxTasks") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5) + .HasColumnName("daily_prep_max_tasks"); + + b.Property("DefaultClaudeInstructions") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("") + .HasColumnName("default_claude_instructions"); + + b.Property("DefaultMaxTurns") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(40) + .HasColumnName("default_max_turns"); + + b.Property("DefaultModel") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("sonnet") + .HasColumnName("default_model"); + + b.Property("DefaultPermissionMode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("bypassPermissions") + .HasColumnName("default_permission_mode"); + + b.Property("MaxParallelExecutions") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1) + .HasColumnName("max_parallel_executions"); + + b.Property("MaxTurnsCeiling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(80) + .HasColumnName("max_turns_ceiling"); + + b.Property("ModelPresets") + .HasColumnType("TEXT") + .HasColumnName("model_presets"); + + b.Property("NextTaskNumber") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1) + .HasColumnName("next_task_number"); + + b.Property("RepoImportFolders") + .HasColumnType("TEXT") + .HasColumnName("repo_import_folders"); + + b.Property("ReportExcludedPaths") + .HasColumnType("TEXT") + .HasColumnName("report_excluded_paths"); + + b.Property("SessionSkills") + .HasColumnType("TEXT") + .HasColumnName("session_skills"); + + b.Property("StandupWeekday") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(3) + .HasColumnName("standup_weekday"); + + b.Property("UsageGateFiveHourPct") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(80) + .HasColumnName("usage_gate_five_hour_pct"); + + b.Property("UsageGateSevenDayPct") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(90) + .HasColumnName("usage_gate_seven_day_pct"); + + b.Property("UsageThrottleFiveHourHardPct") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(65) + .HasColumnName("usage_throttle_five_hour_hard_pct"); + + b.Property("UsageThrottleFiveHourSoftPct") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(50) + .HasColumnName("usage_throttle_five_hour_soft_pct"); + + b.Property("UsageThrottleSevenDayHardPct") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(65) + .HasColumnName("usage_throttle_seven_day_hard_pct"); + + b.Property("UsageThrottleSevenDaySoftPct") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(50) + .HasColumnName("usage_throttle_seven_day_soft_pct"); + + b.Property("WorktreeAutoCleanupDays") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(7) + .HasColumnName("worktree_auto_cleanup_days"); + + b.Property("WorktreeAutoCleanupEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("worktree_auto_cleanup_enabled"); + + b.Property("WorktreeStrategy") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("sibling") + .HasColumnName("worktree_strategy"); + + b.HasKey("Id"); + + b.ToTable("app_settings", (string)null); + + b.HasData( + new + { + Id = 1, + AutoContinueOnUsageLimit = false, + DailyPrepMaxTasks = 5, + DefaultClaudeInstructions = "", + DefaultMaxTurns = 40, + DefaultModel = "sonnet", + DefaultPermissionMode = "auto", + MaxParallelExecutions = 1, + MaxTurnsCeiling = 80, + NextTaskNumber = 1, + 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("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("note_date"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasColumnName("sort_order"); + + b.Property("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("ListId") + .HasColumnType("TEXT") + .HasColumnName("list_id"); + + b.Property("AgentPath") + .HasColumnType("TEXT") + .HasColumnName("agent_path"); + + b.Property("MaxTurns") + .HasColumnType("INTEGER") + .HasColumnName("max_turns"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("model"); + + b.Property("PermissionMode") + .HasColumnType("TEXT") + .HasColumnName("permission_mode"); + + b.Property("SerializeOnFileOverlap") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("serialize_on_file_overlap"); + + b.Property("SessionSkills") + .HasColumnType("TEXT") + .HasColumnName("session_skills"); + + b.Property("SystemPrompt") + .HasColumnType("TEXT") + .HasColumnName("system_prompt"); + + b.Property("VerifyCommand") + .HasColumnType("TEXT") + .HasColumnName("verify_command"); + + b.HasKey("ListId"); + + b.ToTable("list_config", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DefaultCommitType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("chore") + .HasColumnName("default_commit_type"); + + b.Property("FindingsTracked") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("findings_tracked"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_manual"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Days") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(31) + .HasColumnName("days_of_week"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("enabled"); + + b.Property("Kind") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("ping") + .HasColumnName("action_kind"); + + b.Property("LastRunAt") + .HasColumnType("TEXT") + .HasColumnName("last_run_at"); + + b.Property("PromptOverride") + .HasColumnType("TEXT") + .HasColumnName("prompt_override"); + + b.Property("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("Name") + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("AddedAt") + .HasColumnType("TEXT") + .HasColumnName("added_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("PinnedRef") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("pinned_ref"); + + b.Property("SourceUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("source_url"); + + b.Property("Subpath") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("subpath"); + + b.HasKey("Name"); + + b.ToTable("session_skills", (string)null); + }); + + modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Completed") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("completed"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("OrderNum") + .HasColumnType("INTEGER") + .HasColumnName("order_num"); + + b.Property("TaskId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ByteSize") + .HasColumnType("INTEGER") + .HasColumnName("byte_size"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("file_name"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AgentPath") + .HasColumnType("TEXT") + .HasColumnName("agent_path"); + + b.Property("BlockedByTaskId") + .HasColumnType("TEXT") + .HasColumnName("blocked_by_task_id"); + + b.Property("CommitType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("chore") + .HasColumnName("commit_type"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedBy") + .HasColumnType("TEXT") + .HasColumnName("created_by"); + + b.Property("DependsOnTaskId") + .HasColumnType("TEXT") + .HasColumnName("depends_on_task_id"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("FailureMaxTurns") + .HasColumnType("INTEGER") + .HasColumnName("failure_max_turns"); + + b.Property("FailureReason") + .HasColumnType("TEXT") + .HasColumnName("failure_reason"); + + b.Property("FailureTurnsUsed") + .HasColumnType("INTEGER") + .HasColumnName("failure_turns_used"); + + b.Property("FinishedAt") + .HasColumnType("TEXT") + .HasColumnName("finished_at"); + + b.Property("HandlerBaseCommit") + .HasColumnType("TEXT") + .HasColumnName("handler_base_commit"); + + b.Property("HandlerHeadCommit") + .HasColumnType("TEXT") + .HasColumnName("handler_head_commit"); + + b.Property("InteractiveSessionId") + .HasColumnType("TEXT") + .HasColumnName("interactive_session_id"); + + b.Property("IsManual") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_manual"); + + b.Property("IsMyDay") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_my_day"); + + b.Property("IsStarred") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_starred"); + + b.Property("ListId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("list_id"); + + b.Property("LogPath") + .HasColumnType("TEXT") + .HasColumnName("log_path"); + + b.Property("MaxTurns") + .HasColumnType("INTEGER") + .HasColumnName("max_turns"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("model"); + + b.Property("Notes") + .HasColumnType("TEXT") + .HasColumnName("notes"); + + b.Property("Number") + .HasColumnType("INTEGER") + .HasColumnName("number"); + + b.Property("ParentTaskId") + .HasColumnType("TEXT") + .HasColumnName("parent_task_id"); + + b.Property("PermissionMode") + .HasColumnType("TEXT") + .HasColumnName("permission_mode"); + + b.Property("PlanningFinalizedAt") + .HasColumnType("TEXT") + .HasColumnName("planning_finalized_at"); + + b.Property("PlanningPhase") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("none") + .HasColumnName("planning_phase"); + + b.Property("PlanningSessionId") + .HasColumnType("TEXT") + .HasColumnName("planning_session_id"); + + b.Property("PlanningSessionToken") + .HasColumnType("TEXT") + .HasColumnName("planning_session_token"); + + b.Property("Result") + .HasColumnType("TEXT") + .HasColumnName("result"); + + b.Property("ReviewFeedback") + .HasColumnType("TEXT") + .HasColumnName("review_feedback"); + + b.Property("RoadblockCount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("roadblock_count"); + + b.Property("ScheduledFor") + .HasColumnType("TEXT") + .HasColumnName("scheduled_for"); + + b.Property("ScopeGlobs") + .HasColumnType("TEXT") + .HasColumnName("scope_globs"); + + b.Property("SessionSkills") + .HasColumnType("TEXT") + .HasColumnName("session_skills"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("sort_order"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("SystemPrompt") + .HasColumnType("TEXT") + .HasColumnName("system_prompt"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.Property("UsageLimitAutoContinuedAt") + .HasColumnType("TEXT") + .HasColumnName("usage_limit_auto_continued_at"); + + b.HasKey("Id"); + + b.HasIndex("BlockedByTaskId") + .HasDatabaseName("idx_tasks_blocked_by"); + + b.HasIndex("DependsOnTaskId") + .HasDatabaseName("idx_tasks_depends_on"); + + b.HasIndex("ListId") + .HasDatabaseName("idx_tasks_list_id"); + + b.HasIndex("Number") + .IsUnique() + .HasDatabaseName("idx_tasks_number"); + + 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("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CacheReadTokens") + .HasColumnType("INTEGER") + .HasColumnName("cache_read_tokens"); + + b.Property("CacheWriteTokens") + .HasColumnType("INTEGER") + .HasColumnName("cache_write_tokens"); + + b.Property("ErrorMarkdown") + .HasColumnType("TEXT") + .HasColumnName("error_markdown"); + + b.Property("Errors") + .HasColumnType("TEXT") + .HasColumnName("errors"); + + b.Property("ExitCode") + .HasColumnType("INTEGER") + .HasColumnName("exit_code"); + + b.Property("FinishedAt") + .HasColumnType("TEXT") + .HasColumnName("finished_at"); + + b.Property("IsRetry") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_retry"); + + b.Property("LogPath") + .HasColumnType("TEXT") + .HasColumnName("log_path"); + + b.Property("Model") + .HasColumnType("TEXT") + .HasColumnName("model"); + + b.Property("Prompt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("prompt"); + + b.Property("ResultMarkdown") + .HasColumnType("TEXT") + .HasColumnName("result_markdown"); + + b.Property("ResultSubtype") + .HasColumnType("TEXT") + .HasColumnName("result_subtype"); + + b.Property("RunNumber") + .HasColumnType("INTEGER") + .HasColumnName("run_number"); + + b.Property("SessionId") + .HasColumnType("TEXT") + .HasColumnName("session_id"); + + b.Property("StartedAt") + .HasColumnType("TEXT") + .HasColumnName("started_at"); + + b.Property("StructuredOutputJson") + .HasColumnType("TEXT") + .HasColumnName("structured_output"); + + b.Property("TaskId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("TerminalReason") + .HasColumnType("TEXT") + .HasColumnName("terminal_reason"); + + b.Property("TokensIn") + .HasColumnType("INTEGER") + .HasColumnName("tokens_in"); + + b.Property("TokensOut") + .HasColumnType("INTEGER") + .HasColumnName("tokens_out"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("EndDate") + .HasColumnType("TEXT") + .HasColumnName("end_date"); + + b.Property("GeneratedAt") + .HasColumnType("TEXT") + .HasColumnName("generated_at"); + + b.Property("Markdown") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("markdown"); + + b.Property("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("TaskId") + .HasColumnType("TEXT") + .HasColumnName("task_id"); + + b.Property("BaseCommit") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("base_commit"); + + b.Property("BranchName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("branch_name"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DiffStat") + .HasColumnType("TEXT") + .HasColumnName("diff_stat"); + + b.Property("HeadCommit") + .HasColumnType("TEXT") + .HasColumnName("head_commit"); + + b.Property("MergeCommit") + .HasColumnType("TEXT") + .HasColumnName("merge_commit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("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.TaskEntity", null) + .WithMany() + .HasForeignKey("DependsOnTaskId") + .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 + } + } +} diff --git a/src/ClaudeDo.Data/Migrations/20260827081703_AddPermissionModeOverrides.cs b/src/ClaudeDo.Data/Migrations/20260827081703_AddPermissionModeOverrides.cs new file mode 100644 index 00000000..4ed1fd2b --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260827081703_AddPermissionModeOverrides.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ClaudeDo.Data.Migrations +{ + /// + public partial class AddPermissionModeOverrides : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "permission_mode", + table: "tasks", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "permission_mode", + table: "list_config", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "permission_mode", + table: "tasks"); + + migrationBuilder.DropColumn( + name: "permission_mode", + table: "list_config"); + } + } +} diff --git a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs index e911bab8..725047f0 100644 --- a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs +++ b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs @@ -239,6 +239,10 @@ namespace ClaudeDo.Data.Migrations .HasColumnType("TEXT") .HasColumnName("model"); + b.Property("PermissionMode") + .HasColumnType("TEXT") + .HasColumnName("permission_mode"); + b.Property("SerializeOnFileOverlap") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") @@ -577,6 +581,10 @@ namespace ClaudeDo.Data.Migrations .HasColumnType("TEXT") .HasColumnName("parent_task_id"); + b.Property("PermissionMode") + .HasColumnType("TEXT") + .HasColumnName("permission_mode"); + b.Property("PlanningFinalizedAt") .HasColumnType("TEXT") .HasColumnName("planning_finalized_at"); diff --git a/src/ClaudeDo.Data/Models/ListConfigEntity.cs b/src/ClaudeDo.Data/Models/ListConfigEntity.cs index 026877d9..11acda3a 100644 --- a/src/ClaudeDo.Data/Models/ListConfigEntity.cs +++ b/src/ClaudeDo.Data/Models/ListConfigEntity.cs @@ -9,6 +9,8 @@ public sealed class ListConfigEntity public int? MaxTurns { get; set; } public string? SessionSkills { get; set; } public string? VerifyCommand { get; set; } + // Null = inherit AppSettingsEntity.DefaultPermissionMode. One of PermissionModeRegistry.Modes. + public string? PermissionMode { 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 diff --git a/src/ClaudeDo.Data/Models/TaskEntity.cs b/src/ClaudeDo.Data/Models/TaskEntity.cs index 80e53f0a..c0abcc64 100644 --- a/src/ClaudeDo.Data/Models/TaskEntity.cs +++ b/src/ClaudeDo.Data/Models/TaskEntity.cs @@ -62,6 +62,9 @@ public sealed class TaskEntity public string? SystemPrompt { get; set; } public string? AgentPath { get; set; } public int? MaxTurns { get; set; } + // Null = inherit the list's config, then AppSettingsEntity.DefaultPermissionMode. + // One of PermissionModeRegistry.Modes. + public string? PermissionMode { get; set; } public bool IsStarred { get; set; } public bool IsMyDay { get; set; } // Manual = a reminder only the user can do. Automation skips it (queue picker, daily prep, diff --git a/src/ClaudeDo.Data/Repositories/TaskRepository.cs b/src/ClaudeDo.Data/Repositories/TaskRepository.cs index 894778fc..2a4dacf8 100644 --- a/src/ClaudeDo.Data/Repositories/TaskRepository.cs +++ b/src/ClaudeDo.Data/Repositories/TaskRepository.cs @@ -266,6 +266,7 @@ public sealed class TaskRepository string? agentPath, int? maxTurns = null, string? sessionSkills = null, + string? permissionMode = null, CancellationToken ct = default) { await _context.Tasks @@ -275,7 +276,8 @@ public sealed class TaskRepository .SetProperty(t => t.SystemPrompt, systemPrompt) .SetProperty(t => t.AgentPath, agentPath) .SetProperty(t => t.MaxTurns, maxTurns) - .SetProperty(t => t.SessionSkills, sessionSkills), ct); + .SetProperty(t => t.SessionSkills, sessionSkills) + .SetProperty(t => t.PermissionMode, permissionMode), ct); } #endregion diff --git a/src/ClaudeDo.Data/Wire.cs b/src/ClaudeDo.Data/Wire.cs index 12013131..0cef71f3 100644 --- a/src/ClaudeDo.Data/Wire.cs +++ b/src/ClaudeDo.Data/Wire.cs @@ -97,11 +97,11 @@ public record UpdateListDto(string Id, string Name, string? WorkingDir, string D // SerializeOnFileOverlap is tri-state on purpose: null = leave the stored flag alone. A caller that // doesn't own the field (anything but the list-settings modal) must not be able to clear it by // omission — SetConfigAsync copies the entity verbatim. -public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null, bool? SerializeOnFileOverlap = null); +public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null, bool? SerializeOnFileOverlap = null, string? PermissionMode = null); -public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null); +public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? PermissionMode = null); -public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null, bool SerializeOnFileOverlap = false); +public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null, bool SerializeOnFileOverlap = false, string? PermissionMode = null); public record SeedResultDto(int Copied, int Skipped); diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 0cfab45c..e507e90c 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -134,6 +134,7 @@ "agentEditor": { "model": "Modell", "maxTurns": "Max. Durchläufe", + "permissionMode": "Permission-Modus", "systemPrompt": "System-Prompt (angehängt)", "promptPrepended": "Wird automatisch vorangestellt:", "agentFile": "Agent-Datei", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 5dbb32dd..32cfc353 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -134,6 +134,7 @@ "agentEditor": { "model": "Model", "maxTurns": "Max turns", + "permissionMode": "Permission mode", "systemPrompt": "System prompt (appended)", "promptPrepended": "Prepended automatically:", "agentFile": "Agent file", diff --git a/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs index 493c386a..5900a11f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs @@ -38,6 +38,8 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa [ObservableProperty] private decimal? _maxTurns; [ObservableProperty] private string _systemPrompt = ""; [ObservableProperty] private AgentInfo? _selectedAgent; + // Null = inherit (list → global). Restricted to PermissionModeRegistry.Modes by the ComboBox. + [ObservableProperty] private string? _permissionMode; [ObservableProperty] private string _modelBadge = ""; [ObservableProperty] private string _modelInheritedHint = ""; @@ -45,6 +47,8 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa [ObservableProperty] private string _turnsInheritedHint = ""; [ObservableProperty] private string _turnsCeilingHint = ""; [ObservableProperty] private string _agentBadge = ""; + [ObservableProperty] private string _permissionBadge = ""; + [ObservableProperty] private string _permissionInheritedHint = ""; [ObservableProperty] private string _effectiveSystemPromptHint = ""; private string _globalModel = ModelRegistry.DefaultAlias; @@ -54,9 +58,11 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa private int _maxTurnsCeiling = 80; private string EffectiveModel => Model ?? _listModel ?? _globalModel; private int GlobalMaxTurns => ModelPresets.For(_presets, EffectiveModel).MaxTurns; - private string? _listModel; // Task scope only - private int? _listMaxTurns; // Task scope only - private string? _listAgentName; // Task scope only + private string _globalPermissionMode = PermissionModeRegistry.DefaultMode; + private string? _listModel; // Task scope only + private int? _listMaxTurns; // Task scope only + private string? _listAgentName; // Task scope only + private string? _listPermissionMode; // Task scope only private bool _suppressSave; private CancellationTokenSource? _saveCts; @@ -65,6 +71,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa MaxTurns is decimal t ? (int)t : (_listMaxTurns ?? GlobalMaxTurns); public ObservableCollection ModelOptions { get; } = new(ModelRegistry.Aliases); + public IReadOnlyList PermissionModeOptions { get; } = PermissionModeRegistry.Modes; public ObservableCollection Agents { get; } = new(); public ObservableCollection SessionSkills { get; } = new(); @@ -121,12 +128,14 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa partial void OnSystemPromptChanged(string value) => QueueSave(); partial void OnSelectedAgentChanged(AgentInfo? value) { RecomputeAgentBadge(); QueueSave(); } + partial void OnPermissionModeChanged(string? value) { RecomputePermissionBadge(); QueueSave(); } private void RecomputeBadges() { RecomputeModelBadge(); RecomputeTurnsBadge(); RecomputeAgentBadge(); + RecomputePermissionBadge(); } private void RecomputeModelBadge() @@ -162,6 +171,16 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa AgentBadge = BadgeFor(source, agentSet); } + private void RecomputePermissionBadge() + { + var own = string.IsNullOrWhiteSpace(PermissionMode) ? null : PermissionMode; + var (value, source) = _scope == AgentConfigScope.Task + ? InheritanceResolver.Resolve(own, _listPermissionMode, _globalPermissionMode) + : InheritanceResolver.ResolveList(own, _globalPermissionMode); + PermissionInheritedHint = value; + PermissionBadge = BadgeFor(source, own is not null); + } + private static string BadgeFor(InheritSource source, bool isSet) => isSet ? Loc.T("settings.inherit.overrideBadge") : source == InheritSource.List @@ -202,11 +221,12 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa var ap = SelectedAgent is null || string.IsNullOrWhiteSpace(SelectedAgent.Path) ? null : SelectedAgent.Path; var turns = MaxTurns is decimal d ? (int?)d : null; var skills = SelectedSessionSkillNames(); + var permission = string.IsNullOrWhiteSpace(PermissionMode) ? null : PermissionMode; if (_scope == AgentConfigScope.Task) - await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills)); + await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills, permission)); else - await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, serializeOnFileOverlap)); + await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, serializeOnFileOverlap, permission)); } private List? SelectedSessionSkillNames() @@ -225,10 +245,10 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa await LoadGlobalDefaultsAsync(); var cfg = await _worker.GetListConfigAsync(listId); - ApplyConfig(cfg?.Model, cfg?.MaxTurns, cfg?.SystemPrompt, cfg?.AgentPath); + ApplyConfig(cfg?.Model, cfg?.MaxTurns, cfg?.SystemPrompt, cfg?.AgentPath, cfg?.PermissionMode); await ReloadSessionSkillsAsync(cfg?.SessionSkills); - _listModel = null; _listMaxTurns = null; _listAgentName = null; + _listModel = null; _listMaxTurns = null; _listAgentName = null; _listPermissionMode = null; EffectiveSystemPromptHint = ""; RecomputeBadges(); OnPropertyChanged(nameof(EffectiveMaxTurns)); @@ -243,13 +263,14 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa { TargetId = entity.Id; await ReloadAgentsAsync("(inherited)"); - ApplyConfig(entity.Model, entity.MaxTurns, entity.SystemPrompt, entity.AgentPath); + ApplyConfig(entity.Model, entity.MaxTurns, entity.SystemPrompt, entity.AgentPath, entity.PermissionMode); await ReloadSessionSkillsAsync(ParseSessionSkills(entity.SessionSkills)); var listCfg = await _worker.GetListConfigAsync(entity.ListId); await LoadGlobalDefaultsAsync(); _listModel = listCfg?.Model; _listMaxTurns = listCfg?.MaxTurns; + _listPermissionMode = listCfg?.PermissionMode; _listAgentName = string.IsNullOrWhiteSpace(listCfg?.AgentPath) ? null : System.IO.Path.GetFileName(listCfg!.AgentPath!); EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) @@ -270,6 +291,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa MaxTurns = null; SystemPrompt = ""; SelectedAgent = null; + PermissionMode = null; foreach (var s in SessionSkills) s.IsSelected = false; } finally { _suppressSave = false; } @@ -309,13 +331,16 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa ? rows.Select(r => new ModelPreset(r.Model, r.Effort, r.MaxTurns)).ToList() : ModelPresets.Defaults; _maxTurnsCeiling = app?.MaxTurnsCeiling ?? 80; + _globalPermissionMode = app?.DefaultPermissionMode ?? PermissionModeRegistry.DefaultMode; } - private void ApplyConfig(string? model, int? maxTurns, string? systemPrompt, string? agentPath) + private void ApplyConfig(string? model, int? maxTurns, string? systemPrompt, string? agentPath, + string? permissionMode) { Model = string.IsNullOrWhiteSpace(model) ? null : model!; MaxTurns = maxTurns is int mt ? mt : (decimal?)null; SystemPrompt = systemPrompt ?? ""; + PermissionMode = string.IsNullOrWhiteSpace(permissionMode) ? null : permissionMode!; SelectedAgent = string.IsNullOrWhiteSpace(agentPath) ? Agents[0] : (Agents.FirstOrDefault(a => a.Path == agentPath) ?? Agents[0]); @@ -324,6 +349,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa [RelayCommand] private void ResetModel() => Model = null; [RelayCommand] private void ResetTurns() => MaxTurns = null; [RelayCommand] private void ResetAgent() => SelectedAgent = Agents.Count > 0 ? Agents[0] : null; + [RelayCommand] private void ResetPermissionMode() => PermissionMode = null; [RelayCommand] private void ResetAll() @@ -332,6 +358,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa MaxTurns = null; SystemPrompt = ""; SelectedAgent = Agents.Count > 0 ? Agents[0] : null; + PermissionMode = null; foreach (var s in SessionSkills) s.IsSelected = false; } } diff --git a/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml b/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml index bed96c5a..807663d1 100644 --- a/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml +++ b/src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml @@ -39,6 +39,20 @@ IsVisible="{Binding TurnsCeilingHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> + + + + + +