feat(data): add ImportTickets prime kind, ticket fallback list and import prompt

This commit is contained in:
mika kuns
2026-10-01 08:33:48 +02:00
parent c34680b42d
commit a0b234fe10
12 changed files with 1285 additions and 2 deletions
@@ -81,6 +81,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
builder.Property(s => s.ListHandlerMaxHandoffs)
.HasColumnName("list_handler_max_handoffs").IsRequired().HasDefaultValue(4);
builder.Property(s => s.TicketFallbackListId)
.HasColumnName("ticket_fallback_list_id");
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
}
}
@@ -11,12 +11,14 @@ public class PrimeScheduleEntityConfiguration : IEntityTypeConfiguration<PrimeSc
=> v == PrimeActionKind.Ping ? "ping"
: v == PrimeActionKind.FillMyDay ? "fill_my_day"
: v == PrimeActionKind.Custom ? "custom"
: v == PrimeActionKind.ImportTickets ? "import_tickets"
: throw new ArgumentOutOfRangeException(nameof(v));
private static PrimeActionKind KindFromString(string v)
=> v == "ping" ? PrimeActionKind.Ping
: v == "fill_my_day" ? PrimeActionKind.FillMyDay
: v == "custom" ? PrimeActionKind.Custom
: v == "import_tickets" ? PrimeActionKind.ImportTickets
: throw new ArgumentOutOfRangeException(nameof(v));
private static readonly ValueConverter<PrimeActionKind, string> KindConverter =
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddTicketFallbackList : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ticket_fallback_list_id",
table: "app_settings",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "app_settings",
keyColumn: "id",
keyValue: 1,
column: "ticket_fallback_list_id",
value: null);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ticket_fallback_list_id",
table: "app_settings");
}
}
}
@@ -132,6 +132,10 @@ namespace ClaudeDo.Data.Migrations
.HasDefaultValue(3)
.HasColumnName("standup_weekday");
b.Property<string>("TicketFallbackListId")
.HasColumnType("TEXT")
.HasColumnName("ticket_fallback_list_id");
b.Property<int>("UsageGateFiveHourPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -83,4 +83,8 @@ public sealed class AppSettingsEntity
// 4 = two full wait/merge cycles, the long-standing default. Raise it for a run meant to keep
// going for hours. It exists to bound runaway handoff loops, so it must stay >= 1.
public int ListHandlerMaxHandoffs { get; set; } = 4;
// List that receives imported ticket items Claude could not place (import_ticket without a
// list). Null = such items are skipped. No FK: a deleted list simply reads as unset at import.
public string? TicketFallbackListId { get; set; }
}
+3 -1
View File
@@ -8,5 +8,7 @@ public enum PrimeActionKind
/// <summary>Run the daily prep prompt and let Claude fill MyDay.</summary>
FillMyDay,
/// <summary>Run the schedule's own prompt with the ClaudeDo MCP tools available.</summary>
Custom
Custom,
/// <summary>Import the assigned ticket-system items into lists by content, then fill MyDay.</summary>
ImportTickets
}
+19 -1
View File
@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
namespace ClaudeDo.Data;
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelperTriage, MergeHelperWait, MergeHelperMerge, MergeHelperInitial, MergeHelperHandoff, PowerUseAddendum }
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelperTriage, MergeHelperWait, MergeHelperMerge, MergeHelperInitial, MergeHelperHandoff, PowerUseAddendum, TicketImport }
/// <summary>
/// How a prompt kind's on-disk override (if any) relates to the bundled default.
@@ -45,6 +45,7 @@ public static class PromptFiles
PromptKind.MergeHelperInitial => "merge-helper-initial.md",
PromptKind.MergeHelperHandoff => "merge-helper-handoff.md",
PromptKind.PowerUseAddendum => "power-use.md",
PromptKind.TicketImport => "ticket-import.md",
_ => throw new ArgumentOutOfRangeException(nameof(kind))
};
@@ -223,6 +224,7 @@ public static class PromptFiles
PromptKind.MergeHelperInitial => MergeHelperInitialDefault,
PromptKind.MergeHelperHandoff => MergeHelperHandoffDefault,
PromptKind.PowerUseAddendum => PowerUseAddendumDefault,
PromptKind.TicketImport => TicketImportDefault,
_ => ""
};
@@ -385,6 +387,22 @@ public static class PromptFiles
(build + tests) before you stop.
""";
private const string TicketImportDefault = """
You are sorting my assigned items from the company ticket system into my ClaudeDo lists.
1. Call mcp__claudedo__get_ticket_import_candidates. If available is false, say why and stop.
If items is empty, say so and stop.
2. For every item, pick the list whose purpose fits the item's CONTENT — its title, its
parent and its project. A list's ticketProject is only a hint: one ticket project can
hold work for several repositories, and several lists can share one project.
3. Import each item exactly once with mcp__claudedo__import_ticket(ticketId, list).
If no list clearly fits, omit list — the item then goes to the fallback list.
Never force a weak match just to avoid the fallback.
4. Do not create, edit or move anything else.
5. Finish with a short summary: imported items per list, items that went to the fallback
list, and skipped items with their reason.
""";
private const string DailyPrepDefault = """
You are preparing my workday for {date}.
@@ -127,4 +127,12 @@ public sealed class AppSettingsRepository
row.RepoImportFolders = list.Count == 0 ? null : JsonSerializer.Serialize(list);
await _context.SaveChangesAsync(ct);
}
/// <summary>Writes only the ticket fallback list; blank clears it.</summary>
public async Task SetTicketFallbackListIdAsync(string? listId, CancellationToken ct = default)
{
var row = await GetOrCreateTrackedRowAsync(ct);
row.TicketFallbackListId = string.IsNullOrWhiteSpace(listId) ? null : listId.Trim();
await _context.SaveChangesAsync(ct);
}
}
@@ -64,4 +64,15 @@ public sealed class PrimeScheduleRepositoryTests : IDisposable
var found = await _repo.GetAsync(entity.Id);
Assert.Equal("new override", found!.PromptOverride);
}
[Fact]
public async Task ImportTickets_kind_round_trips()
{
var entity = new PrimeScheduleEntity { Kind = PrimeActionKind.ImportTickets };
await _repo.UpsertAsync(entity);
var found = await _repo.GetAsync(entity.Id);
Assert.Equal(PrimeActionKind.ImportTickets, found!.Kind);
}
}
@@ -432,4 +432,12 @@ public class PromptFilesTests
Assert.Contains("handoff_list_handler", d);
Assert.Contains("do not continue into phase 3 yourself", d, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void TicketImport_default_names_both_tools()
{
var d = PromptFiles.DefaultFor(PromptKind.TicketImport);
Assert.Contains("mcp__claudedo__get_ticket_import_candidates", d);
Assert.Contains("mcp__claudedo__import_ticket", d);
}
}
@@ -1,3 +1,4 @@
using ClaudeDo.Data.Repositories;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
@@ -60,4 +61,31 @@ public sealed class TicketFieldsTests : IDisposable
Assert.Null((await read.Tasks.SingleAsync()).TicketRef);
Assert.Null((await read.ListConfigs.SingleAsync()).TicketProjectId);
}
[Fact]
public async Task TicketFallbackListId_round_trips_and_survives_a_settings_save()
{
await using (var ctx = NewContext())
await new AppSettingsRepository(ctx).SetTicketFallbackListIdAsync("l-fallback");
// The general settings save must not wipe it — UpdateAsync's copy block doesn't carry it.
await using (var ctx = NewContext())
{
var repo = new AppSettingsRepository(ctx);
await repo.UpdateAsync(await repo.GetAsync());
}
await using var read = NewContext();
Assert.Equal("l-fallback", (await new AppSettingsRepository(read).GetAsync()).TicketFallbackListId);
}
[Fact]
public async Task Blank_TicketFallbackListId_is_stored_as_null()
{
await using (var ctx = NewContext())
await new AppSettingsRepository(ctx).SetTicketFallbackListIdAsync(" ");
await using var read = NewContext();
Assert.Null((await new AppSettingsRepository(read).GetAsync()).TicketFallbackListId);
}
}