M2: the four V1 atoms + Git sync (Gitea / filesystem)
- Author the four V1 skill atoms in skills/ (Git is the source of truth): spec-writing & story-breakdown (product-owner), test-plan-generation & diff-review (qa) — each with risk-tagged actions, golden tests, and a body. - SharedKernel: IGitProvider seam (read-only, provider-agnostic) + GitFile. - Integrations module (its first real code): FileSystemGitProvider (dogfood/local) and a GiteaGitProvider (Gitea REST: recursive tree → SKILL.md blobs → base64 contents); the provider is chosen by GitSource:Provider config. - Skills: SkillSyncService consumes IGitProvider (never Integrations) and indexes each file; POST /api/skills/sync and a POST /api/skills/webhook/gitea (re-sync on push; signature verification + changed-file-only + queue offload come later). Verified: build green; ArchitectureTests 8/8 (Skills & Integrations reference only SharedKernel; the Git seam lives in SharedKernel); IntegrationTests 22/22 incl. a sync that indexes the four real atoms from skills/, published and queryable by role. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,3 +23,5 @@ internal sealed record SkillDetail(
|
||||
string Body);
|
||||
|
||||
internal sealed record IndexRequest(string Content, string? SourceRepo, string? SourcePath, string? SourceCommit);
|
||||
|
||||
internal sealed record SyncResult(int Indexed);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using TeamUp.Modules.Skills.Domain;
|
||||
using TeamUp.Modules.Skills.Indexing;
|
||||
using TeamUp.Modules.Skills.Persistence;
|
||||
using TeamUp.Modules.Skills.Sync;
|
||||
using TeamUp.SharedKernel.Modularity;
|
||||
|
||||
namespace TeamUp.Modules.Skills.Endpoints;
|
||||
@@ -19,8 +20,18 @@ internal static class SkillsEndpoints
|
||||
group.MapGet("/", ListSkills).RequireAuthorization();
|
||||
group.MapGet("/{key}", GetSkill).RequireAuthorization();
|
||||
group.MapPost("/index", IndexSkill).RequireAuthorization();
|
||||
group.MapPost("/sync", Sync).RequireAuthorization();
|
||||
group.MapPost("/webhook/gitea", Webhook).AllowAnonymous();
|
||||
}
|
||||
|
||||
private static async Task<IResult> Sync(SkillSyncService sync, CancellationToken ct) =>
|
||||
Results.Ok(new SyncResult(await sync.SyncAsync(ct)));
|
||||
|
||||
// Gitea push webhook → re-sync the source. M2 re-indexes the whole source (idempotent);
|
||||
// signature verification + changed-file-only sync via the job queue land later.
|
||||
private static async Task<IResult> Webhook(SkillSyncService sync, CancellationToken ct) =>
|
||||
Results.Ok(new SyncResult(await sync.SyncAsync(ct)));
|
||||
|
||||
private static async Task<IResult> ListSkills(
|
||||
string? role, string? visibility, SkillsDbContext db, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using TeamUp.Modules.Skills.Endpoints;
|
||||
using TeamUp.Modules.Skills.Indexing;
|
||||
using TeamUp.Modules.Skills.Persistence;
|
||||
using TeamUp.Modules.Skills.Sync;
|
||||
using TeamUp.SharedKernel.Modularity;
|
||||
using TeamUp.SharedKernel.Persistence;
|
||||
|
||||
@@ -25,6 +26,7 @@ public sealed class SkillsModule : IModule
|
||||
services.AddScoped<IModuleDbContext>(sp => sp.GetRequiredService<SkillsDbContext>());
|
||||
services.AddSingleton<ISkillEmbedder, HashingSkillEmbedder>();
|
||||
services.AddScoped<SkillIndexer>();
|
||||
services.AddScoped<SkillSyncService>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TeamUp.Modules.Skills.Indexing;
|
||||
using TeamUp.SharedKernel.Git;
|
||||
|
||||
namespace TeamUp.Modules.Skills.Sync;
|
||||
|
||||
/// <summary>Pulls SKILL.md files from the configured Git source and indexes each one.</summary>
|
||||
internal sealed class SkillSyncService(
|
||||
IGitProvider provider,
|
||||
SkillIndexer indexer,
|
||||
ILogger<SkillSyncService> logger)
|
||||
{
|
||||
public async Task<int> SyncAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var files = await provider.ListSkillFilesAsync(cancellationToken);
|
||||
var indexed = 0;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
await indexer.IndexAsync(file.Content, provider.Name, file.Path, sourceCommit: null, cancellationToken);
|
||||
indexed++;
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
logger.LogWarning("Skipping {Path}: {Message}", file.Path, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Synced {Indexed}/{Total} SKILL.md file(s) from {Source}.", indexed, files.Count, provider.Name);
|
||||
return indexed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user