up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
This commit is contained in:
@@ -1236,8 +1236,78 @@ internal static class CommandFactory
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
var explainOptions = CreateAdvisoryOptions();
|
||||
var explain = new Command("explain", "Explain an advisory conflict set with narrative and rationale.");
|
||||
AddAdvisoryOptions(explain, explainOptions);
|
||||
explain.SetAction((parseResult, _) =>
|
||||
{
|
||||
var advisoryKey = parseResult.GetValue(explainOptions.AdvisoryKey) ?? string.Empty;
|
||||
var artifactId = parseResult.GetValue(explainOptions.ArtifactId);
|
||||
var artifactPurl = parseResult.GetValue(explainOptions.ArtifactPurl);
|
||||
var policyVersion = parseResult.GetValue(explainOptions.PolicyVersion);
|
||||
var profile = parseResult.GetValue(explainOptions.Profile) ?? "default";
|
||||
var sections = parseResult.GetValue(explainOptions.Sections) ?? Array.Empty<string>();
|
||||
var forceRefresh = parseResult.GetValue(explainOptions.ForceRefresh);
|
||||
var timeoutSeconds = parseResult.GetValue(explainOptions.TimeoutSeconds) ?? 120;
|
||||
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(explainOptions.Format));
|
||||
var outputPath = parseResult.GetValue(explainOptions.Output);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleAdviseRunAsync(
|
||||
services,
|
||||
AdvisoryAiTaskType.Conflict,
|
||||
advisoryKey,
|
||||
artifactId,
|
||||
artifactPurl,
|
||||
policyVersion,
|
||||
profile,
|
||||
sections,
|
||||
forceRefresh,
|
||||
timeoutSeconds,
|
||||
outputFormat,
|
||||
outputPath,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
var remediateOptions = CreateAdvisoryOptions();
|
||||
var remediate = new Command("remediate", "Generate remediation guidance for an advisory.");
|
||||
AddAdvisoryOptions(remediate, remediateOptions);
|
||||
remediate.SetAction((parseResult, _) =>
|
||||
{
|
||||
var advisoryKey = parseResult.GetValue(remediateOptions.AdvisoryKey) ?? string.Empty;
|
||||
var artifactId = parseResult.GetValue(remediateOptions.ArtifactId);
|
||||
var artifactPurl = parseResult.GetValue(remediateOptions.ArtifactPurl);
|
||||
var policyVersion = parseResult.GetValue(remediateOptions.PolicyVersion);
|
||||
var profile = parseResult.GetValue(remediateOptions.Profile) ?? "default";
|
||||
var sections = parseResult.GetValue(remediateOptions.Sections) ?? Array.Empty<string>();
|
||||
var forceRefresh = parseResult.GetValue(remediateOptions.ForceRefresh);
|
||||
var timeoutSeconds = parseResult.GetValue(remediateOptions.TimeoutSeconds) ?? 120;
|
||||
var outputFormat = ParseAdvisoryOutputFormat(parseResult.GetValue(remediateOptions.Format));
|
||||
var outputPath = parseResult.GetValue(remediateOptions.Output);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return CommandHandlers.HandleAdviseRunAsync(
|
||||
services,
|
||||
AdvisoryAiTaskType.Remediation,
|
||||
advisoryKey,
|
||||
artifactId,
|
||||
artifactPurl,
|
||||
policyVersion,
|
||||
profile,
|
||||
sections,
|
||||
forceRefresh,
|
||||
timeoutSeconds,
|
||||
outputFormat,
|
||||
outputPath,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
advise.Add(run);
|
||||
advise.Add(summarize);
|
||||
advise.Add(explain);
|
||||
advise.Add(remediate);
|
||||
return advise;
|
||||
}
|
||||
|
||||
|
||||
@@ -553,6 +553,12 @@ internal static class CommandHandlers
|
||||
logger.LogInformation("Advisory output written to {Path}.", fullPath);
|
||||
}
|
||||
|
||||
if (rendered is not null)
|
||||
{
|
||||
// Surface the rendered advisory to the active console so users (and tests) can see it even when also writing to disk.
|
||||
AnsiConsole.Console.WriteLine(rendered);
|
||||
}
|
||||
|
||||
if (output.Guardrail.Blocked)
|
||||
{
|
||||
logger.LogError("Guardrail blocked advisory output (cache key {CacheKey}).", output.CacheKey);
|
||||
@@ -3075,7 +3081,7 @@ internal static class CommandHandlers
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
|
||||
Console.WriteLine(json);
|
||||
AnsiConsole.Console.WriteLine(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -6359,9 +6365,9 @@ internal static class CommandHandlers
|
||||
builder.AppendLine($"# Advisory {output.TaskType} ({output.Profile})");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine($"- Cache Key: `{output.CacheKey}`");
|
||||
builder.AppendLine($"- Generated: {output.GeneratedAtUtc.ToString(\"O\", CultureInfo.InvariantCulture)}");
|
||||
builder.AppendLine($"- Plan From Cache: {(output.PlanFromCache ? \"yes\" : \"no\")}");
|
||||
builder.AppendLine($"- Guardrail Blocked: {(output.Guardrail.Blocked ? \"yes\" : \"no\")}");
|
||||
builder.AppendLine($"- Generated: {output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture)}");
|
||||
builder.AppendLine($"- Plan From Cache: {(output.PlanFromCache ? "yes" : "no")}");
|
||||
builder.AppendLine($"- Guardrail Blocked: {(output.Guardrail.Blocked ? "yes" : "no")}");
|
||||
builder.AppendLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(output.Response))
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
| Task ID | State | Notes |
|
||||
| --- | --- | --- |
|
||||
| `SCANNER-CLI-0001` | DONE (2025-11-12) | Ruby verbs now consume the persisted `RubyPackageInventory`, warn when inventories are missing, and docs/tests were refreshed per Sprint 138. |
|
||||
| `CLI-AIAI-31-001` | BLOCKED (2025-11-22) | `stella advise summarize` command implemented; blocked on upstream Scanner analyzers (Node/Java) compile failures preventing CLI test run. |
|
||||
| `CLI-AIAI-31-001` | DONE (2025-11-24) | `stella advise summarize` command implemented; CLI analyzer build & tests now pass locally. |
|
||||
| `CLI-AIAI-31-002` | DONE (2025-11-24) | `stella advise explain` (conflict narrative) command implemented and tested. |
|
||||
| `CLI-AIAI-31-003` | DONE (2025-11-24) | `stella advise remediate` command implemented and tested. |
|
||||
|
||||
@@ -877,6 +877,202 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations_ForExplain()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
var testConsole = new TestConsole();
|
||||
|
||||
try
|
||||
{
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.Console = testConsole;
|
||||
|
||||
var planResponse = new AdvisoryPipelinePlanResponseModel
|
||||
{
|
||||
TaskType = "Conflict",
|
||||
CacheKey = "plan-conflict",
|
||||
PromptTemplate = "prompts/advisory/conflict.liquid",
|
||||
Budget = new AdvisoryTaskBudgetModel
|
||||
{
|
||||
PromptTokens = 128,
|
||||
CompletionTokens = 64
|
||||
},
|
||||
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
|
||||
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var outputResponse = new AdvisoryPipelineOutputModel
|
||||
{
|
||||
CacheKey = planResponse.CacheKey,
|
||||
TaskType = planResponse.TaskType,
|
||||
Profile = "default",
|
||||
Prompt = "Sanitized prompt",
|
||||
Response = "Rendered conflict body.",
|
||||
Citations = new[]
|
||||
{
|
||||
new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-42", ChunkId = "chunk-42" }
|
||||
},
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Guardrail = new AdvisoryOutputGuardrailModel
|
||||
{
|
||||
Blocked = false,
|
||||
SanitizedPrompt = "Sanitized prompt",
|
||||
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
},
|
||||
Provenance = new AdvisoryOutputProvenanceModel
|
||||
{
|
||||
InputDigest = "sha256:conflict-in",
|
||||
OutputHash = "sha256:conflict-out",
|
||||
Signatures = Array.Empty<string>()
|
||||
},
|
||||
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
|
||||
PlanFromCache = false
|
||||
};
|
||||
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
|
||||
{
|
||||
AdvisoryPlanResponse = planResponse,
|
||||
AdvisoryOutputResponse = outputResponse
|
||||
};
|
||||
|
||||
var provider = BuildServiceProvider(backend);
|
||||
var outputPath = Path.GetTempFileName();
|
||||
|
||||
await CommandHandlers.HandleAdviseRunAsync(
|
||||
provider,
|
||||
AdvisoryAiTaskType.Conflict,
|
||||
"ADV-42",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"default",
|
||||
Array.Empty<string>(),
|
||||
forceRefresh: false,
|
||||
timeoutSeconds: 0,
|
||||
outputFormat: AdvisoryOutputFormat.Markdown,
|
||||
outputPath: outputPath,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
var markdown = await File.ReadAllTextAsync(outputPath);
|
||||
Assert.Contains("Conflict", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Rendered conflict body", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("doc-42", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("chunk-42", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Citations", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.Contains("Conflict", testConsole.Output, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(AdvisoryAiTaskType.Conflict, backend.AdvisoryPlanRequests.Last().TaskType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = originalConsole;
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations_ForRemediation()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
var testConsole = new TestConsole();
|
||||
|
||||
try
|
||||
{
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.Console = testConsole;
|
||||
|
||||
var planResponse = new AdvisoryPipelinePlanResponseModel
|
||||
{
|
||||
TaskType = "Remediation",
|
||||
CacheKey = "plan-remediation",
|
||||
PromptTemplate = "prompts/advisory/remediation.liquid",
|
||||
Budget = new AdvisoryTaskBudgetModel
|
||||
{
|
||||
PromptTokens = 192,
|
||||
CompletionTokens = 96
|
||||
},
|
||||
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
|
||||
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var outputResponse = new AdvisoryPipelineOutputModel
|
||||
{
|
||||
CacheKey = planResponse.CacheKey,
|
||||
TaskType = planResponse.TaskType,
|
||||
Profile = "default",
|
||||
Prompt = "Sanitized prompt",
|
||||
Response = "Rendered remediation body.",
|
||||
Citations = new[]
|
||||
{
|
||||
new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-77", ChunkId = "chunk-77" }
|
||||
},
|
||||
Metadata = new Dictionary<string, string>(),
|
||||
Guardrail = new AdvisoryOutputGuardrailModel
|
||||
{
|
||||
Blocked = false,
|
||||
SanitizedPrompt = "Sanitized prompt",
|
||||
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
|
||||
Metadata = new Dictionary<string, string>()
|
||||
},
|
||||
Provenance = new AdvisoryOutputProvenanceModel
|
||||
{
|
||||
InputDigest = "sha256:remediation-in",
|
||||
OutputHash = "sha256:remediation-out",
|
||||
Signatures = Array.Empty<string>()
|
||||
},
|
||||
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
|
||||
PlanFromCache = false
|
||||
};
|
||||
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
|
||||
{
|
||||
AdvisoryPlanResponse = planResponse,
|
||||
AdvisoryOutputResponse = outputResponse
|
||||
};
|
||||
|
||||
var provider = BuildServiceProvider(backend);
|
||||
var outputPath = Path.GetTempFileName();
|
||||
|
||||
await CommandHandlers.HandleAdviseRunAsync(
|
||||
provider,
|
||||
AdvisoryAiTaskType.Remediation,
|
||||
"ADV-77",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"default",
|
||||
Array.Empty<string>(),
|
||||
forceRefresh: false,
|
||||
timeoutSeconds: 0,
|
||||
outputFormat: AdvisoryOutputFormat.Markdown,
|
||||
outputPath: outputPath,
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
var markdown = await File.ReadAllTextAsync(outputPath);
|
||||
Assert.Contains("Remediation", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Rendered remediation body", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("doc-77", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("chunk-77", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Citations", markdown, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.Contains("Remediation", testConsole.Output, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(AdvisoryAiTaskType.Remediation, backend.AdvisoryPlanRequests.Last().TaskType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AnsiConsole.Console = originalConsole;
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_ReturnsGuardrailExitCodeOnBlock()
|
||||
{
|
||||
@@ -3776,6 +3972,7 @@ spec:
|
||||
Array.Empty<TaskRunnerSimulationOutput>(),
|
||||
false);
|
||||
public Exception? TaskRunnerSimulationException { get; set; }
|
||||
public OfflineKitStatus? OfflineStatus { get; set; }
|
||||
public PolicyActivationResult ActivationResult { get; set; } = new PolicyActivationResult(
|
||||
"activated",
|
||||
new PolicyActivationRevision(
|
||||
@@ -3966,7 +4163,19 @@ spec:
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<OfflineKitStatus> GetOfflineKitStatusAsync(CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
{
|
||||
return Task.FromResult(OfflineStatus ?? new OfflineKitStatus(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<OfflineKitComponentStatus>()));
|
||||
}
|
||||
|
||||
public Task<EntryTraceResponseModel?> GetEntryTraceAsync(string scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user