diff --git a/src/ClaudeDo.Data/Configuration/DailyNoteEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/DailyNoteEntityConfiguration.cs index c85bcdb3..e5a31ab3 100644 --- a/src/ClaudeDo.Data/Configuration/DailyNoteEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/DailyNoteEntityConfiguration.cs @@ -14,6 +14,7 @@ public class DailyNoteEntityConfiguration : IEntityTypeConfiguration n.Date).HasColumnName("note_date").IsRequired(); builder.Property(n => n.Text).HasColumnName("text").IsRequired(); builder.Property(n => n.SortOrder).HasColumnName("sort_order").IsRequired(); + builder.Property(n => n.FromAgent).HasColumnName("from_agent").IsRequired(); builder.Property(n => n.CreatedAt).HasColumnName("created_at").IsRequired(); builder.HasIndex(n => n.Date); } diff --git a/src/ClaudeDo.Data/Migrations/20260828185839_AddDailyNoteFromAgent.Designer.cs b/src/ClaudeDo.Data/Migrations/20260828185839_AddDailyNoteFromAgent.Designer.cs new file mode 100644 index 00000000..e08a9dc5 --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260828185839_AddDailyNoteFromAgent.Designer.cs @@ -0,0 +1,982 @@ +// +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("20260828185839_AddDailyNoteFromAgent")] + partial class AddDailyNoteFromAgent + { + /// + 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("FromAgent") + .HasColumnType("INTEGER") + .HasColumnName("from_agent"); + + 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("SessionSkills") + .HasColumnType("TEXT") + .HasColumnName("session_skills"); + + b.Property("SystemPrompt") + .HasColumnType("TEXT") + .HasColumnName("system_prompt"); + + b.Property("TicketProjectId") + .HasColumnType("INTEGER") + .HasColumnName("ticket_project_id"); + + 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("Icon") + .HasColumnType("TEXT") + .HasColumnName("icon"); + + 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("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("TicketRef") + .HasColumnType("TEXT") + .HasColumnName("ticket_ref"); + + 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/20260828185839_AddDailyNoteFromAgent.cs b/src/ClaudeDo.Data/Migrations/20260828185839_AddDailyNoteFromAgent.cs new file mode 100644 index 00000000..9aa79117 --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260828185839_AddDailyNoteFromAgent.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ClaudeDo.Data.Migrations +{ + /// + public partial class AddDailyNoteFromAgent : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "from_agent", + table: "daily_notes", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "from_agent", + table: "daily_notes"); + } + } +} diff --git a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs index 4573df48..12c24ebe 100644 --- a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs +++ b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs @@ -205,6 +205,10 @@ namespace ClaudeDo.Data.Migrations .HasColumnType("TEXT") .HasColumnName("note_date"); + b.Property("FromAgent") + .HasColumnType("INTEGER") + .HasColumnName("from_agent"); + b.Property("SortOrder") .HasColumnType("INTEGER") .HasColumnName("sort_order"); diff --git a/src/ClaudeDo.Data/Models/DailyNoteEntity.cs b/src/ClaudeDo.Data/Models/DailyNoteEntity.cs index 7d684bac..d2046d50 100644 --- a/src/ClaudeDo.Data/Models/DailyNoteEntity.cs +++ b/src/ClaudeDo.Data/Models/DailyNoteEntity.cs @@ -6,5 +6,9 @@ public sealed class DailyNoteEntity public DateOnly Date { get; set; } public string Text { get; set; } = string.Empty; public int SortOrder { get; set; } + + /// True when an agent wrote this note through the MCP tool rather than the user. + /// Drives the UI marker and the per-day cap in NotesMcpTools. + public bool FromAgent { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/src/ClaudeDo.Data/Repositories/DailyNoteRepository.cs b/src/ClaudeDo.Data/Repositories/DailyNoteRepository.cs index 2d1d94f6..fcb94496 100644 --- a/src/ClaudeDo.Data/Repositories/DailyNoteRepository.cs +++ b/src/ClaudeDo.Data/Repositories/DailyNoteRepository.cs @@ -22,7 +22,13 @@ public sealed class DailyNoteRepository .OrderBy(n => n.Date).ThenBy(n => n.SortOrder) .ToListAsync(ct); - public async Task AddAsync(DateOnly day, string text, CancellationToken ct = default) + /// Notes an agent wrote on — the number the MCP cap counts against. + public async Task CountAgentNotesAsync(DateOnly day, CancellationToken ct = default) => + await _context.DailyNotes.AsNoTracking() + .CountAsync(n => n.Date == day && n.FromAgent, ct); + + public async Task AddAsync( + DateOnly day, string text, bool fromAgent = false, CancellationToken ct = default) { var nextOrder = await _context.DailyNotes .Where(n => n.Date == day) @@ -34,6 +40,7 @@ public sealed class DailyNoteRepository Date = day, Text = text, SortOrder = nextOrder + 1, + FromAgent = fromAgent, CreatedAt = DateTime.UtcNow, }; _context.DailyNotes.Add(note); diff --git a/src/ClaudeDo.Ui/Services/ReportDtos.cs b/src/ClaudeDo.Ui/Services/ReportDtos.cs index a8531977..395bdf37 100644 --- a/src/ClaudeDo.Ui/Services/ReportDtos.cs +++ b/src/ClaudeDo.Ui/Services/ReportDtos.cs @@ -1,3 +1,3 @@ namespace ClaudeDo.Ui.Services; -public sealed record DailyNoteDto(string Id, string Date, string Text, int SortOrder); +public sealed record DailyNoteDto(string Id, string Date, string Text, int SortOrder, bool FromAgent); diff --git a/src/ClaudeDo.Worker/External/NotesMcpTools.cs b/src/ClaudeDo.Worker/External/NotesMcpTools.cs new file mode 100644 index 00000000..a884ea9b --- /dev/null +++ b/src/ClaudeDo.Worker/External/NotesMcpTools.cs @@ -0,0 +1,59 @@ +using System.ComponentModel; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Hub; +using ModelContextProtocol.Server; + +namespace ClaudeDo.Worker.External; + +public sealed record AddNoteResult(bool Saved, string Id, int AgentNotesToday, int RemainingToday); + +[McpServerToolType] +public sealed class NotesMcpTools +{ + /// Agent-written notes allowed per day. The user's own notes are never counted or + /// capped — this only stops an agent from burying the day in its own bullets. + public const int DailyAgentCap = 10; + + /// A note is a line, not a report. Longer output belongs in a task or a finding. + public const int MaxLength = 500; + + private readonly DailyNoteRepository _notes; + private readonly HubBroadcaster _broadcaster; + + public NotesMcpTools(DailyNoteRepository notes, HubBroadcaster broadcaster) + { + _notes = notes; + _broadcaster = broadcaster; + } + + [McpServerTool, Description( + "Add one short line to the user's notes for today — the day log they read in ClaudeDo, " + + "not a log for you. Write only what the user would want to reread later and would " + + "otherwise lose: a decision they made, something you found that changes their plans, " + + "something they asked you to remember for the day. Task status, your progress and " + + "anything already visible in the task list are noise here. When unsure, don't. " + + "Capped at 10 agent notes per day; you cannot edit or delete notes, so a bad one stays " + + "until the user removes it.")] + public async Task AddNote( + [Description("The note itself: one plain sentence, no markdown, no prefix, max 500 characters.")] string text, + CancellationToken cancellationToken = default) + { + var trimmed = (text ?? "").Trim(); + if (trimmed.Length == 0) + throw new InvalidOperationException("text is required."); + if (trimmed.Length > MaxLength) + throw new InvalidOperationException( + $"Note is {trimmed.Length} characters, the limit is {MaxLength}. Shorten it to one line."); + + var today = DateOnly.FromDateTime(DateTime.Today); + var used = await _notes.CountAgentNotesAsync(today, cancellationToken); + if (used >= DailyAgentCap) + throw new InvalidOperationException( + $"Daily cap reached: {DailyAgentCap} agent notes already written today. " + + "Nothing was saved — tell the user instead of retrying."); + + var note = await _notes.AddAsync(today, trimmed, fromAgent: true, cancellationToken); + await _broadcaster.NotesUpdated(); + return new AddNoteResult(true, note.Id, used + 1, DailyAgentCap - used - 1); + } +} diff --git a/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs b/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs index 34b9fcbd..4c8430bd 100644 --- a/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs +++ b/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs @@ -37,6 +37,10 @@ public sealed class HubBroadcaster : IPrimeBroadcaster, IRefineBroadcaster public Task HandoffRequested(string taskId, IReadOnlyList survivingTaskIds, string nextPhase) => _hub.Clients.All.SendAsync("HandoffRequested", taskId, survivingTaskIds, nextPhase); + /// An agent wrote a note through MCP — the open notes editor reloads. + public Task NotesUpdated() => + _hub.Clients.All.SendAsync("NotesUpdated"); + public Task ListUpdated(string listId) => _hub.Clients.All.SendAsync("ListUpdated", listId); diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index b2fe1685..83e6e6d1 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -970,14 +970,25 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub { using var ctx = _dbFactory.CreateDbContext(); var notes = await new DailyNoteRepository(ctx).ListByDayAsync(Day(dayIso)); - return notes.Select(n => new DailyNoteDto(n.Id, n.Date.ToString("yyyy-MM-dd"), n.Text, n.SortOrder)).ToList(); + return notes.Select(Dto).ToList(); } + /// Both ends inclusive — the editor loads today plus its recent history in one call. + public async Task> GetDailyNotesBetween(string startIso, string endIso) + { + using var ctx = _dbFactory.CreateDbContext(); + var notes = await new DailyNoteRepository(ctx).ListBetweenAsync(Day(startIso), Day(endIso)); + return notes.Select(Dto).ToList(); + } + + private static DailyNoteDto Dto(Data.Models.DailyNoteEntity n) => + new(n.Id, n.Date.ToString("yyyy-MM-dd"), n.Text, n.SortOrder, n.FromAgent); + public async Task AddDailyNote(string dayIso, string text) { using var ctx = _dbFactory.CreateDbContext(); var n = await new DailyNoteRepository(ctx).AddAsync(Day(dayIso), text); - return new DailyNoteDto(n.Id, n.Date.ToString("yyyy-MM-dd"), n.Text, n.SortOrder); + return Dto(n); } public async Task UpdateDailyNote(string id, string text) diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index e9108319..3d28bc59 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -355,7 +355,9 @@ if (cfg.ExternalMcpPort > 0) externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); + externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); + externalBuilder.Services.AddScoped(); externalBuilder.Services.AddMcpServer() .WithHttpTransport(o => o.Stateless = true) .WithRequestFilters(f => f.AddCallToolFilter(ExternalMcpExceptionFilter.Wrap)) @@ -371,7 +373,8 @@ if (cfg.ExternalMcpPort > 0) .WithTools() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); externalBuilder.WebHost.UseUrls($"http://127.0.0.1:{cfg.ExternalMcpPort}"); externalApp = externalBuilder.Build(); diff --git a/src/ClaudeDo.Worker/Report/DailyNoteDto.cs b/src/ClaudeDo.Worker/Report/DailyNoteDto.cs index f2e734b9..8921bbf2 100644 --- a/src/ClaudeDo.Worker/Report/DailyNoteDto.cs +++ b/src/ClaudeDo.Worker/Report/DailyNoteDto.cs @@ -1,3 +1,3 @@ namespace ClaudeDo.Worker.Report; -public sealed record DailyNoteDto(string Id, string Date, string Text, int SortOrder); +public sealed record DailyNoteDto(string Id, string Date, string Text, int SortOrder, bool FromAgent); diff --git a/tests/ClaudeDo.Worker.Tests/External/NotesMcpToolsTests.cs b/tests/ClaudeDo.Worker.Tests/External/NotesMcpToolsTests.cs new file mode 100644 index 00000000..1321cc15 --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/External/NotesMcpToolsTests.cs @@ -0,0 +1,77 @@ +using ClaudeDo.Data; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.External; +using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Tests.Infrastructure; + +namespace ClaudeDo.Worker.Tests.External; + +public sealed class NotesMcpToolsTests : IDisposable +{ + private readonly DbFixture _db = new(); + private readonly ClaudeDoDbContext _ctx; + private readonly DailyNoteRepository _notes; + private readonly NotesMcpTools _sut; + private static readonly DateOnly Today = DateOnly.FromDateTime(DateTime.Today); + + public NotesMcpToolsTests() + { + _ctx = _db.CreateContext(); + _notes = new DailyNoteRepository(_ctx); + _sut = new NotesMcpTools(_notes, new HubBroadcaster(new CapturingHubContext())); + } + + public void Dispose() { _ctx.Dispose(); _db.Dispose(); } + + [Fact] + public async Task AddNote_stores_it_on_today_flagged_as_agent_written() + { + var result = await _sut.AddNote(" Mika decided to keep the daily grouping. "); + + Assert.True(result.Saved); + var row = Assert.Single(await _notes.ListByDayAsync(Today)); + Assert.Equal("Mika decided to keep the daily grouping.", row.Text); + Assert.True(row.FromAgent); + Assert.Equal(NotesMcpTools.DailyAgentCap - 1, result.RemainingToday); + } + + [Fact] + public async Task AddNote_refuses_once_the_daily_cap_is_used_up_and_saves_nothing() + { + for (var i = 0; i < NotesMcpTools.DailyAgentCap; i++) + await _sut.AddNote($"note {i}"); + + var ex = await Assert.ThrowsAsync(() => _sut.AddNote("one too many")); + + Assert.Contains("cap", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal(NotesMcpTools.DailyAgentCap, (await _notes.ListByDayAsync(Today)).Count); + } + + [Fact] + public async Task Notes_the_user_wrote_do_not_count_against_the_agent_cap() + { + for (var i = 0; i < NotesMcpTools.DailyAgentCap; i++) + await _notes.AddAsync(Today, $"mine {i}"); + + var result = await _sut.AddNote("still allowed"); + + Assert.Equal(1, result.AgentNotesToday); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public async Task AddNote_rejects_empty_text(string text) => + await Assert.ThrowsAsync(() => _sut.AddNote(text)); + + [Fact] + public async Task AddNote_rejects_text_past_the_length_limit() + { + var tooLong = new string('x', NotesMcpTools.MaxLength + 1); + + var ex = await Assert.ThrowsAsync(() => _sut.AddNote(tooLong)); + + Assert.Contains(NotesMcpTools.MaxLength.ToString(), ex.Message); + Assert.Empty(await _notes.ListByDayAsync(Today)); + } +}