Merge branch 'claudedo/2de2f008758640b3a75e95719b1555bf'
This commit is contained in:
@@ -108,6 +108,21 @@ Offene Entscheidungen dazu:
|
||||
undokumentiert und kann sich ändern; bei Ausfall/Formatänderung ist das Gate wirkungslos
|
||||
(fail-open by design — kein Blocker, aber der Schutz fällt dann aus, ohne dass es auffällt).
|
||||
|
||||
## Offene Verifikation (2026-08-05, Max-Turns-Ceiling)
|
||||
|
||||
Build + unit tests grün (`ResolveMaxTurns`-Klemmung, Repository-Backfill von `model_presets`,
|
||||
Migration `AddMaxTurnsCeiling` gegen eine Scratch-DB angewendet), aber **nicht visuell
|
||||
verifiziert**:
|
||||
|
||||
- Agent-Settings-Editor (Task **und** Liste): Max-Turns-Feld auf einen Wert über der Ceiling
|
||||
(Default 80) setzen, Hinweistext unter dem `NumericUpDown` erscheint ("Runs are capped at
|
||||
{N} turns…").
|
||||
- Settings → Allgemein → Vorgaben pro Modell: eine Zeile über 80 setzen, derselbe Hinweistext
|
||||
erscheint unter der Zeile.
|
||||
- `MaxTurnsCeiling` selbst hat noch **keinen** eigenen Editor in der Settings-UI — der Wert wird
|
||||
beim Laden übernommen und beim Speichern nur unverändert zurückgeschrieben (kein Clobber),
|
||||
aber nicht editierbar. Falls gewünscht, ein eigenes Feld ergänzen.
|
||||
|
||||
---
|
||||
|
||||
## Bewusst verworfen (nicht erneut vorschlagen)
|
||||
|
||||
@@ -13,7 +13,7 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
||||
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||
- **TaskAttachmentEntity** — Id, TaskId (FK to tasks, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → table `task_attachments`
|
||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay) , `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets`), and `UsageGateFiveHourPct` / `UsageGateSevenDayPct` (int, defaults 80/90, columns `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` — percentage of the 5h/7d Claude usage window at which the autonomous queue pauses; `0` = gate off; `AppSettingsRepository.UpdateAsync` clamps both to 0..100)
|
||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay) , `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets` — `AppSettingsRepository.GetAsync` backfills shipping defaults into this column on the first read after it's null, so it's never null once a run has started), `MaxTurnsCeiling` (int, default 80, column `max_turns_ceiling` — hard ceiling every resolved max-turns value (task/list/global) is clamped to before a run starts; `AppSettingsRepository.UpdateAsync` clamps it to a minimum of 1), and `UsageGateFiveHourPct` / `UsageGateSevenDayPct` (int, defaults 80/90, columns `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` — percentage of the 5h/7d Claude usage window at which the autonomous queue pauses; `0` = gate off; `AppSettingsRepository.UpdateAsync` clamps both to 0..100). `DefaultMaxTurns` default was lowered from 100 to 40 (entity default + `AddMaxTurnsCeiling` migration backfill on the seeded row).
|
||||
- **ModelPresets** / **ModelPreset** — per-model run defaults (`Model`, `Effort`, `MaxTurns`): one row per `ModelRegistry.Aliases` entry, supplying the **global** effort and max-turns defaults. `Parse`/`Serialize` normalize (unknown models dropped, missing aliases filled from `Defaults`, effort validated, turns clamped 1–200) and never throw — a malformed settings row must not stop a run. `For(presets, model, fallbackMaxTurns = 30)` always returns a usable row: `model` is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`) still hits its alias's preset row instead of missing every lookup and falling through; only a model that normalizes to nothing recognized falls back to a synthesized row (`EffortRegistry.DefaultLevel` + `fallbackMaxTurns` — callers pass `AppSettings.DefaultMaxTurns` here so that setting has a real effect instead of a hardcoded number). Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
|
||||
- **ModelRegistry.TryNormalizeAlias** — non-throwing counterpart to `NormalizeAlias` for the run path: exact alias match, then substring match against a full model id, else `null`. Never throws, unlike `NormalizeAlias` (which stays the strict, throwing validator for `add_task`/planning model input).
|
||||
- **EffortRegistry** — the `--effort` levels (`low|medium|high|xhigh|max`) + `NormalizeLevel` (blank → null = don't pass the flag)
|
||||
@@ -45,7 +45,7 @@ All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Q
|
||||
|
||||
## Schema
|
||||
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. Migration `AddModelPresetsAndManualFlag` added `app_settings.model_presets` plus the `is_manual` flags on `tasks` and `lists`. Migration `AddHandlerCommitRange` added the nullable `handler_base_commit`/`handler_head_commit` columns to `tasks`. Migration `AddUsageGateAndRunModel` added `app_settings.usage_gate_five_hour_pct`/`usage_gate_seven_day_pct` (defaults 80/90) and the nullable `task_runs.model` column. Migration `AddWorktreeMergeCommit` added the nullable `worktrees.merge_commit` column. Migration `AddVerifyCommand` added the nullable `list_config.verify_command` column. Migration `AddRunCacheTokens` added the nullable `task_runs.cache_read_tokens`/`cache_write_tokens` columns. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. Migration `AddModelPresetsAndManualFlag` added `app_settings.model_presets` plus the `is_manual` flags on `tasks` and `lists`. Migration `AddHandlerCommitRange` added the nullable `handler_base_commit`/`handler_head_commit` columns to `tasks`. Migration `AddUsageGateAndRunModel` added `app_settings.usage_gate_five_hour_pct`/`usage_gate_seven_day_pct` (defaults 80/90) and the nullable `task_runs.model` column. Migration `AddWorktreeMergeCommit` added the nullable `worktrees.merge_commit` column. Migration `AddVerifyCommand` added the nullable `list_config.verify_command` column. Migration `AddMaxTurnsCeiling` added `app_settings.max_turns_ceiling` (default 80) and lowered `default_max_turns`'s default/seeded value from 100 to 40. Migration `AddRunCacheTokens` added the nullable `task_runs.cache_read_tokens`/`cache_write_tokens` columns. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -18,10 +18,13 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
||||
builder.Property(s => s.DefaultModel)
|
||||
.HasColumnName("default_model").IsRequired().HasDefaultValue("sonnet");
|
||||
builder.Property(s => s.DefaultMaxTurns)
|
||||
.HasColumnName("default_max_turns").IsRequired().HasDefaultValue(30);
|
||||
.HasColumnName("default_max_turns").IsRequired().HasDefaultValue(40);
|
||||
builder.Property(s => s.DefaultPermissionMode)
|
||||
.HasColumnName("default_permission_mode").IsRequired().HasDefaultValue("bypassPermissions");
|
||||
|
||||
builder.Property(s => s.MaxTurnsCeiling)
|
||||
.HasColumnName("max_turns_ceiling").IsRequired().HasDefaultValue(80);
|
||||
|
||||
builder.Property(s => s.MaxParallelExecutions)
|
||||
.HasColumnName("max_parallel_executions").IsRequired().HasDefaultValue(1);
|
||||
|
||||
|
||||
@@ -0,0 +1,843 @@
|
||||
// <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("20260805132052_AddMaxTurnsCeiling")]
|
||||
partial class AddMaxTurnsCeiling
|
||||
{
|
||||
/// <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>("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,
|
||||
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<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>("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<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>("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<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,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMaxTurnsCeiling : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "default_max_turns",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 40,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldDefaultValue: 30);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "max_turns_ceiling",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 80);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "default_max_turns", "max_turns_ceiling" },
|
||||
values: new object[] { 40, 80 });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "max_turns_ceiling",
|
||||
table: "app_settings");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "default_max_turns",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 30,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldDefaultValue: 40);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
column: "default_max_turns",
|
||||
value: 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ namespace ClaudeDo.Data.Migrations
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasDefaultValue(40)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
@@ -66,6 +66,12 @@ namespace ClaudeDo.Data.Migrations
|
||||
.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");
|
||||
@@ -129,10 +135,11 @@ namespace ClaudeDo.Data.Migrations
|
||||
Id = 1,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultMaxTurns = 40,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
MaxTurnsCeiling = 80,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
|
||||
@@ -8,9 +8,13 @@ public sealed class AppSettingsEntity
|
||||
|
||||
public string DefaultClaudeInstructions { get; set; } = string.Empty;
|
||||
public string DefaultModel { get; set; } = "sonnet";
|
||||
public int DefaultMaxTurns { get; set; } = 100;
|
||||
public int DefaultMaxTurns { get; set; } = 40;
|
||||
public string DefaultPermissionMode { get; set; } = "auto";
|
||||
|
||||
// Hard ceiling every resolved max-turns value (task/list/global) is clamped to before a run
|
||||
// starts. Guards against runaway sessions regardless of what a task/list override requests.
|
||||
public int MaxTurnsCeiling { get; set; } = 80;
|
||||
|
||||
public int MaxParallelExecutions { get; set; } = 1;
|
||||
|
||||
public string WorktreeStrategy { get; set; } = "sibling";
|
||||
|
||||
@@ -14,9 +14,9 @@ public sealed class AppSettingsRepository
|
||||
{
|
||||
var row = await _context.AppSettings.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == AppSettingsEntity.SingletonId, ct);
|
||||
if (row is not null) return row;
|
||||
|
||||
row = new AppSettingsEntity { Id = AppSettingsEntity.SingletonId };
|
||||
if (row is null)
|
||||
{
|
||||
row = new AppSettingsEntity { Id = AppSettingsEntity.SingletonId, ModelPresets = ModelPresets.SerializeDefaults() };
|
||||
_context.AppSettings.Add(row);
|
||||
try
|
||||
{
|
||||
@@ -33,6 +33,26 @@ public sealed class AppSettingsRepository
|
||||
return row;
|
||||
}
|
||||
|
||||
// First read after upgrading from a null model_presets column: persist the shipping
|
||||
// defaults so the Settings UI shows real, editable rows instead of a code-only fallback.
|
||||
if (row.ModelPresets is null)
|
||||
row.ModelPresets = await BackfillModelPresetsAsync(ct);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private async Task<string> BackfillModelPresetsAsync(CancellationToken ct)
|
||||
{
|
||||
var defaults = ModelPresets.SerializeDefaults();
|
||||
var tracked = await GetOrCreateTrackedRowAsync(ct);
|
||||
if (tracked.ModelPresets is null)
|
||||
{
|
||||
tracked.ModelPresets = defaults;
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
private async Task<AppSettingsEntity> GetOrCreateTrackedRowAsync(CancellationToken ct)
|
||||
{
|
||||
var row = await _context.AppSettings
|
||||
@@ -52,6 +72,7 @@ public sealed class AppSettingsRepository
|
||||
row.DefaultClaudeInstructions = updated.DefaultClaudeInstructions ?? string.Empty;
|
||||
row.DefaultModel = string.IsNullOrWhiteSpace(updated.DefaultModel) ? "sonnet" : updated.DefaultModel;
|
||||
row.DefaultMaxTurns = updated.DefaultMaxTurns;
|
||||
row.MaxTurnsCeiling = updated.MaxTurnsCeiling < 1 ? 1 : updated.MaxTurnsCeiling;
|
||||
row.DefaultPermissionMode = string.IsNullOrWhiteSpace(updated.DefaultPermissionMode)
|
||||
? "auto" : updated.DefaultPermissionMode;
|
||||
row.MaxParallelExecutions = updated.MaxParallelExecutions < 1 ? 1 : updated.MaxParallelExecutions;
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"overrideBadge": "überschrieben",
|
||||
"resetToInherited": "Auf geerbt zurücksetzen"
|
||||
},
|
||||
"turnsCeilingHint": "Läufe sind auf {0} Durchläufe gedeckelt — dieser Wert wird geklemmt.",
|
||||
"agentEditor": {
|
||||
"model": "Modell",
|
||||
"maxTurns": "Max. Durchläufe",
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"overrideBadge": "override",
|
||||
"resetToInherited": "Reset to inherited"
|
||||
},
|
||||
"turnsCeilingHint": "Runs are capped at {0} turns — this value will be clamped.",
|
||||
"agentEditor": {
|
||||
"model": "Model",
|
||||
"maxTurns": "Max turns",
|
||||
|
||||
@@ -625,7 +625,8 @@ public sealed record AppSettingsDto(
|
||||
List<string>? SessionSkills = null,
|
||||
List<ModelPresetDto>? ModelPresets = null,
|
||||
int UsageGateFiveHourPct = 80,
|
||||
int UsageGateSevenDayPct = 90);
|
||||
int UsageGateSevenDayPct = 90,
|
||||
int MaxTurnsCeiling = 80);
|
||||
|
||||
// Per-model run defaults (effort + turn budget) edited in Settings → General.
|
||||
public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||
|
||||
@@ -43,6 +43,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
[ObservableProperty] private string _modelInheritedHint = "";
|
||||
[ObservableProperty] private string _turnsBadge = "";
|
||||
[ObservableProperty] private string _turnsInheritedHint = "";
|
||||
[ObservableProperty] private string _turnsCeilingHint = "";
|
||||
[ObservableProperty] private string _agentBadge = "";
|
||||
[ObservableProperty] private string _effectiveSystemPromptHint = "";
|
||||
|
||||
@@ -50,6 +51,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
// The global max-turns default is per-model (Settings -> General), so it moves with whichever
|
||||
// model actually ends up in effect here.
|
||||
private IReadOnlyList<ModelPreset> _presets = ModelPresets.Defaults;
|
||||
private int _maxTurnsCeiling = 80;
|
||||
private string EffectiveModel => Model ?? _listModel ?? _globalModel;
|
||||
private int GlobalMaxTurns => ModelPresets.For(_presets, EffectiveModel).MaxTurns;
|
||||
private string? _listModel; // Task scope only
|
||||
@@ -145,6 +147,9 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
: InheritanceResolver.ResolveList(own, GlobalMaxTurns.ToString());
|
||||
TurnsInheritedHint = value;
|
||||
TurnsBadge = BadgeFor(source, MaxTurns is not null);
|
||||
TurnsCeilingHint = MaxTurns is decimal t && (int)t > _maxTurnsCeiling
|
||||
? Loc.T("settings.turnsCeilingHint", _maxTurnsCeiling)
|
||||
: "";
|
||||
}
|
||||
|
||||
private void RecomputeAgentBadge()
|
||||
@@ -301,6 +306,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa
|
||||
_presets = app?.ModelPresets is { Count: > 0 } rows
|
||||
? rows.Select(r => new ModelPreset(r.Model, r.Effort, r.MaxTurns)).ToList()
|
||||
: ModelPresets.Defaults;
|
||||
_maxTurnsCeiling = app?.MaxTurnsCeiling ?? 80;
|
||||
}
|
||||
|
||||
private void ApplyConfig(string? model, int? maxTurns, string? systemPrompt, string? agentPath)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Agent;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
@@ -15,7 +16,11 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
||||
|
||||
[ObservableProperty] private string _defaultClaudeInstructions = "";
|
||||
[ObservableProperty] private string _defaultModel = ModelRegistry.DefaultAlias;
|
||||
[ObservableProperty] private int _defaultMaxTurns = 100;
|
||||
[ObservableProperty] private int _defaultMaxTurns = 40;
|
||||
// Hard ceiling every resolved max-turns value is clamped to before a run starts. Not directly
|
||||
// editable here yet — loaded and echoed back on save so it round-trips, and drives the clamp
|
||||
// hints on the preset rows below and on AgentConfigEditorViewModel.
|
||||
[ObservableProperty] private int _maxTurnsCeiling = 80;
|
||||
[ObservableProperty] private string _defaultPermissionMode = PermissionModeRegistry.DefaultMode;
|
||||
[ObservableProperty] private int _maxParallelExecutions = 1;
|
||||
// Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off.
|
||||
@@ -56,14 +61,14 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
||||
/// model. Supplies the global defaults; list- and task-level max-turns overrides still win.</summary>
|
||||
public ObservableCollection<ModelPresetRowViewModel> ModelPresets { get; } = new();
|
||||
|
||||
public void LoadModelPresets(IReadOnlyCollection<ModelPresetDto>? presets)
|
||||
public void LoadModelPresets(IReadOnlyCollection<ModelPresetDto>? presets, int ceiling)
|
||||
{
|
||||
ModelPresets.Clear();
|
||||
var source = presets is { Count: > 0 }
|
||||
? presets.Select(p => new ModelPreset(p.Model, p.Effort, p.MaxTurns)).ToList()
|
||||
: Data.Models.ModelPresets.Defaults.ToList();
|
||||
foreach (var p in Data.Models.ModelPresets.Parse(Data.Models.ModelPresets.Serialize(source)))
|
||||
ModelPresets.Add(new ModelPresetRowViewModel(p));
|
||||
ModelPresets.Add(new ModelPresetRowViewModel(p, ceiling));
|
||||
}
|
||||
|
||||
public List<ModelPresetDto> ModelPresetDtos()
|
||||
@@ -130,11 +135,20 @@ public sealed partial class ModelPresetRowViewModel : ViewModelBase
|
||||
[ObservableProperty] private string _effort;
|
||||
// decimal so it binds straight to a NumericUpDown, like the other numeric settings.
|
||||
[ObservableProperty] private decimal _maxTurns;
|
||||
[ObservableProperty] private int _ceiling;
|
||||
|
||||
public ModelPresetRowViewModel(ModelPreset preset)
|
||||
public ModelPresetRowViewModel(ModelPreset preset, int ceiling)
|
||||
{
|
||||
Model = preset.Model;
|
||||
_effort = preset.Effort;
|
||||
_maxTurns = preset.MaxTurns;
|
||||
_ceiling = ceiling;
|
||||
}
|
||||
|
||||
public string ClampHint => MaxTurns > Ceiling
|
||||
? Loc.T("settings.turnsCeilingHint", Ceiling)
|
||||
: "";
|
||||
|
||||
partial void OnMaxTurnsChanged(decimal value) => OnPropertyChanged(nameof(ClampHint));
|
||||
partial void OnCeilingChanged(int value) => OnPropertyChanged(nameof(ClampHint));
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
General.DefaultClaudeInstructions = dto.DefaultClaudeInstructions ?? "";
|
||||
General.DefaultModel = dto.DefaultModel ?? "sonnet";
|
||||
General.DefaultMaxTurns = dto.DefaultMaxTurns;
|
||||
General.MaxTurnsCeiling = dto.MaxTurnsCeiling;
|
||||
General.DefaultPermissionMode = dto.DefaultPermissionMode ?? "auto";
|
||||
General.MaxParallelExecutions = dto.MaxParallelExecutions;
|
||||
General.UsageGateFiveHourPct = dto.UsageGateFiveHourPct;
|
||||
@@ -80,7 +81,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
await OnlineInbox.LoadAsync();
|
||||
await SessionSkills.LoadAsync();
|
||||
await General.LoadSessionSkillsAsync(_worker, dto?.SessionSkills);
|
||||
General.LoadModelPresets(dto?.ModelPresets);
|
||||
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
@@ -113,7 +114,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
General.SelectedSessionSkillNames(),
|
||||
General.ModelPresetDtos(),
|
||||
General.UsageGateFiveHourPct,
|
||||
General.UsageGateSevenDayPct);
|
||||
General.UsageGateSevenDayPct,
|
||||
General.MaxTurnsCeiling);
|
||||
await _worker.UpdateAppSettingsAsync(dto);
|
||||
await Prime.SaveAsync();
|
||||
await OnlineInbox.SaveAsync();
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
PlaceholderText="{Binding TurnsInheritedHint}"
|
||||
Minimum="1" Maximum="200" Increment="1" FormatString="0"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="{Binding TurnsCeilingHint}"
|
||||
IsVisible="{Binding TurnsCeilingHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- System prompt -->
|
||||
|
||||
@@ -129,7 +129,8 @@
|
||||
<ItemsControl ItemsSource="{Binding General.ModelPresets}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="settings:ModelPresetRowViewModel">
|
||||
<Grid ColumnDefinitions="90,12,*,12,110" Margin="0,0,0,6">
|
||||
<StackPanel Spacing="2" Margin="0,0,0,6">
|
||||
<Grid ColumnDefinitions="90,12,*,12,110">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Model}" VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Column="2" ItemsSource="{Binding EffortLevels}"
|
||||
SelectedItem="{Binding Effort, Mode=TwoWay}"
|
||||
@@ -137,6 +138,10 @@
|
||||
<NumericUpDown Grid.Column="4" Value="{Binding MaxTurns, Mode=TwoWay}"
|
||||
Minimum="1" Maximum="200" Increment="1" FormatString="0"/>
|
||||
</Grid>
|
||||
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="{Binding ClampHint}"
|
||||
IsVisible="{Binding ClampHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -161,7 +161,7 @@ A child that hits a roadblock (fails, or reports `CLAUDEDO_BLOCKED` roadblocks)
|
||||
1. Load task + list metadata from DB; resolve config from `list_config` + task-level overrides (model, system_prompt, agent_path)
|
||||
2. Create worktree (if `WorkingDir` set) or sandbox directory
|
||||
3. Mark task "running", broadcast `TaskStarted`
|
||||
4. Resolve the effective model (task → list → `AppSettings.DefaultModel`), then take its `ModelPresets` row via `ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns)`: the model string is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`, not just the bare `sonnet`/`opus`/`haiku`/`fable` aliases) still hits its alias's preset row; only a truly unrecognized model falls back to a synthesized row using `AppSettings.DefaultMaxTurns` (never a hardcoded number, and never throws — an unknown model must not block a run). The preset supplies `--effort` and the **global** max-turns default (task/list `MaxTurns` overrides still win). Build CLI args via `ClaudeArgsBuilder`; inject attachment absolute paths via `TaskPromptComposer.Compose` (appends a read-only "## Reference files" section); invoke `ClaudeProcess` with task prompt
|
||||
4. Resolve the effective model (task → list → `AppSettings.DefaultModel`), then take its `ModelPresets` row via `ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns)`: the model string is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`, not just the bare `sonnet`/`opus`/`haiku`/`fable` aliases) still hits its alias's preset row; only a truly unrecognized model falls back to a synthesized row using `AppSettings.DefaultMaxTurns` (never a hardcoded number, and never throws — an unknown model must not block a run). The preset supplies `--effort` and the **global** max-turns default (task/list `MaxTurns` overrides still win). The task/list/global-resolved value is then hard-clamped to `AppSettings.MaxTurnsCeiling` (default 80) via `TaskRunner.ResolveMaxTurns` — a task or list override above the ceiling still starts, just capped, and a Warn is logged with the task id, requested, and effective value. Build CLI args via `ClaudeArgsBuilder`; inject attachment absolute paths via `TaskPromptComposer.Compose` (appends a read-only "## Reference files" section); invoke `ClaudeProcess` with task prompt
|
||||
5. Stream NDJSON output through `StreamAnalyzer`; lines forwarded to log file and SignalR (`TaskMessage`)
|
||||
6. On success: auto-commit changes (worktree only), store run record, mark "done"
|
||||
7. On failure: retry once if session ID available (`--resume`), then mark "failed"
|
||||
|
||||
@@ -45,7 +45,8 @@ public record AppSettingsDto(
|
||||
List<string>? SessionSkills = null,
|
||||
List<ModelPresetDto>? ModelPresets = null,
|
||||
int UsageGateFiveHourPct = 80,
|
||||
int UsageGateSevenDayPct = 90);
|
||||
int UsageGateSevenDayPct = 90,
|
||||
int MaxTurnsCeiling = 80);
|
||||
|
||||
// Per-model run defaults (effort + turn budget) edited in Settings -> General.
|
||||
public record ModelPresetDto(string Model, string Effort, int MaxTurns);
|
||||
@@ -382,7 +383,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
Data.Models.ModelPresets.Parse(row.ModelPresets)
|
||||
.Select(p => new ModelPresetDto(p.Model, p.Effort, p.MaxTurns)).ToList(),
|
||||
row.UsageGateFiveHourPct,
|
||||
row.UsageGateSevenDayPct);
|
||||
row.UsageGateSevenDayPct,
|
||||
row.MaxTurnsCeiling);
|
||||
}
|
||||
|
||||
public async Task UpdateAppSettings(AppSettingsDto dto)
|
||||
@@ -412,6 +414,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
: Data.Models.ModelPresets.SerializeDefaults(),
|
||||
UsageGateFiveHourPct = dto.UsageGateFiveHourPct,
|
||||
UsageGateSevenDayPct = dto.UsageGateSevenDayPct,
|
||||
MaxTurnsCeiling = dto.MaxTurnsCeiling,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -564,12 +564,21 @@ public sealed class TaskRunner
|
||||
var model = task.Model ?? listConfig?.Model ?? global.DefaultModel;
|
||||
var preset = Data.Models.ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns);
|
||||
|
||||
var requestedMaxTurns = task.MaxTurns ?? listConfig?.MaxTurns ?? preset.MaxTurns;
|
||||
var maxTurns = ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns, global.MaxTurnsCeiling);
|
||||
if (maxTurns < requestedMaxTurns)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Task {TaskId}: max turns clamped to ceiling (requested={Requested}, effective={Effective}, ceiling={Ceiling})",
|
||||
task.Id, requestedMaxTurns, maxTurns, global.MaxTurnsCeiling);
|
||||
}
|
||||
|
||||
return new ClaudeRunConfig(
|
||||
Model: model,
|
||||
SystemPrompt: string.IsNullOrWhiteSpace(instructions) ? null : instructions,
|
||||
AgentPath: task.AgentPath ?? listConfig?.AgentPath,
|
||||
ResumeSessionId: resumeSessionId,
|
||||
MaxTurns: ResolveMaxTurns(task.MaxTurns, listConfig?.MaxTurns, preset.MaxTurns),
|
||||
MaxTurns: maxTurns,
|
||||
PermissionMode: global.DefaultPermissionMode,
|
||||
SkillNames: skillNames,
|
||||
Effort: preset.Effort);
|
||||
@@ -628,8 +637,8 @@ public sealed class TaskRunner
|
||||
return names;
|
||||
}
|
||||
|
||||
internal static int? ResolveMaxTurns(int? taskTurns, int? listTurns, int globalDefault)
|
||||
=> taskTurns ?? listTurns ?? globalDefault;
|
||||
internal static int? ResolveMaxTurns(int? taskTurns, int? listTurns, int globalDefault, int ceiling)
|
||||
=> Math.Min(taskTurns ?? listTurns ?? globalDefault, ceiling);
|
||||
|
||||
public static string MergeInstructions(params string?[] parts)
|
||||
{
|
||||
|
||||
@@ -20,13 +20,49 @@ public class AppSettingsRepositoryTests : IDisposable
|
||||
|
||||
Assert.Equal(AppSettingsEntity.SingletonId, row.Id);
|
||||
Assert.Equal("sonnet", row.DefaultModel);
|
||||
Assert.Equal(100, row.DefaultMaxTurns);
|
||||
Assert.Equal(40, row.DefaultMaxTurns);
|
||||
Assert.Equal(80, row.MaxTurnsCeiling);
|
||||
Assert.Equal("auto", row.DefaultPermissionMode);
|
||||
Assert.Equal("sibling", row.WorktreeStrategy);
|
||||
Assert.Null(row.CentralWorktreeRoot);
|
||||
Assert.False(row.WorktreeAutoCleanupEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_Persists_MaxTurnsCeiling()
|
||||
{
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
var repo = new AppSettingsRepository(ctx);
|
||||
await repo.UpdateAsync(new AppSettingsEntity { MaxTurnsCeiling = 60 });
|
||||
}
|
||||
|
||||
using var readCtx = _db.CreateContext();
|
||||
var row = await new AppSettingsRepository(readCtx).GetAsync();
|
||||
Assert.Equal(60, row.MaxTurnsCeiling);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_Backfills_Null_ModelPresets_With_Defaults()
|
||||
{
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
var repo = new AppSettingsRepository(ctx);
|
||||
// Force a row to exist with model_presets left null (the pre-upgrade state).
|
||||
await repo.UpdateAsync(new AppSettingsEntity());
|
||||
}
|
||||
|
||||
using var readCtx = _db.CreateContext();
|
||||
var row = await new AppSettingsRepository(readCtx).GetAsync();
|
||||
|
||||
Assert.NotNull(row.ModelPresets);
|
||||
Assert.Equal(ModelPresets.SerializeDefaults(), row.ModelPresets);
|
||||
|
||||
using var rereadCtx = _db.CreateContext();
|
||||
var reread = await new AppSettingsRepository(rereadCtx).GetAsync();
|
||||
Assert.Equal(ModelPresets.SerializeDefaults(), reread.ModelPresets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_Persists_And_RoundTrips()
|
||||
{
|
||||
|
||||
@@ -7,13 +7,21 @@ public class MaxTurnsResolutionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Task_override_wins()
|
||||
=> Assert.Equal(5, TaskRunner.ResolveMaxTurns(taskTurns: 5, listTurns: 20, globalDefault: 100));
|
||||
=> Assert.Equal(5, TaskRunner.ResolveMaxTurns(taskTurns: 5, listTurns: 20, globalDefault: 100, ceiling: 200));
|
||||
|
||||
[Fact]
|
||||
public void List_override_used_when_no_task_override()
|
||||
=> Assert.Equal(20, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: 20, globalDefault: 100));
|
||||
=> Assert.Equal(20, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: 20, globalDefault: 100, ceiling: 200));
|
||||
|
||||
[Fact]
|
||||
public void Falls_back_to_global_default()
|
||||
=> Assert.Equal(100, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: null, globalDefault: 100));
|
||||
=> Assert.Equal(100, TaskRunner.ResolveMaxTurns(taskTurns: null, listTurns: null, globalDefault: 100, ceiling: 200));
|
||||
|
||||
[Fact]
|
||||
public void Below_ceiling_is_unchanged()
|
||||
=> Assert.Equal(60, TaskRunner.ResolveMaxTurns(taskTurns: 60, listTurns: null, globalDefault: 40, ceiling: 80));
|
||||
|
||||
[Fact]
|
||||
public void Above_ceiling_is_clamped()
|
||||
=> Assert.Equal(80, TaskRunner.ResolveMaxTurns(taskTurns: 200, listTurns: null, globalDefault: 40, ceiling: 80));
|
||||
}
|
||||
|
||||
@@ -87,9 +87,7 @@ public sealed class ModelResolutionWireTests : IDisposable
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
var settingsRepo = new AppSettingsRepository(ctx);
|
||||
var settings = await settingsRepo.GetAsync();
|
||||
settings.DefaultMaxTurns = 100;
|
||||
await ctx.SaveChangesAsync();
|
||||
await settingsRepo.UpdateAsync(new AppSettingsEntity { DefaultMaxTurns = 50 });
|
||||
}
|
||||
|
||||
Exception? thrown = null;
|
||||
@@ -106,6 +104,6 @@ public sealed class ModelResolutionWireTests : IDisposable
|
||||
Assert.Null(thrown);
|
||||
var args = getArgs().ToList();
|
||||
Assert.Contains("--max-turns", args);
|
||||
Assert.Equal("100", args[args.IndexOf("--max-turns") + 1]);
|
||||
Assert.Equal("50", args[args.IndexOf("--max-turns") + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user