← All source files

V16 / 6df58cd3ed86

KanbanOperations.cs

Lilith/Core/Kanban/KanbanOperations.cs

Changed in this version
CS
using Lilith.Core.Commands;
using Lilith.Core.Projects;

namespace Lilith.Core.Kanban;

internal static class KanbanOperations
{
    internal static readonly string[] Names = ["kanban_prepare_operation", "kanban_status_operation"];
    internal static Task<string> ExecuteAsync(ProviderClient provider, string name, IReadOnlyDictionary<string, string> inputs, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        var session = provider.Kanban ?? throw new InvalidOperationException("Kanban is not configured.");
        string Get(string key) => inputs.GetValueOrDefault(key, "");
        var result = name switch
        {
            "kanban_prepare_operation" => session.Prepare(Get("name"), Get("kind"), Get("prd"), Get("architecture"),
                int.TryParse(Get("repair_limit"), out int limit) ? limit : throw new ArgumentException("repair_limit must be an integer.")),
            "kanban_status_operation" => session.Store.Get(Get("id")),
            _ => throw new ArgumentException("Unknown Kanban operation.")
        };
        return Task.FromResult(ProjectJson.Serialize(new { result.Id, result.State, result.Message, board = session.Dashboard.PublicUrl,
            viewer = "Use the host /kanban command to open the authenticated viewer. Its control token is not supplied to model calls.",
            command = result.State == "waiting_plan" ? $"/kanban-approve {result.Id} {result.Revision}" : $"/kanban-status {result.Id}" }));
    }
}

public static class KanbanCommands
{
    public static void Register(ChatCommandRegistry commands, Func<ProviderClient> provider)
    {
        KanbanSession Session() => provider().Kanban ?? throw new InvalidOperationException("Kanban unavailable.");
        commands.Register("kanban", "Show the local board dashboard and saved runs.", c => { c.RequireNoArguments(); return Session().Dashboard.Url + "\n" + ProjectJson.Serialize(Session().Store.List().Select(b => new { b.Id, b.Name, b.State, b.Message })); });
        commands.Register("kanban-status", "Inspect one board and all task details.", c => System.Text.Json.JsonSerializer.Serialize(Session().Store.Get(c.Arguments.Trim()), KanbanStore.Json));
        commands.Register("kanban-pause", "Pause before the next subagent; return to chat when paused.", c => Session().Pause(c.Arguments.Trim()));
        commands.Register("kanban-resume", "Explicitly resume one approved board; never grants code execution.", c => Session().Resume(c.Arguments.Trim()));
        commands.RegisterAsync("kanban-cancel", "Cancel a run while retaining private drafts and evidence.", async c => new(true, await Session().CancelAsync(c.Arguments.Trim(),c.CancellationToken)));
        commands.Register("kanban-approve", "Approve the exact reviewed board: ID REVISION.", c =>
        {
            var args = c.Arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries);
            if (args.Length != 2 || !long.TryParse(args[1], out long revision)) throw new ArgumentException("Use /kanban-approve ID REVISION.");
            return Session().Approve(args[0], revision);
        });
        commands.RegisterAsync("kanban-test", "Explicit execution decision for this board's project; up to ten runtime rounds.", async c => new(true, await provider().TestKanbanAsync(c.Arguments.Trim(), c.CancellationToken)));
        commands.RegisterAsync("kanban-publish", "Prepare separate exact publication for this qualified board.", async c => new(true, await provider().PublishKanbanAsync(c.Arguments.Trim(), c.CancellationToken)));
        commands.Register("kanban-publication-review", "Read the complete exact publication proposal for a board.", c => Session().ReadPublication(c.Arguments.Trim()));
        commands.RegisterAsync("kanban-publication-approve", "User approval of a board's already prepared exact workspace proposal.", async c => new(true, await provider().ApproveKanbanPublicationAsync(c.Arguments.Trim(), c.CancellationToken)));
        commands.RegisterAsync("kanban-wait", "Wait for the current worker to pause or reach its next host decision; no permissions granted.", async c =>
        { c.RequireNoArguments(); while (Session().IsWorking) await Task.Delay(200, c.CancellationToken); return new(true, "Kanban reached a host decision or stopped."); });
        KanbanBoard Pending(string state)
        {
            var boards = Session().Store.List().Where(b => b.State == state).ToArray();
            return boards.Length == 1 ? boards[0] : throw new InvalidOperationException("Expected exactly one " + state + " board. Use an explicit ID.");
        }
        commands.Register("kanban-approve-pending", "Scripted user decision: approve the single pending source plan, not code execution.", c => { c.RequireNoArguments(); var b = Pending("waiting_plan"); return Session().Approve(b.Id,b.Revision); });
        commands.RegisterAsync("kanban-test-pending", "Explicit scripted user execution decision for the single pending Kanban project.", async c => { c.RequireNoArguments(); return new(true, await provider().TestKanbanAsync(Pending("waiting_test").Id,c.CancellationToken)); });
        commands.RegisterAsync("kanban-publish-pending", "Prepare exact publication for the single qualified board; does not approve workspace writes.", async c => { c.RequireNoArguments(); return new(true, await provider().PublishKanbanAsync(Pending("waiting_publication").Id,c.CancellationToken)); });
        commands.RegisterAsync("kanban-publication-approve-pending", "Scripted user decision approving the single prepared exact board publication.", async c => { c.RequireNoArguments(); return new(true, await provider().ApproveKanbanPublicationAsync(Pending("waiting_publication").Id,c.CancellationToken)); });
    }
}