diff --git a/ClaudeDo.slnx b/ClaudeDo.slnx
new file mode 100644
index 0000000..e680914
--- /dev/null
+++ b/ClaudeDo.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/schema/schema.sql b/schema/schema.sql
new file mode 100644
index 0000000..83a09e2
--- /dev/null
+++ b/schema/schema.sql
@@ -0,0 +1,62 @@
+-- ClaudeDo SQLite schema (single source of truth, 3NF)
+-- Applied by Worker on first startup. WAL mode is set via PRAGMA after open.
+
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE IF NOT EXISTS lists (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ working_dir TEXT NULL,
+ default_commit_type TEXT NOT NULL DEFAULT 'chore'
+);
+
+CREATE TABLE IF NOT EXISTS tasks (
+ id TEXT PRIMARY KEY,
+ list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ description TEXT NULL,
+ status TEXT NOT NULL CHECK (status IN ('manual','queued','running','done','failed')),
+ scheduled_for TIMESTAMP NULL,
+ result TEXT NULL,
+ log_path TEXT NULL,
+ created_at TIMESTAMP NOT NULL,
+ started_at TIMESTAMP NULL,
+ finished_at TIMESTAMP NULL,
+ commit_type TEXT NOT NULL DEFAULT 'chore'
+);
+
+CREATE INDEX IF NOT EXISTS idx_tasks_list_id ON tasks(list_id);
+CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
+
+CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE IF NOT EXISTS list_tags (
+ list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
+ tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
+ PRIMARY KEY (list_id, tag_id)
+);
+
+CREATE TABLE IF NOT EXISTS task_tags (
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
+ tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
+ PRIMARY KEY (task_id, tag_id)
+);
+
+CREATE TABLE IF NOT EXISTS worktrees (
+ task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
+ path TEXT NOT NULL,
+ branch_name TEXT NOT NULL,
+ base_commit TEXT NOT NULL,
+ head_commit TEXT NULL,
+ diff_stat TEXT NULL,
+ state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active','merged','discarded','kept')),
+ created_at TIMESTAMP NOT NULL
+);
+
+-- Seed: minimal tag set (ignored if already present)
+INSERT OR IGNORE INTO tags (name) VALUES ('agent');
+INSERT OR IGNORE INTO tags (name) VALUES ('manual');
diff --git a/src/ClaudeDo.App/App.axaml b/src/ClaudeDo.App/App.axaml
new file mode 100644
index 0000000..c2f87da
--- /dev/null
+++ b/src/ClaudeDo.App/App.axaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ClaudeDo.App/App.axaml.cs b/src/ClaudeDo.App/App.axaml.cs
new file mode 100644
index 0000000..c655858
--- /dev/null
+++ b/src/ClaudeDo.App/App.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Data.Core;
+using Avalonia.Data.Core.Plugins;
+using System.Linq;
+using Avalonia.Markup.Xaml;
+using ClaudeDo.Ui.ViewModels;
+using ClaudeDo.Ui.Views;
+
+namespace ClaudeDo.App;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = new MainWindowViewModel(),
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/src/ClaudeDo.App/Assets/avalonia-logo.ico b/src/ClaudeDo.App/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..f7da8bb
Binary files /dev/null and b/src/ClaudeDo.App/Assets/avalonia-logo.ico differ
diff --git a/src/ClaudeDo.App/ClaudeDo.App.csproj b/src/ClaudeDo.App/ClaudeDo.App.csproj
new file mode 100644
index 0000000..852bd5f
--- /dev/null
+++ b/src/ClaudeDo.App/ClaudeDo.App.csproj
@@ -0,0 +1,30 @@
+
+
+ WinExe
+ net8.0
+ enable
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs
new file mode 100644
index 0000000..039b475
--- /dev/null
+++ b/src/ClaudeDo.App/Program.cs
@@ -0,0 +1,24 @@
+using Avalonia;
+using System;
+
+namespace ClaudeDo.App;
+
+sealed class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+#if DEBUG
+ .WithDeveloperTools()
+#endif
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/src/ClaudeDo.App/ViewLocator.cs b/src/ClaudeDo.App/ViewLocator.cs
new file mode 100644
index 0000000..86eee3c
--- /dev/null
+++ b/src/ClaudeDo.App/ViewLocator.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using ClaudeDo.Ui.ViewModels;
+
+namespace ClaudeDo.App;
+
+///
+/// Given a view model, returns the corresponding view if possible.
+///
+[RequiresUnreferencedCode(
+ "Default implementation of ViewLocator involves reflection which may be trimmed away.",
+ Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
+public class ViewLocator : IDataTemplate
+{
+ public Control? Build(object? param)
+ {
+ if (param is null)
+ return null;
+
+ var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
+ var type = typeof(ViewModelBase).Assembly.GetType(name);
+
+ if (type != null)
+ {
+ return (Control)Activator.CreateInstance(type)!;
+ }
+
+ return new TextBlock { Text = "Not Found: " + name };
+ }
+
+ public bool Match(object? data)
+ {
+ return data is ViewModelBase;
+ }
+}
diff --git a/src/ClaudeDo.App/app.manifest b/src/ClaudeDo.App/app.manifest
new file mode 100644
index 0000000..b80bdb3
--- /dev/null
+++ b/src/ClaudeDo.App/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Data/ClaudeDo.Data.csproj b/src/ClaudeDo.Data/ClaudeDo.Data.csproj
new file mode 100644
index 0000000..624930a
--- /dev/null
+++ b/src/ClaudeDo.Data/ClaudeDo.Data.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
new file mode 100644
index 0000000..fae1598
--- /dev/null
+++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
diff --git a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..f753981
--- /dev/null
+++ b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,6 @@
+namespace ClaudeDo.Ui.ViewModels;
+
+public partial class MainWindowViewModel : ViewModelBase
+{
+ public string Greeting { get; } = "Welcome to Avalonia!";
+}
diff --git a/src/ClaudeDo.Ui/ViewModels/ViewModelBase.cs b/src/ClaudeDo.Ui/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..32e48dc
--- /dev/null
+++ b/src/ClaudeDo.Ui/ViewModels/ViewModelBase.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace ClaudeDo.Ui.ViewModels;
+
+public abstract class ViewModelBase : ObservableObject
+{
+}
diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml b/src/ClaudeDo.Ui/Views/MainWindow.axaml
new file mode 100644
index 0000000..e0d243f
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs b/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..6a1e516
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ClaudeDo.Ui.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
new file mode 100644
index 0000000..4cf11ce
--- /dev/null
+++ b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs
new file mode 100644
index 0000000..1760df1
--- /dev/null
+++ b/src/ClaudeDo.Worker/Program.cs
@@ -0,0 +1,6 @@
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+app.MapGet("/", () => "Hello World!");
+
+app.Run();
diff --git a/src/ClaudeDo.Worker/Properties/launchSettings.json b/src/ClaudeDo.Worker/Properties/launchSettings.json
new file mode 100644
index 0000000..446b5e4
--- /dev/null
+++ b/src/ClaudeDo.Worker/Properties/launchSettings.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:25359",
+ "sslPort": 44337
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5095",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7295;http://localhost:5095",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/ClaudeDo.Worker/appsettings.Development.json b/src/ClaudeDo.Worker/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/ClaudeDo.Worker/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/ClaudeDo.Worker/appsettings.json b/src/ClaudeDo.Worker/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/src/ClaudeDo.Worker/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
new file mode 100644
index 0000000..21410a0
--- /dev/null
+++ b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+