old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions

This commit is contained in:
master
2026-01-15 18:37:59 +02:00
parent c631bacee2
commit 88a85cdd92
208 changed files with 32271 additions and 2287 deletions

View File

@@ -44,6 +44,13 @@ public static class UnknownsCommandGroup
unknownsCommand.Add(BuildResolveCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildBudgetCommand(services, verboseOption, cancellationToken));
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001, CLI-UNK-002, CLI-UNK-003)
unknownsCommand.Add(BuildSummaryCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildShowCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildProofCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildExportCommand(services, verboseOption, cancellationToken));
unknownsCommand.Add(BuildTriageCommand(services, verboseOption, cancellationToken));
return unknownsCommand;
}
@@ -274,6 +281,194 @@ public static class UnknownsCommandGroup
return escalateCommand;
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
private static Command BuildSummaryCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: table, json"
};
formatOption.SetDefaultValue("table");
var summaryCommand = new Command("summary", "Show unknowns summary by band with counts and fingerprints");
summaryCommand.Add(formatOption);
summaryCommand.Add(verboseOption);
summaryCommand.SetAction(async (parseResult, ct) =>
{
var format = parseResult.GetValue(formatOption) ?? "table";
var verbose = parseResult.GetValue(verboseOption);
return await HandleSummaryAsync(services, format, verbose, cancellationToken);
});
return summaryCommand;
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
private static Command BuildShowCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var idOption = new Option<string>("--id", new[] { "-i" })
{
Description = "Unknown ID to show details for",
Required = true
};
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: table, json"
};
formatOption.SetDefaultValue("table");
var showCommand = new Command("show", "Show detailed unknown info including fingerprint, triggers, and next actions");
showCommand.Add(idOption);
showCommand.Add(formatOption);
showCommand.Add(verboseOption);
showCommand.SetAction(async (parseResult, ct) =>
{
var id = parseResult.GetValue(idOption) ?? string.Empty;
var format = parseResult.GetValue(formatOption) ?? "table";
var verbose = parseResult.GetValue(verboseOption);
return await HandleShowAsync(services, id, format, verbose, cancellationToken);
});
return showCommand;
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
private static Command BuildProofCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var idOption = new Option<string>("--id", new[] { "-i" })
{
Description = "Unknown ID to get proof for",
Required = true
};
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: json, envelope"
};
formatOption.SetDefaultValue("json");
var proofCommand = new Command("proof", "Get evidence proof for an unknown (fingerprint, triggers, evidence refs)");
proofCommand.Add(idOption);
proofCommand.Add(formatOption);
proofCommand.Add(verboseOption);
proofCommand.SetAction(async (parseResult, ct) =>
{
var id = parseResult.GetValue(idOption) ?? string.Empty;
var format = parseResult.GetValue(formatOption) ?? "json";
var verbose = parseResult.GetValue(verboseOption);
return await HandleProofAsync(services, id, format, verbose, cancellationToken);
});
return proofCommand;
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
private static Command BuildExportCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var bandOption = new Option<string?>("--band", new[] { "-b" })
{
Description = "Filter by band: HOT, WARM, COLD, all"
};
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: json, csv, ndjson"
};
formatOption.SetDefaultValue("json");
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output file path (default: stdout)"
};
var exportCommand = new Command("export", "Export unknowns with fingerprints and triggers for offline analysis");
exportCommand.Add(bandOption);
exportCommand.Add(formatOption);
exportCommand.Add(outputOption);
exportCommand.Add(verboseOption);
exportCommand.SetAction(async (parseResult, ct) =>
{
var band = parseResult.GetValue(bandOption);
var format = parseResult.GetValue(formatOption) ?? "json";
var output = parseResult.GetValue(outputOption);
var verbose = parseResult.GetValue(verboseOption);
return await HandleExportAsync(services, band, format, output, verbose, cancellationToken);
});
return exportCommand;
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-003)
private static Command BuildTriageCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var idOption = new Option<string>("--id", new[] { "-i" })
{
Description = "Unknown ID to triage",
Required = true
};
var actionOption = new Option<string>("--action", new[] { "-a" })
{
Description = "Triage action: accept-risk, require-fix, defer, escalate, dispute",
Required = true
};
var reasonOption = new Option<string>("--reason", new[] { "-r" })
{
Description = "Reason for triage decision",
Required = true
};
var durationOption = new Option<int?>("--duration-days", new[] { "-d" })
{
Description = "Duration in days for defer/accept-risk actions"
};
var triageCommand = new Command("triage", "Apply manual triage decision to an unknown (grey queue adjudication)");
triageCommand.Add(idOption);
triageCommand.Add(actionOption);
triageCommand.Add(reasonOption);
triageCommand.Add(durationOption);
triageCommand.Add(verboseOption);
triageCommand.SetAction(async (parseResult, ct) =>
{
var id = parseResult.GetValue(idOption) ?? string.Empty;
var action = parseResult.GetValue(actionOption) ?? string.Empty;
var reason = parseResult.GetValue(reasonOption) ?? string.Empty;
var duration = parseResult.GetValue(durationOption);
var verbose = parseResult.GetValue(verboseOption);
return await HandleTriageAsync(services, id, action, reason, duration, verbose, cancellationToken);
});
return triageCommand;
}
private static Command BuildResolveCommand(
IServiceProvider services,
Option<bool> verboseOption,
@@ -558,6 +753,452 @@ public static class UnknownsCommandGroup
}
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
private static async Task<int> HandleSummaryAsync(
IServiceProvider services,
string format,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is null)
{
logger?.LogError("HTTP client factory not available");
return 1;
}
try
{
if (verbose)
{
logger?.LogDebug("Fetching unknowns summary");
}
var client = httpClientFactory.CreateClient("PolicyApi");
var response = await client.GetAsync("/api/v1/policy/unknowns/summary", ct);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Error: Failed to fetch summary ({response.StatusCode})");
return 1;
}
var summary = await response.Content.ReadFromJsonAsync<UnknownsSummaryResponse>(JsonOptions, ct);
if (summary is null)
{
Console.WriteLine("Error: Empty response from server");
return 1;
}
if (format == "json")
{
Console.WriteLine(JsonSerializer.Serialize(summary, JsonOptions));
}
else
{
Console.WriteLine("Unknowns Summary");
Console.WriteLine("================");
Console.WriteLine($" HOT: {summary.Hot,6}");
Console.WriteLine($" WARM: {summary.Warm,6}");
Console.WriteLine($" COLD: {summary.Cold,6}");
Console.WriteLine($" Resolved: {summary.Resolved,6}");
Console.WriteLine($" ----------------");
Console.WriteLine($" Total: {summary.Total,6}");
}
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Summary failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001)
private static async Task<int> HandleShowAsync(
IServiceProvider services,
string id,
string format,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is null)
{
logger?.LogError("HTTP client factory not available");
return 1;
}
try
{
if (verbose)
{
logger?.LogDebug("Fetching unknown {Id}", id);
}
var client = httpClientFactory.CreateClient("PolicyApi");
var response = await client.GetAsync($"/api/v1/policy/unknowns/{id}", ct);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Error: Unknown not found ({response.StatusCode})");
return 1;
}
var result = await response.Content.ReadFromJsonAsync<UnknownDetailResponse>(JsonOptions, ct);
if (result?.Unknown is null)
{
Console.WriteLine("Error: Empty response from server");
return 1;
}
var unknown = result.Unknown;
if (format == "json")
{
Console.WriteLine(JsonSerializer.Serialize(unknown, JsonOptions));
}
else
{
Console.WriteLine($"Unknown: {unknown.Id}");
Console.WriteLine(new string('=', 60));
Console.WriteLine($" Package: {unknown.PackageId}@{unknown.PackageVersion}");
Console.WriteLine($" Band: {unknown.Band}");
Console.WriteLine($" Score: {unknown.Score:F2}");
Console.WriteLine($" Reason: {unknown.ReasonCode} ({unknown.ReasonCodeShort})");
Console.WriteLine($" First Seen: {unknown.FirstSeenAt:u}");
Console.WriteLine($" Last Evaluated: {unknown.LastEvaluatedAt:u}");
if (!string.IsNullOrEmpty(unknown.FingerprintId))
{
Console.WriteLine();
Console.WriteLine("Fingerprint");
Console.WriteLine($" ID: {unknown.FingerprintId}");
}
if (unknown.Triggers?.Count > 0)
{
Console.WriteLine();
Console.WriteLine("Triggers");
foreach (var trigger in unknown.Triggers)
{
Console.WriteLine($" - {trigger.EventType}@{trigger.EventVersion} ({trigger.ReceivedAt:u})");
}
}
if (unknown.NextActions?.Count > 0)
{
Console.WriteLine();
Console.WriteLine("Next Actions");
foreach (var action in unknown.NextActions)
{
Console.WriteLine($" - {action}");
}
}
if (unknown.ConflictInfo?.HasConflict == true)
{
Console.WriteLine();
Console.WriteLine("Conflicts");
Console.WriteLine($" Severity: {unknown.ConflictInfo.Severity:F2}");
Console.WriteLine($" Suggested Path: {unknown.ConflictInfo.SuggestedPath}");
foreach (var conflict in unknown.ConflictInfo.Conflicts)
{
Console.WriteLine($" - {conflict.Type}: {conflict.Signal1} vs {conflict.Signal2}");
}
}
if (!string.IsNullOrEmpty(unknown.RemediationHint))
{
Console.WriteLine();
Console.WriteLine($"Hint: {unknown.RemediationHint}");
}
}
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Show failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
private static async Task<int> HandleProofAsync(
IServiceProvider services,
string id,
string format,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is null)
{
logger?.LogError("HTTP client factory not available");
return 1;
}
try
{
if (verbose)
{
logger?.LogDebug("Fetching proof for unknown {Id}", id);
}
var client = httpClientFactory.CreateClient("PolicyApi");
var response = await client.GetAsync($"/api/v1/policy/unknowns/{id}", ct);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Error: Unknown not found ({response.StatusCode})");
return 1;
}
var result = await response.Content.ReadFromJsonAsync<UnknownDetailResponse>(JsonOptions, ct);
if (result?.Unknown is null)
{
Console.WriteLine("Error: Empty response from server");
return 1;
}
var unknown = result.Unknown;
// Build proof object with deterministic ordering
var proof = new UnknownProof
{
Id = unknown.Id,
FingerprintId = unknown.FingerprintId,
PackageId = unknown.PackageId,
PackageVersion = unknown.PackageVersion,
Band = unknown.Band,
Score = unknown.Score,
ReasonCode = unknown.ReasonCode,
Triggers = unknown.Triggers?.OrderBy(t => t.ReceivedAt).ToList() ?? [],
EvidenceRefs = unknown.EvidenceRefs?.OrderBy(e => e.Type).ThenBy(e => e.Uri).ToList() ?? [],
ObservationState = unknown.ObservationState,
ConflictInfo = unknown.ConflictInfo
};
Console.WriteLine(JsonSerializer.Serialize(proof, JsonOptions));
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Proof failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-002)
private static async Task<int> HandleExportAsync(
IServiceProvider services,
string? band,
string format,
string? outputPath,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is null)
{
logger?.LogError("HTTP client factory not available");
return 1;
}
try
{
if (verbose)
{
logger?.LogDebug("Exporting unknowns: band={Band}, format={Format}", band ?? "all", format);
}
var client = httpClientFactory.CreateClient("PolicyApi");
var url = string.IsNullOrEmpty(band) || band == "all"
? "/api/v1/policy/unknowns?limit=10000"
: $"/api/v1/policy/unknowns?band={band}&limit=10000";
var response = await client.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Error: Failed to fetch unknowns ({response.StatusCode})");
return 1;
}
var result = await response.Content.ReadFromJsonAsync<UnknownsListResponse>(JsonOptions, ct);
if (result?.Items is null)
{
Console.WriteLine("Error: Empty response from server");
return 1;
}
// Deterministic ordering by band priority, then score descending
var sorted = result.Items
.OrderBy(u => u.Band switch { "hot" => 0, "warm" => 1, "cold" => 2, _ => 3 })
.ThenByDescending(u => u.Score)
.ToList();
TextWriter writer = outputPath is not null
? new StreamWriter(outputPath)
: Console.Out;
try
{
switch (format.ToLowerInvariant())
{
case "csv":
await WriteCsvAsync(writer, sorted);
break;
case "ndjson":
foreach (var item in sorted)
{
await writer.WriteLineAsync(JsonSerializer.Serialize(item, JsonOptions));
}
break;
case "json":
default:
await writer.WriteLineAsync(JsonSerializer.Serialize(sorted, JsonOptions));
break;
}
}
finally
{
if (outputPath is not null)
{
await writer.DisposeAsync();
}
}
if (verbose && outputPath is not null)
{
Console.WriteLine($"Exported {sorted.Count} unknowns to {outputPath}");
}
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Export failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static async Task WriteCsvAsync(TextWriter writer, IReadOnlyList<UnknownDto> items)
{
// CSV header
await writer.WriteLineAsync("id,package_id,package_version,band,score,reason_code,fingerprint_id,first_seen_at,last_evaluated_at");
foreach (var item in items)
{
await writer.WriteLineAsync(string.Format(
System.Globalization.CultureInfo.InvariantCulture,
"{0},{1},{2},{3},{4:F2},{5},{6},{7:u},{8:u}",
item.Id,
EscapeCsv(item.PackageId),
EscapeCsv(item.PackageVersion),
item.Band,
item.Score,
item.ReasonCode,
item.FingerprintId ?? "",
item.FirstSeenAt,
item.LastEvaluatedAt));
}
}
private static string EscapeCsv(string value)
{
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
{
return $"\"{value.Replace("\"", "\"\"")}\"";
}
return value;
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-003)
private static async Task<int> HandleTriageAsync(
IServiceProvider services,
string id,
string action,
string reason,
int? durationDays,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(UnknownsCommandGroup));
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory is null)
{
logger?.LogError("HTTP client factory not available");
return 1;
}
// Validate action
var validActions = new[] { "accept-risk", "require-fix", "defer", "escalate", "dispute" };
if (!validActions.Contains(action.ToLowerInvariant()))
{
Console.WriteLine($"Error: Invalid action '{action}'. Valid actions: {string.Join(", ", validActions)}");
return 1;
}
try
{
if (verbose)
{
logger?.LogDebug("Triaging unknown {Id} with action {Action}", id, action);
}
var client = httpClientFactory.CreateClient("PolicyApi");
var request = new TriageRequest(action, reason, durationDays);
var response = await client.PostAsJsonAsync(
$"/api/v1/policy/unknowns/{id}/triage",
request,
JsonOptions,
ct);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(ct);
logger?.LogError("Triage failed: {Status}", response.StatusCode);
Console.WriteLine($"Error: Triage failed ({response.StatusCode})");
return 1;
}
Console.WriteLine($"Unknown {id} triaged with action '{action}'.");
if (durationDays.HasValue)
{
Console.WriteLine($"Duration: {durationDays} days");
}
return 0;
}
catch (Exception ex)
{
logger?.LogError(ex, "Triage failed unexpectedly");
Console.WriteLine($"Error: {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle budget check command.
/// Sprint: SPRINT_5100_0004_0001 Task T1
@@ -927,5 +1568,102 @@ public static class UnknownsCommandGroup
public IReadOnlyDictionary<string, int>? ByReasonCode { get; init; }
}
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-001, CLI-UNK-002, CLI-UNK-003)
private sealed record UnknownsSummaryResponse
{
public int Hot { get; init; }
public int Warm { get; init; }
public int Cold { get; init; }
public int Resolved { get; init; }
public int Total { get; init; }
}
private sealed record UnknownDetailResponse
{
public UnknownDto? Unknown { get; init; }
}
private sealed record UnknownsListResponse
{
public IReadOnlyList<UnknownDto>? Items { get; init; }
public int TotalCount { get; init; }
}
private sealed record UnknownDto
{
public Guid Id { get; init; }
public string PackageId { get; init; } = string.Empty;
public string PackageVersion { get; init; } = string.Empty;
public string Band { get; init; } = string.Empty;
public decimal Score { get; init; }
public decimal UncertaintyFactor { get; init; }
public decimal ExploitPressure { get; init; }
public DateTimeOffset FirstSeenAt { get; init; }
public DateTimeOffset LastEvaluatedAt { get; init; }
public string? ResolutionReason { get; init; }
public DateTimeOffset? ResolvedAt { get; init; }
public string ReasonCode { get; init; } = string.Empty;
public string ReasonCodeShort { get; init; } = string.Empty;
public string? RemediationHint { get; init; }
public string? DetailedHint { get; init; }
public string? AutomationCommand { get; init; }
public IReadOnlyList<EvidenceRefDto>? EvidenceRefs { get; init; }
public string? FingerprintId { get; init; }
public IReadOnlyList<TriggerDto>? Triggers { get; init; }
public IReadOnlyList<string>? NextActions { get; init; }
public ConflictInfoDto? ConflictInfo { get; init; }
public string? ObservationState { get; init; }
}
private sealed record EvidenceRefDto
{
public string Type { get; init; } = string.Empty;
public string Uri { get; init; } = string.Empty;
public string? Digest { get; init; }
}
private sealed record TriggerDto
{
public string EventType { get; init; } = string.Empty;
public int EventVersion { get; init; }
public string? Source { get; init; }
public DateTimeOffset ReceivedAt { get; init; }
public string? CorrelationId { get; init; }
}
private sealed record ConflictInfoDto
{
public bool HasConflict { get; init; }
public double Severity { get; init; }
public string SuggestedPath { get; init; } = string.Empty;
public IReadOnlyList<ConflictDetailDto> Conflicts { get; init; } = [];
}
private sealed record ConflictDetailDto
{
public string Signal1 { get; init; } = string.Empty;
public string Signal2 { get; init; } = string.Empty;
public string Type { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public double Severity { get; init; }
}
private sealed record UnknownProof
{
public Guid Id { get; init; }
public string? FingerprintId { get; init; }
public string PackageId { get; init; } = string.Empty;
public string PackageVersion { get; init; } = string.Empty;
public string Band { get; init; } = string.Empty;
public decimal Score { get; init; }
public string ReasonCode { get; init; } = string.Empty;
public IReadOnlyList<TriggerDto> Triggers { get; init; } = [];
public IReadOnlyList<EvidenceRefDto> EvidenceRefs { get; init; } = [];
public string? ObservationState { get; init; }
public ConflictInfoDto? ConflictInfo { get; init; }
}
private sealed record TriageRequest(string Action, string Reason, int? DurationDays);
#endregion
}