feat(data): add ImportTickets prime kind, ticket fallback list and import prompt
This commit is contained in:
@@ -81,6 +81,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
|||||||
builder.Property(s => s.ListHandlerMaxHandoffs)
|
builder.Property(s => s.ListHandlerMaxHandoffs)
|
||||||
.HasColumnName("list_handler_max_handoffs").IsRequired().HasDefaultValue(4);
|
.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 });
|
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ public class PrimeScheduleEntityConfiguration : IEntityTypeConfiguration<PrimeSc
|
|||||||
=> v == PrimeActionKind.Ping ? "ping"
|
=> v == PrimeActionKind.Ping ? "ping"
|
||||||
: v == PrimeActionKind.FillMyDay ? "fill_my_day"
|
: v == PrimeActionKind.FillMyDay ? "fill_my_day"
|
||||||
: v == PrimeActionKind.Custom ? "custom"
|
: v == PrimeActionKind.Custom ? "custom"
|
||||||
|
: v == PrimeActionKind.ImportTickets ? "import_tickets"
|
||||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
: throw new ArgumentOutOfRangeException(nameof(v));
|
||||||
|
|
||||||
private static PrimeActionKind KindFromString(string v)
|
private static PrimeActionKind KindFromString(string v)
|
||||||
=> v == "ping" ? PrimeActionKind.Ping
|
=> v == "ping" ? PrimeActionKind.Ping
|
||||||
: v == "fill_my_day" ? PrimeActionKind.FillMyDay
|
: v == "fill_my_day" ? PrimeActionKind.FillMyDay
|
||||||
: v == "custom" ? PrimeActionKind.Custom
|
: v == "custom" ? PrimeActionKind.Custom
|
||||||
|
: v == "import_tickets" ? PrimeActionKind.ImportTickets
|
||||||
: throw new ArgumentOutOfRangeException(nameof(v));
|
: throw new ArgumentOutOfRangeException(nameof(v));
|
||||||
|
|
||||||
private static readonly ValueConverter<PrimeActionKind, string> KindConverter =
|
private static readonly ValueConverter<PrimeActionKind, string> KindConverter =
|
||||||
|
|||||||
+1160
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)
|
.HasDefaultValue(3)
|
||||||
.HasColumnName("standup_weekday");
|
.HasColumnName("standup_weekday");
|
||||||
|
|
||||||
|
b.Property<string>("TicketFallbackListId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("ticket_fallback_list_id");
|
||||||
|
|
||||||
b.Property<int>("UsageGateFiveHourPct")
|
b.Property<int>("UsageGateFiveHourPct")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.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
|
// 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.
|
// going for hours. It exists to bound runaway handoff loops, so it must stay >= 1.
|
||||||
public int ListHandlerMaxHandoffs { get; set; } = 4;
|
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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ public enum PrimeActionKind
|
|||||||
/// <summary>Run the daily prep prompt and let Claude fill MyDay.</summary>
|
/// <summary>Run the daily prep prompt and let Claude fill MyDay.</summary>
|
||||||
FillMyDay,
|
FillMyDay,
|
||||||
/// <summary>Run the schedule's own prompt with the ClaudeDo MCP tools available.</summary>
|
/// <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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
|
|||||||
|
|
||||||
namespace ClaudeDo.Data;
|
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>
|
/// <summary>
|
||||||
/// How a prompt kind's on-disk override (if any) relates to the bundled default.
|
/// 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.MergeHelperInitial => "merge-helper-initial.md",
|
||||||
PromptKind.MergeHelperHandoff => "merge-helper-handoff.md",
|
PromptKind.MergeHelperHandoff => "merge-helper-handoff.md",
|
||||||
PromptKind.PowerUseAddendum => "power-use.md",
|
PromptKind.PowerUseAddendum => "power-use.md",
|
||||||
|
PromptKind.TicketImport => "ticket-import.md",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,6 +224,7 @@ public static class PromptFiles
|
|||||||
PromptKind.MergeHelperInitial => MergeHelperInitialDefault,
|
PromptKind.MergeHelperInitial => MergeHelperInitialDefault,
|
||||||
PromptKind.MergeHelperHandoff => MergeHelperHandoffDefault,
|
PromptKind.MergeHelperHandoff => MergeHelperHandoffDefault,
|
||||||
PromptKind.PowerUseAddendum => PowerUseAddendumDefault,
|
PromptKind.PowerUseAddendum => PowerUseAddendumDefault,
|
||||||
|
PromptKind.TicketImport => TicketImportDefault,
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -385,6 +387,22 @@ public static class PromptFiles
|
|||||||
(build + tests) before you stop.
|
(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 = """
|
private const string DailyPrepDefault = """
|
||||||
You are preparing my workday for {date}.
|
You are preparing my workday for {date}.
|
||||||
|
|
||||||
|
|||||||
@@ -127,4 +127,12 @@ public sealed class AppSettingsRepository
|
|||||||
row.RepoImportFolders = list.Count == 0 ? null : JsonSerializer.Serialize(list);
|
row.RepoImportFolders = list.Count == 0 ? null : JsonSerializer.Serialize(list);
|
||||||
await _context.SaveChangesAsync(ct);
|
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);
|
var found = await _repo.GetAsync(entity.Id);
|
||||||
Assert.Equal("new override", found!.PromptOverride);
|
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("handoff_list_handler", d);
|
||||||
Assert.Contains("do not continue into phase 3 yourself", d, StringComparison.OrdinalIgnoreCase);
|
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;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -60,4 +61,31 @@ public sealed class TicketFieldsTests : IDisposable
|
|||||||
Assert.Null((await read.Tasks.SingleAsync()).TicketRef);
|
Assert.Null((await read.Tasks.SingleAsync()).TicketRef);
|
||||||
Assert.Null((await read.ListConfigs.SingleAsync()).TicketProjectId);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user