Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 30 additions & 34 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<!-- Core + DI -->
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />

<!-- Test infrastructure -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />

<!-- Roslyn analyzers + testing -->
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Features" Version="4.14.0" />
<PackageVersion Include="System.Composition" Version="9.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.2" />

<!-- Security: override vulnerable transitive packages in tests -->
<PackageVersion Include="NuGet.Common" Version="6.14.0" />
<PackageVersion Include="NuGet.Packaging" Version="6.14.0" />
<PackageVersion Include="NuGet.Protocol" Version="6.14.0" />
</ItemGroup>
</Project>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Core + DI -->
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.8" />
<!-- Test infrastructure -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<!-- Roslyn analyzers + testing -->
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Features" Version="4.14.0" />
<PackageVersion Include="System.Composition" Version="9.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.2" />
<!-- Security: override vulnerable transitive packages in tests -->
<PackageVersion Include="NuGet.Common" Version="6.14.0" />
<PackageVersion Include="NuGet.Packaging" Version="6.14.0" />
<PackageVersion Include="NuGet.Protocol" Version="6.14.0" />
</ItemGroup>
</Project>
3 changes: 1 addition & 2 deletions build/packages.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
{ "id": "Coven.Chat", "path": "src/Coven.Chat" },
{ "id": "Coven.Chat.Journal", "path": "src/Coven.Chat.Journal" },
{ "id": "Coven.Chat.Adapter.Discord", "path": "src/Coven.Chat.Adapter.Discord" },
{ "id": "Coven.Analyzers", "path": "src/Coven.Analyzers" },
{ "id": "Coven.Analyzers.CodeFixes", "path": "src/Coven.Analyzers.CodeFixes" }
{ "id": "Coven.Analyzers", "path": "src/Coven.Analyzers" }
]
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<ProjectReference Include="../../../../src/Coven.Spellcasting/Coven.Spellcasting.csproj" />
<ProjectReference Include="../../../../src/Coven.Spellcasting.Agents/Coven.Spellcasting.Agents.csproj" />
<ProjectReference Include="../../../../src/Coven.Spellcasting.Agents.Codex/Coven.Spellcasting.Agents.Codex.csproj" />
<ProjectReference Include="../../../../src/Coven.Spellcasting.Agents.Validation/Coven.Spellcasting.Agents.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
</Project>

168 changes: 166 additions & 2 deletions samples/GettingStarted/01.LocalCodexCLI/src/Program.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,176 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Coven.Core;
using Coven.Core.Di;
using Coven.Spellcasting;
using Coven.Spellcasting.Agents;
using Coven.Spellcasting.Agents.Codex;
using Coven.Spellcasting.Agents.Validation;

namespace Coven.Samples.LocalCodexCLI;

// Simple runtime config for DI
internal sealed class SampleConfig
{
public string RepoRoot { get; init; } = string.Empty;
}

// Inputs + payload
internal sealed record ChangeRequest(string RepoRoot, string Goal);
internal sealed record FixSpell(string GuideMarkdown, string SpellVersion, string TestSuite, string Goal);

// Block 0: Translate from string (goal) to ChangeRequest using DI config
internal sealed class MakeChangeRequestBlock : IMagikBlock<string, ChangeRequest>
{
private readonly SampleConfig _cfg;
public MakeChangeRequestBlock(SampleConfig cfg) { _cfg = cfg; }
public Task<ChangeRequest> DoMagik(string goal)
{
var repo = string.IsNullOrWhiteSpace(_cfg.RepoRoot) ? Environment.CurrentDirectory : _cfg.RepoRoot;
return Task.FromResult(new ChangeRequest(repo, goal));
}
}

// Block 1: Build SpellContext from ChangeRequest
internal sealed class MakeContextBlock : IMagikBlock<ChangeRequest, SpellContext>
{
public Task<SpellContext> DoMagik(ChangeRequest input)
{
var ctx = new SpellContext
{
ContextUri = new Uri($"file://{Path.GetFullPath(input.RepoRoot)}"),
Permissions = AgentPermissions.AutoEdit()
};
return Task.FromResult(ctx);
}
}

// Block 3: User that invokes the agent given a SpellContext (after validation)
internal sealed class SpellUserFromContext : MagikUser<SpellContext, string>
{
private readonly ICovenAgent<FixSpell, string> _agent;
private readonly string _goal;
public SpellUserFromContext(ICovenAgent<FixSpell, string> agent, string goal)
{ _agent = agent; _goal = goal; }

protected override Task<string> InvokeAsync(
SpellContext input,
IBook<DefaultGuide> guide,
IBook<DefaultSpell> spell,
IBook<DefaultTest> test,
CancellationToken ct)
{
var payload = new FixSpell(
guide.Payload.Markdown,
spell.Payload.Version,
test.Payload.Suite,
_goal);
return _agent.CastSpellAsync(payload, input, ct);
}
}

internal static class Program
{
public static async Task<int> Main(string[] args)
{
await Task.CompletedTask;
// Arguments: --repo <path> --goal <text> [--codex <exe>]
if (args.Length == 0 || HasArg(args, "-h") || HasArg(args, "--help"))
{
PrintHelp();
return 2;
}

var repo = GetValue(args, "--repo") ?? Environment.CurrentDirectory;
var goal = GetValue(args, "--goal");
var codexPath = GetValue(args, "--codex");

if (string.IsNullOrWhiteSpace(goal))
{
Console.Error.WriteLine("error: --goal is required.\n");
PrintHelp();
return 2;
}

if (!Directory.Exists(repo))
{
Console.Error.WriteLine($"error: repo directory not found: {repo}");
return 2;
}

// Use HostApplicationBuilder for DI + configuration
var builder = Host.CreateApplicationBuilder(args);

// DI wiring: register Codex CLI agent and compose pipeline
builder.Services.AddSingleton(new SampleConfig { RepoRoot = repo });
builder.Services.AddSingleton<ICovenAgent<FixSpell, string>>(sp =>
{
string ToPrompt(FixSpell f) => $"goal={f.Goal}; version={f.SpellVersion}; suite={f.TestSuite}";
string Parse(string s) => s.Trim();
var opts = new CodexCliAgent<FixSpell, string>.Options
{
ExecutablePath = string.IsNullOrWhiteSpace(codexPath) ? "codex" : codexPath
};
return new CodexCliAgent<FixSpell, string>(ToPrompt, Parse, opts);
});
builder.Services.AddSingleton<IAgentValidation>(sp => new CodexCliValidation(new CodexCliValidation.Options
{
ExecutablePath = string.IsNullOrWhiteSpace(codexPath) ? "codex" : codexPath
}));

builder.BuildCoven(c =>
{
// string (goal) -> ChangeRequest
c.AddBlock<string, ChangeRequest, MakeChangeRequestBlock>();
// ChangeRequest -> SpellContext
c.AddBlock<ChangeRequest, SpellContext, MakeContextBlock>();
// SpellContext -> SpellContext (validation)
c.AddBlock<SpellContext, SpellContext, ValidateAgentBlock>();
// SpellContext -> string (invoke agent)
c.AddBlock<SpellContext, string>(sp =>
new SpellUserFromContext(
sp.GetRequiredService<ICovenAgent<FixSpell, string>>(),
goal!));
c.Done();
});

using var host = builder.Build();
var coven = host.Services.GetRequiredService<ICoven>();

// Start pipeline from string (goal)
var output = await coven.Ritual<string, string>(goal!);
Console.WriteLine(output);
return 0;
}

private static bool HasArg(string[] args, string key)
{
foreach (var a in args)
{
if (string.Equals(a, key, StringComparison.OrdinalIgnoreCase)) return true;
}
return false;
}

private static string? GetValue(string[] args, string key)
{
for (int i = 0; i < args.Length; i++)
{
if (string.Equals(args[i], key, StringComparison.OrdinalIgnoreCase))
{
if (i + 1 < args.Length) return args[i + 1];
return null;
}
}
return null;
}

private static void PrintHelp()
{
Console.WriteLine("Usage: 01.LocalCodexCLI --repo <path> --goal <text> [--codex <exe>]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" --repo <path> Repository root (defaults to current directory)");
Console.WriteLine(" --goal <text> High-level goal given to Codex CLI");
Console.WriteLine(" --codex <exe> Optional path to codex executable (defaults to 'codex')");
}
}
101 changes: 101 additions & 0 deletions samples/GettingStarted/01.LocalCodexCLI/test/MultiBlockFlowTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Coven.Core;
using Coven.Core.Di;
using Coven.Spellcasting;
using Coven.Spellcasting.Agents;
using Coven.Spellcasting.Agents.Validation;

namespace Coven.Samples.LocalCodexCLI.Tests;

public class MultiBlockFlowTests
{
private sealed class FakeAgent : ICovenAgent<FixSpell, string>
{
public string Id => "fake";
public Task<string> CastSpellAsync(FixSpell input, SpellContext? context = null, CancellationToken ct = default)
{
var mode = context?.Permissions?.Allows<WriteFile>() == true ? "edit" : "suggest";
var cwd = context?.ContextUri?.IsAbsoluteUri == true ? context.ContextUri.LocalPath : string.Empty;
var text = $"{input.Goal}|{input.SpellVersion}|{input.TestSuite}|{mode}|{cwd}";
return Task.FromResult(text);
}
}

private sealed class FakeValidation : IAgentValidation
{
public string AgentId => "fake";
public int Calls { get; private set; }
public Task<AgentValidationResult> ValidateAsync(SpellContext? context = null, CancellationToken ct = default)
{
Calls++;
return Task.FromResult(AgentValidationResult.Noop());
}
}

[Fact]
public async Task Translates_String_To_ChangeRequest_Using_Config()
{
var repo = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n"));
Directory.CreateDirectory(repo);

var services = new ServiceCollection();
services.AddSingleton(new SampleConfig { RepoRoot = repo });

services.BuildCoven(c =>
{
c.AddBlock<string, ChangeRequest, MakeChangeRequestBlock>();
c.Done();
});

using var sp = services.BuildServiceProvider();
var coven = sp.GetRequiredService<ICoven>();

var cr = await coven.Ritual<string, ChangeRequest>("goal-text");
Assert.Equal(Path.GetFullPath(repo).TrimEnd(Path.DirectorySeparatorChar),
Path.GetFullPath(cr.RepoRoot).TrimEnd(Path.DirectorySeparatorChar));
Assert.Equal("goal-text", cr.Goal);
}

[Fact]
public async Task Full_Pipeline_Validates_And_Invokes_Agent()
{
var repo = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n"));
Directory.CreateDirectory(repo);

var services = new ServiceCollection();
services.AddSingleton(new SampleConfig { RepoRoot = repo });
var validation = new FakeValidation();
services.AddSingleton<IAgentValidation>(validation);
services.AddSingleton<ICovenAgent<FixSpell, string>, FakeAgent>();

services.BuildCoven(c =>
{
c.AddBlock<string, ChangeRequest, MakeChangeRequestBlock>();
c.AddBlock<ChangeRequest, SpellContext, MakeContextBlock>();
c.AddBlock<SpellContext, SpellContext, ValidateAgentBlock>();
c.AddBlock<SpellContext, string>(sp =>
new SpellUserFromContext(
sp.GetRequiredService<ICovenAgent<FixSpell, string>>(),
goal: "do-the-thing"));
c.Done();
});

using var sp = services.BuildServiceProvider();
var coven = sp.GetRequiredService<ICoven>();

var output = await coven.Ritual<string, string>("ignored-goal-because-configured-in-user");

Assert.True(validation.Calls >= 1);
Assert.Contains("do-the-thing", output);
Assert.Contains("0.1", output); // default spell version
Assert.Contains("smoke", output); // default test suite
Assert.Contains("edit", output); // permission reflected
Assert.Contains(Path.GetFullPath(repo).TrimEnd(Path.DirectorySeparatorChar), output);
}
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
Expand Down
Loading