@kodrunhq/opencode-autopilot 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/commands/oc-review-agents.md +103 -0
- package/assets/skills/coding-standards/SKILL.md +313 -0
- package/assets/skills/csharp-patterns/SKILL.md +327 -0
- package/assets/skills/frontend-design/SKILL.md +433 -0
- package/assets/skills/java-patterns/SKILL.md +258 -0
- package/assets/templates/cli-tool.md +49 -0
- package/assets/templates/fullstack.md +71 -0
- package/assets/templates/library.md +49 -0
- package/assets/templates/web-api.md +60 -0
- package/package.json +1 -1
- package/src/agents/debugger.ts +329 -0
- package/src/agents/index.ts +13 -4
- package/src/agents/metaprompter.ts +1 -1
- package/src/agents/planner.ts +563 -0
- package/src/agents/researcher.ts +1 -1
- package/src/agents/reviewer.ts +270 -0
- package/src/installer.ts +11 -3
- package/src/registry/model-groups.ts +4 -1
- package/src/review/stack-gate.ts +2 -0
- package/src/skills/adaptive-injector.ts +47 -2
- package/src/tools/stocktake.ts +64 -9
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: csharp-patterns
|
|
3
|
+
description: Idiomatic C# patterns including records, .NET DI, Entity Framework, and async best practices
|
|
4
|
+
stacks:
|
|
5
|
+
- csharp
|
|
6
|
+
requires:
|
|
7
|
+
- coding-standards
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# C# Patterns
|
|
11
|
+
|
|
12
|
+
Idiomatic C# patterns for modern (.NET 8+) projects. Covers language features, .NET dependency injection, Entity Framework conventions, async best practices, and common pitfalls. Apply these when writing, reviewing, or refactoring C# code.
|
|
13
|
+
|
|
14
|
+
## 1. Modern C# Idioms
|
|
15
|
+
|
|
16
|
+
**DO:** Use modern C# features to write concise, null-safe, and expressive code.
|
|
17
|
+
|
|
18
|
+
- Use records for immutable data:
|
|
19
|
+
```csharp
|
|
20
|
+
// Record: immutable, value equality, ToString auto-generated
|
|
21
|
+
public record UserDto(string Name, string Email, DateTimeOffset CreatedAt);
|
|
22
|
+
|
|
23
|
+
// With expression for non-destructive mutation
|
|
24
|
+
var updated = original with { Email = "new@example.com" };
|
|
25
|
+
```
|
|
26
|
+
- Use pattern matching with switch expressions:
|
|
27
|
+
```csharp
|
|
28
|
+
// DO: Switch expression with property patterns
|
|
29
|
+
string GetStatusMessage(Order order) => order switch
|
|
30
|
+
{
|
|
31
|
+
{ Status: OrderStatus.Pending, Total: > 1000 } => "Large order pending approval",
|
|
32
|
+
{ Status: OrderStatus.Pending } => "Awaiting processing",
|
|
33
|
+
{ Status: OrderStatus.Shipped } => $"Shipped on {order.ShippedDate}",
|
|
34
|
+
{ Status: OrderStatus.Delivered } => "Delivered",
|
|
35
|
+
_ => "Unknown status"
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
- Enable nullable reference types and annotate nullability:
|
|
39
|
+
```csharp
|
|
40
|
+
// In .csproj: <Nullable>enable</Nullable>
|
|
41
|
+
|
|
42
|
+
// Explicit nullability
|
|
43
|
+
public string GetDisplayName(User? user)
|
|
44
|
+
{
|
|
45
|
+
return user?.Name ?? "Anonymous";
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
- Use `init`-only properties for immutable object initialization:
|
|
49
|
+
```csharp
|
|
50
|
+
public class Config
|
|
51
|
+
{
|
|
52
|
+
public required string ConnectionString { get; init; }
|
|
53
|
+
public int MaxRetries { get; init; } = 3;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var config = new Config { ConnectionString = "Server=..." };
|
|
57
|
+
// config.ConnectionString = "other"; // Compile error
|
|
58
|
+
```
|
|
59
|
+
- Use file-scoped namespaces to reduce nesting:
|
|
60
|
+
```csharp
|
|
61
|
+
// DO: File-scoped namespace (one less indentation level)
|
|
62
|
+
namespace MyApp.Services;
|
|
63
|
+
|
|
64
|
+
public class OrderService { ... }
|
|
65
|
+
```
|
|
66
|
+
- Use raw string literals for multi-line strings:
|
|
67
|
+
```csharp
|
|
68
|
+
var query = """
|
|
69
|
+
SELECT u.Name, u.Email
|
|
70
|
+
FROM Users u
|
|
71
|
+
WHERE u.IsActive = 1
|
|
72
|
+
ORDER BY u.Name
|
|
73
|
+
""";
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**DON'T:**
|
|
77
|
+
|
|
78
|
+
- Ignore nullable warnings -- they prevent `NullReferenceException` at runtime
|
|
79
|
+
- Use `class` when `record` better represents the data (DTOs, value objects, events)
|
|
80
|
+
- Use verbose `if/else if` chains when switch expressions are clearer
|
|
81
|
+
- Use `string.Format()` when string interpolation (`$"Hello {name}"`) is available
|
|
82
|
+
|
|
83
|
+
## 2. .NET Patterns
|
|
84
|
+
|
|
85
|
+
**DO:** Follow .NET conventions for dependency injection, configuration, and middleware.
|
|
86
|
+
|
|
87
|
+
- Register services with the built-in DI container:
|
|
88
|
+
```csharp
|
|
89
|
+
// Program.cs
|
|
90
|
+
builder.Services.AddScoped<IOrderService, OrderService>();
|
|
91
|
+
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
|
|
92
|
+
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
|
|
93
|
+
```
|
|
94
|
+
- Use `IOptions<T>` for strongly-typed configuration:
|
|
95
|
+
```csharp
|
|
96
|
+
public record EmailOptions
|
|
97
|
+
{
|
|
98
|
+
public required string Host { get; init; }
|
|
99
|
+
public int Port { get; init; } = 587;
|
|
100
|
+
public bool UseTls { get; init; } = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Registration
|
|
104
|
+
builder.Services.Configure<EmailOptions>(builder.Configuration.GetSection("Email"));
|
|
105
|
+
|
|
106
|
+
// Injection
|
|
107
|
+
public class EmailSender(IOptions<EmailOptions> options)
|
|
108
|
+
{
|
|
109
|
+
private readonly EmailOptions _options = options.Value;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
- Order middleware carefully -- order matters in the pipeline:
|
|
113
|
+
```csharp
|
|
114
|
+
app.UseExceptionHandler("/error"); // First: catch everything
|
|
115
|
+
app.UseHttpsRedirection();
|
|
116
|
+
app.UseAuthentication(); // Before authorization
|
|
117
|
+
app.UseAuthorization(); // After authentication
|
|
118
|
+
app.UseRateLimiter();
|
|
119
|
+
app.MapControllers(); // Last: route to handlers
|
|
120
|
+
```
|
|
121
|
+
- Use `IHostedService` for background work:
|
|
122
|
+
```csharp
|
|
123
|
+
public class CleanupService : BackgroundService
|
|
124
|
+
{
|
|
125
|
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
126
|
+
{
|
|
127
|
+
while (!stoppingToken.IsCancellationRequested)
|
|
128
|
+
{
|
|
129
|
+
await CleanupExpiredSessions();
|
|
130
|
+
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
- Use `ILogger<T>` for structured logging:
|
|
136
|
+
```csharp
|
|
137
|
+
public class OrderService(ILogger<OrderService> logger)
|
|
138
|
+
{
|
|
139
|
+
public void PlaceOrder(Order order)
|
|
140
|
+
{
|
|
141
|
+
logger.LogInformation("Placing order {OrderId} for {CustomerId}",
|
|
142
|
+
order.Id, order.CustomerId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**DON'T:**
|
|
148
|
+
|
|
149
|
+
- Use `new` to create services inside other services -- always inject via constructor
|
|
150
|
+
- Register scoped services as singletons -- scoped dependencies in singletons cause captive dependency bugs
|
|
151
|
+
- Put business logic in middleware -- middleware is for cross-cutting concerns (auth, logging, CORS)
|
|
152
|
+
- Use `IServiceProvider` directly (service locator pattern) -- inject specific interfaces instead
|
|
153
|
+
|
|
154
|
+
## 3. Entity Framework Conventions
|
|
155
|
+
|
|
156
|
+
**DO:** Use EF Core idiomatically with code-first migrations and proper lifetime management.
|
|
157
|
+
|
|
158
|
+
- Use code-first migrations for schema management:
|
|
159
|
+
```bash
|
|
160
|
+
dotnet ef migrations add AddOrderTable
|
|
161
|
+
dotnet ef database update
|
|
162
|
+
```
|
|
163
|
+
- Register `DbContext` as scoped (default and correct):
|
|
164
|
+
```csharp
|
|
165
|
+
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
166
|
+
options.UseNpgsql(connectionString));
|
|
167
|
+
```
|
|
168
|
+
- Use navigation properties with explicit loading strategies:
|
|
169
|
+
```csharp
|
|
170
|
+
// DO: Explicit includes for read paths
|
|
171
|
+
var orders = await context.Orders
|
|
172
|
+
.Include(o => o.Items)
|
|
173
|
+
.Include(o => o.Customer)
|
|
174
|
+
.Where(o => o.Status == OrderStatus.Pending)
|
|
175
|
+
.ToListAsync();
|
|
176
|
+
```
|
|
177
|
+
- Use global query filters for soft delete:
|
|
178
|
+
```csharp
|
|
179
|
+
// In DbContext.OnModelCreating
|
|
180
|
+
modelBuilder.Entity<Order>()
|
|
181
|
+
.HasQueryFilter(o => !o.IsDeleted);
|
|
182
|
+
|
|
183
|
+
// To include deleted: context.Orders.IgnoreQueryFilters()
|
|
184
|
+
```
|
|
185
|
+
- Use `AsNoTracking()` for read-only queries (better performance):
|
|
186
|
+
```csharp
|
|
187
|
+
var reports = await context.Orders
|
|
188
|
+
.AsNoTracking()
|
|
189
|
+
.Select(o => new OrderSummary(o.Id, o.Total))
|
|
190
|
+
.ToListAsync();
|
|
191
|
+
```
|
|
192
|
+
- Use value converters for custom types:
|
|
193
|
+
```csharp
|
|
194
|
+
modelBuilder.Entity<Order>()
|
|
195
|
+
.Property(o => o.Currency)
|
|
196
|
+
.HasConversion(
|
|
197
|
+
v => v.Code, // To database
|
|
198
|
+
v => Currency.FromCode(v)); // From database
|
|
199
|
+
```
|
|
200
|
+
- Use owned entities for value objects:
|
|
201
|
+
```csharp
|
|
202
|
+
modelBuilder.Entity<Order>().OwnsOne(o => o.ShippingAddress);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**DON'T:**
|
|
206
|
+
|
|
207
|
+
- Return `IQueryable<T>` from repositories -- materialize queries before returning to prevent unintended SQL generation outside the repository
|
|
208
|
+
- Use `DbContext` as a singleton -- it is not thread-safe, always use scoped lifetime
|
|
209
|
+
- Skip migrations and modify the database manually -- migrations are the source of truth
|
|
210
|
+
- Load entire entity graphs when you only need a few fields -- use `Select` projections
|
|
211
|
+
- Forget to call `SaveChangesAsync()` -- EF tracks changes but does not persist until you call save
|
|
212
|
+
|
|
213
|
+
## 4. Async Best Practices
|
|
214
|
+
|
|
215
|
+
**DO:** Use `async`/`await` consistently and propagate cancellation tokens.
|
|
216
|
+
|
|
217
|
+
- Go async all the way -- never block on async code:
|
|
218
|
+
```csharp
|
|
219
|
+
// DO: Async all the way
|
|
220
|
+
public async Task<Order> GetOrderAsync(Guid id, CancellationToken ct)
|
|
221
|
+
{
|
|
222
|
+
var order = await _repository.FindByIdAsync(id, ct);
|
|
223
|
+
return order ?? throw new OrderNotFoundException(id);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
- Use `ConfigureAwait(false)` in library code:
|
|
227
|
+
```csharp
|
|
228
|
+
// Library code: no need to capture synchronization context
|
|
229
|
+
public async Task<byte[]> DownloadAsync(string url, CancellationToken ct)
|
|
230
|
+
{
|
|
231
|
+
var response = await _client.GetAsync(url, ct).ConfigureAwait(false);
|
|
232
|
+
return await response.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
- Propagate `CancellationToken` through all async methods:
|
|
236
|
+
```csharp
|
|
237
|
+
// DO: Accept and pass CancellationToken
|
|
238
|
+
public async Task ProcessAsync(CancellationToken ct = default)
|
|
239
|
+
{
|
|
240
|
+
await StepOneAsync(ct);
|
|
241
|
+
await StepTwoAsync(ct);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
- Use `ValueTask` for hot paths that often complete synchronously:
|
|
245
|
+
```csharp
|
|
246
|
+
public ValueTask<CacheEntry?> GetCachedAsync(string key)
|
|
247
|
+
{
|
|
248
|
+
if (_memoryCache.TryGetValue(key, out var entry))
|
|
249
|
+
return ValueTask.FromResult<CacheEntry?>(entry); // No allocation
|
|
250
|
+
|
|
251
|
+
return new ValueTask<CacheEntry?>(FetchFromRemoteCacheAsync(key));
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
- Use `IAsyncEnumerable<T>` for streaming results:
|
|
255
|
+
```csharp
|
|
256
|
+
public async IAsyncEnumerable<Order> StreamOrdersAsync(
|
|
257
|
+
[EnumeratorCancellation] CancellationToken ct = default)
|
|
258
|
+
{
|
|
259
|
+
await foreach (var order in context.Orders.AsAsyncEnumerable().WithCancellation(ct))
|
|
260
|
+
{
|
|
261
|
+
yield return order;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**DON'T:**
|
|
267
|
+
|
|
268
|
+
- Use `.Result` or `.Wait()` on async methods -- this causes deadlocks in ASP.NET:
|
|
269
|
+
```csharp
|
|
270
|
+
// DEADLOCK: synchronously blocking on async
|
|
271
|
+
var result = GetOrderAsync(id).Result; // NEVER do this
|
|
272
|
+
```
|
|
273
|
+
- Use `async void` except for event handlers -- `async void` swallows exceptions
|
|
274
|
+
- Forget `CancellationToken` -- without it, cancelled HTTP requests keep executing server-side
|
|
275
|
+
- Use `Task.Run()` to wrap synchronous code and pretend it's async -- that just moves work to the thread pool without making it non-blocking
|
|
276
|
+
|
|
277
|
+
## 5. Common Pitfalls
|
|
278
|
+
|
|
279
|
+
**Pitfall: Disposal Patterns**
|
|
280
|
+
Always dispose resources. Use `using` declarations for deterministic cleanup:
|
|
281
|
+
```csharp
|
|
282
|
+
// DO: using declaration (disposes at end of scope)
|
|
283
|
+
await using var connection = new SqlConnection(connectionString);
|
|
284
|
+
await connection.OpenAsync(ct);
|
|
285
|
+
|
|
286
|
+
// For classes: implement IAsyncDisposable
|
|
287
|
+
public class ResourceManager : IAsyncDisposable
|
|
288
|
+
{
|
|
289
|
+
public async ValueTask DisposeAsync()
|
|
290
|
+
{
|
|
291
|
+
await ReleaseResourcesAsync();
|
|
292
|
+
GC.SuppressFinalize(this);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Pitfall: Captured Loop Variables in Closures**
|
|
298
|
+
In older C# (before foreach fix in C# 5), the loop variable was captured by reference. While fixed for `foreach`, be cautious with `for` loops:
|
|
299
|
+
```csharp
|
|
300
|
+
// CAUTION with for loops
|
|
301
|
+
for (int i = 0; i < 10; i++)
|
|
302
|
+
{
|
|
303
|
+
var captured = i; // Capture a copy
|
|
304
|
+
tasks.Add(Task.Run(() => Process(captured)));
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Pitfall: String Concatenation in Loops**
|
|
309
|
+
Strings are immutable in C#. Concatenation in loops creates N intermediate strings. Use `StringBuilder`:
|
|
310
|
+
```csharp
|
|
311
|
+
// DO
|
|
312
|
+
var sb = new StringBuilder();
|
|
313
|
+
foreach (var line in lines)
|
|
314
|
+
sb.AppendLine(line);
|
|
315
|
+
return sb.ToString();
|
|
316
|
+
|
|
317
|
+
// DON'T
|
|
318
|
+
var result = "";
|
|
319
|
+
foreach (var line in lines)
|
|
320
|
+
result += line + "\n"; // N allocations
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Pitfall: Deadlocks from Mixing Sync/Async**
|
|
324
|
+
Calling `.Result` or `.Wait()` on a `Task` in code that has a synchronization context (ASP.NET, WPF) causes deadlocks. The async continuation needs the context, but `.Result` is blocking it. Solution: go async all the way up.
|
|
325
|
+
|
|
326
|
+
**Pitfall: Service Locator Anti-Pattern**
|
|
327
|
+
Injecting `IServiceProvider` and resolving services manually defeats the purpose of DI. Dependencies become hidden and untestable. Inject specific interfaces instead.
|