From c490fec09adca38ec8711cb6dd7a82a64a0a03dc Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:51:26 +0300 Subject: [PATCH 01/20] docs: add NuGet package README --- README.nuget.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 README.nuget.md diff --git a/README.nuget.md b/README.nuget.md new file mode 100644 index 0000000..bcc49a7 --- /dev/null +++ b/README.nuget.md @@ -0,0 +1,85 @@ +# AgentContextKit + +Offline-first repository context and safety tooling for AI-assisted development. + +AgentContextKit analyzes a repository, generates clean agent context files, creates task-first workflow docs, and catches secret, PII, and brand leakage risks before a project is shared with AI agents or released publicly. + +This NuGet README is intentionally plain Markdown so it renders consistently on nuget.org. The richer GitHub README remains in `README.md`. + +## Install + +```powershell +dotnet tool install --global AgentContextKit --version 0.2.0-alpha.4 +``` + +Update an existing global install: + +```powershell +dotnet tool update --global AgentContextKit --version 0.2.0-alpha.4 +``` + +Verify the tool: + +```powershell +ackit version +ackit --help +``` + +## Quick start + +Run the local repository health check: + +```powershell +ackit doctor +``` + +Scan the current repository for release-blocking context risks: + +```powershell +ackit scan --ci +``` + +Create a task-first workflow note: + +```powershell +ackit task "Describe the next focused change" +``` + +Generate local agent instructions and context files: + +```powershell +ackit generate --target all +ackit prompt-pack --output .ackit/prompt-pack.md +``` + +## Core commands + +| Command | Purpose | +| --- | --- | +| `ackit doctor` | Checks repository readiness signals. | +| `ackit scan --ci` | Scans for secret, PII, brand, artifact, and release-safety findings. | +| `ackit task "title"` | Creates a task-first workflow document. | +| `ackit generate --target all` | Generates supported local agent instruction surfaces. | +| `ackit prompt-pack` | Builds a local Markdown prompt pack for review. | +| `ackit context-export --approve` | Exports reviewed context after explicit approval. | +| `ackit sarif --output ` | Writes SARIF for security tooling. | +| `ackit report --output ` | Writes a local HTML report. | +| `ackit watch --once` | Runs the watch-mode scan path once. | + +## Safety model + +Default commands process repository content locally. They do not upload a repository, call an AI API, send telemetry, or invoke external tools by default. + +The tool is designed for a human-reviewed workflow before a repository is handed to Codex, Claude Code, Cursor, GitHub Copilot, Gemini CLI, or a similar coding agent. + +## Documentation + +- Project website: https://github.com/Cynrath/agent-context-kit +- CLI reference: https://github.com/Cynrath/agent-context-kit/blob/master/docs/CLI_REFERENCE.md +- No-network default policy: https://github.com/Cynrath/agent-context-kit/blob/master/docs/NO_NETWORK_DEFAULT_POLICY.md +- Security policy: https://github.com/Cynrath/agent-context-kit/security +- License: MIT + +## Package note + +The NuGet package uses `README.nuget.md` as `PackageReadmeFile`. Keep this file pure Markdown and avoid raw HTML, local image paths, generated report artifacts, or GitHub-only layout markup. From aa5bd285adf45dd3585d7720f333341ffae3417e Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:51:50 +0300 Subject: [PATCH 02/20] build: target alpha4 package README --- src/AgentContextKit.Cli/AgentContextKit.Cli.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj b/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj index c50c1a1..d217fd1 100644 --- a/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj +++ b/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj @@ -12,15 +12,15 @@ true ackit AgentContextKit - 0.2.0-alpha.3 + 0.2.0-alpha.4 Cynrath Cynrath Copyright (c) 2026 Cynrath Offline-first repository context and safety tooling for AI-assisted development. - README.md + README.nuget.md MIT ai;coding-agent;context;security;cli;oss - MCP stdio server and ackit.rules metadata tool, local watch mode, diff/trim command stabilization, scan include/exclude filter documentation parity, README/CLI reference sync, and release hardening with RB-003/RB-008 evidence cleanup. + NuGet README rendering fix with a dedicated pure-Markdown package README and package metadata guidance. No runtime feature changes. https://github.com/Cynrath/agent-context-kit https://github.com/Cynrath/agent-context-kit git @@ -30,7 +30,7 @@ - + From 986a29ac6c87a679fbf7aa1fcf5a82c2ba9cde6d Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:52:07 +0300 Subject: [PATCH 03/20] chore: bump CLI version to alpha4 --- src/AgentContextKit.Cli/Program.cs | 1927 +--------------------------- 1 file changed, 2 insertions(+), 1925 deletions(-) diff --git a/src/AgentContextKit.Cli/Program.cs b/src/AgentContextKit.Cli/Program.cs index 2755799..d7e9878 100644 --- a/src/AgentContextKit.Cli/Program.cs +++ b/src/AgentContextKit.Cli/Program.cs @@ -5,7 +5,7 @@ namespace AgentContextKit.Cli; public static class Program { - private const string Version = "0.2.0-alpha.3"; + private const string Version = "0.2.0-alpha.4"; private const string DefaultBaselinePath = ".ackit-baseline.json"; private const int JsonSchemaVersion = 2; private const int ExitSuccess = 0; @@ -38,7 +38,7 @@ public static int Main(string[] args) "webui" => RunWebUi(args, repositoryPath, config, language, json, services), "prompt-pack" => RunPromptPack(args, repositoryPath, config, language, json, services), "context-export" => RunContextExport(args, repositoryPath, config, language, json, services), - "generate" => RunGenerate(args, repositoryPath, config, language, json, services), + "generate" => RunGenerate(args, repositoryPath, language, json, services), "task" => RunTask(args, repositoryPath, language, json, services), "redact-check" => RunRedactCheck(args, repositoryPath, config, language, json, services), "doctor" => RunDoctor(repositoryPath, config, language, json, services), @@ -58,1926 +58,3 @@ public static int Main(string[] args) return ExitError; } } - - private static int RunHelp(LanguageCode language, ITextProvider textProvider) - { - Console.WriteLine(textProvider.Get("help", language)); - Console.WriteLine(); - Console.WriteLine(textProvider.Get("usage", language)); - Console.WriteLine(" ackit init [--lang en|tr] [--json]"); - Console.WriteLine(" ackit config-check [--lang en|tr] [--json]"); - Console.WriteLine(" ackit scan [--baseline ] [--include ] [--exclude ] [--lang en|tr] [--json] [--ci]"); - Console.WriteLine(" ackit baseline [--output ] [--update] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit sarif --output [--baseline ] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit report [--output ] [--baseline ] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit webui [--output ] [--baseline ] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit prompt-pack [--output ] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit context-export --prompt-pack --approve [--output ] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit generate [--target codex|claude|anthropic|cursor|copilot|continue|all] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit task \"\" [--lang en|tr] [--json]"); - Console.WriteLine(" ackit redact-check [--profile public-release] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit doctor [--lang en|tr] [--json]"); - Console.WriteLine(" ackit hooks [--target codex|claude|anthropic|continue] [--shell pwsh|sh] [--install|--dry-run] [--output <repo-relative-dir>] [--lang en|tr] [--json]"); - Console.WriteLine(" ackit mcp --stdio-server [--repo <path>] [--lang en|tr]"); - Console.WriteLine(" ackit mcp --stdio <json-request> [--output <repo-relative.jsonl>] [--lang en|tr]"); - Console.WriteLine(" ackit diff --from <from.json> --to <to.json> [--lang en|tr] [--json]"); - Console.WriteLine(" ackit trim --input <repo-relative.md|json> --output <repo-relative.md|json> --max-chars <N> [--lang en|tr] [--json]"); - Console.WriteLine(" ackit watch [--debounce-ms <N>] [--once] [--max-runtime-ms <N>] [--json] [--lang en|tr]"); - Console.WriteLine(" ackit version"); - return ExitSuccess; - } - - private static int RunVersion() - { - Console.WriteLine($"AgentContextKit {Version}"); - return ExitSuccess; - } - - private static int RunInit(string repositoryPath, LanguageCode language, bool json, Services services) - { - var result = services.ConfigWriter.WriteDefaultIfMissing(repositoryPath, language); - var agentFiles = new[] { "AGENTS.md", "CLAUDE.md", ".cursor/rules/project.mdc", ".github/copilot-instructions.md" } - .Select(file => - { - var fullPath = Path.Combine(repositoryPath, file.Replace('/', Path.DirectorySeparatorChar)); - return new - { - path = file, - exists = services.FileSystem.FileExists(fullPath) - }; - }) - .ToArray(); - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "init", - config = ToGeneratedFileDto(result), - agentInstructionFiles = agentFiles - }); - return ExitSuccess; - } - - PrintGeneratedResult(result, services.TextProvider, language); - - Console.WriteLine(); - Console.WriteLine(services.TextProvider.Get("detectedAgentInstructionFiles", language)); - foreach (var file in agentFiles) - { - var status = services.TextProvider.Get(file.exists ? "found" : "missing", language); - Console.WriteLine($"- {file.path}: {status}"); - } - - return ExitSuccess; - } - - private static int RunConfigCheck( - string repositoryPath, - LanguageCode language, - bool json, - Services services) - { - const string relativePath = ".ackit/config.yml"; - var fullPath = Path.Combine(repositoryPath, ".ackit", "config.yml"); - var exists = services.FileSystem.FileExists(fullPath); - var result = exists - ? services.ConfigValidator.Validate(services.FileSystem.ReadAllText(fullPath)) - : new ConfigValidationResult(Array.Empty<ConfigDiagnostic>()); - var errorCount = result.Diagnostics.Count(diagnostic => diagnostic.Severity == ConfigDiagnosticSeverity.Error); - var warningCount = result.Diagnostics.Count(diagnostic => diagnostic.Severity == ConfigDiagnosticSeverity.Warning); - var infoCount = result.Diagnostics.Count(diagnostic => diagnostic.Severity == ConfigDiagnosticSeverity.Info); - var migrationRequired = result.Diagnostics.Any(diagnostic => - diagnostic.Code is ConfigDiagnosticCodes.ObsoleteKey or ConfigDiagnosticCodes.InvalidSchemaVersion); - var status = !exists - ? "default" - : errorCount > 0 - ? "errors" - : warningCount > 0 - ? "warnings" - : "valid"; - var exitCode = errorCount > 0 ? ExitError : ExitSuccess; - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "config-check", - repositoryName = GetRepositoryName(repositoryPath), - exitCode, - config = new - { - path = relativePath, - exists, - status, - supportedSchemaVersion = AckitConfig.Default.SchemaVersion, - migrationRequired - }, - diagnosticSummary = new - { - total = result.Diagnostics.Count, - info = infoCount, - warnings = warningCount, - errors = errorCount - }, - diagnostics = result.Diagnostics.Select(ToConfigDiagnosticDto).ToArray() - }); - return exitCode; - } - - var turkish = string.Equals(language.Value, LanguageCode.Turkish.Value, StringComparison.OrdinalIgnoreCase); - Console.WriteLine(turkish ? "Yapılandırma kontrolü" : "Configuration check"); - Console.WriteLine($"- {(turkish ? "Yol" : "Path")}: {relativePath}"); - Console.WriteLine($"- {(turkish ? "Durum" : "Status")}: {ToConfigStatusLabel(status, turkish)}"); - Console.WriteLine(turkish - ? $"- Tanılar: {result.Diagnostics.Count} ({errorCount} hata, {warningCount} uyarı)" - : $"- Diagnostics: {result.Diagnostics.Count} ({errorCount} errors, {warningCount} warnings)"); - - if (!exists) - { - Console.WriteLine(turkish - ? "Yapılandırma dosyası yok; varsayılan ayarlar geçerli." - : "Configuration file is missing; defaults are valid."); - return exitCode; - } - - if (result.Diagnostics.Count == 0) - { - Console.WriteLine(turkish ? "Yapılandırma tanısı yok." : "No configuration diagnostics."); - return exitCode; - } - - foreach (var diagnostic in result.Diagnostics) - { - var key = diagnostic.Key is null - ? "" - : turkish - ? $" anahtar {diagnostic.Key}" - : $" key {diagnostic.Key}"; - var severity = turkish ? ToTurkishConfigSeverity(diagnostic.Severity) : diagnostic.Severity.ToString(); - var line = turkish ? "satır" : "line"; - Console.WriteLine($"- {severity} {diagnostic.Code} {line} {diagnostic.Line}{key}: {diagnostic.Message}"); - } - - if (migrationRequired) - { - Console.WriteLine(turkish - ? "Geçiş incelemesi gerekli; dosya otomatik olarak değiştirilmedi." - : "Migration review is required; the file was not changed automatically."); - } - - return exitCode; - } - - private static string ToConfigStatusLabel(string status, bool turkish) - { - if (!turkish) - { - return status; - } - - return status switch - { - "default" => "varsayılan", - "valid" => "geçerli", - "warnings" => "uyarılar", - "errors" => "hatalar", - _ => status - }; - } - - private static string ToTurkishConfigSeverity(ConfigDiagnosticSeverity severity) - { - return severity switch - { - ConfigDiagnosticSeverity.Info => "Bilgi", - ConfigDiagnosticSeverity.Warning => "Uyarı", - ConfigDiagnosticSeverity.Error => "Hata", - _ => severity.ToString() - }; - } - - private static int RunScan(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, bool ci, Services services) - { - IReadOnlyList<string> includeGlobs; - IReadOnlyList<string> excludeGlobs; - try - { - includeGlobs = ParseGlobList(args, "--include"); - excludeGlobs = ParseGlobList(args, "--exclude"); - } - catch (ArgumentException ex) - { - return WriteInvalidArgumentError("scan", ex.Message, json, language, services); - } - - var scan = services.RepositoryScanner.Scan(repositoryPath, config, includeGlobs, excludeGlobs); - string? baselinePath; - BaselineEvaluation? baseline; - try - { - (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); - } - catch (BaselineException ex) - { - return WriteBaselineError("scan", ex, json, services.Clock.UtcNow); - } - - var exitCode = baseline is null - ? GetScanExitCode(scan, ci) - : GetBaselineScanExitCode(baseline, ci); - if (json) - { - WriteJson(ToScanDto("scan", scan, services.Clock.UtcNow, ci, exitCode, baselinePath, baseline)); - return exitCode; - } - - PrintScan(scan, language, services); - if (baseline is not null) - { - PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); - } - - return exitCode; - } - - private static IReadOnlyList<string> ParseGlobList(string[] args, string name) - { - var values = new List<string>(); - for (var index = 0; index < args.Length; index++) - { - var current = args[index]; - string? value = null; - if (current.StartsWith(name + "=", StringComparison.OrdinalIgnoreCase)) - { - value = current[(name.Length + 1)..]; - } - else if (string.Equals(current, name, StringComparison.OrdinalIgnoreCase) && index + 1 < args.Length) - { - value = args[index + 1]; - index++; - } - - if (value is null) - { - continue; - } - - if (string.IsNullOrWhiteSpace(value)) - { - throw new ArgumentException($"{name} glob must not be empty or whitespace."); - } - - values.Add(value.Trim()); - } - - return values; - } - - private static int WriteInvalidArgumentError(string command, string message, bool json, LanguageCode language, Services services) - { - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command, - error = "InvalidArgument", - message - }); - } - else - { - var localized = services.TextProvider.Get("invalidArgument", language); - Console.Error.WriteLine($"{localized}: {message}"); - } - - return ExitError; - } - - private static int RunBaseline( - string[] args, - string repositoryPath, - AckitConfig config, - LanguageCode language, - bool json, - Services services) - { - var outputPath = GetOption(args, "--output") ?? DefaultBaselinePath; - try - { - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - var manifest = services.BaselineClassifier.CreateManifest(scan.Findings); - var result = services.BaselineStore.Write(repositoryPath, outputPath, manifest, HasFlag(args, "--update")); - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "baseline", - repositoryName = GetRepositoryName(repositoryPath), - baseline = new - { - path = result.Path, - status = result.Status.ToString(), - schemaVersion = manifest.SchemaVersion, - fingerprintAlgorithm = manifest.FingerprintAlgorithm, - entryCount = result.EntryCount - } - }); - return ExitSuccess; - } - - var statusKey = result.Status == BaselineFileStatus.Created ? "baselineCreated" : "baselineUpdated"; - Console.WriteLine($"{services.TextProvider.Get(statusKey, language)}: {result.Path}"); - Console.WriteLine($"{services.TextProvider.Get("entries", language)}: {result.EntryCount}"); - Console.WriteLine(services.TextProvider.Get("baselineReview", language)); - return ExitSuccess; - } - catch (BaselineException ex) - { - return WriteBaselineError("baseline", ex, json, services.Clock.UtcNow); - } - } - - private static int RunSarif(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - var outputPath = GetOption(args, "--output"); - if (string.IsNullOrWhiteSpace(outputPath)) - { - Console.Error.WriteLine(services.TextProvider.Get("sarifRequiresOutput", language)); - return ExitError; - } - - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - string? baselinePath; - BaselineEvaluation? baseline; - try - { - (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); - } - catch (BaselineException ex) - { - return WriteBaselineError("sarif", ex, json, services.Clock.UtcNow); - } - - var result = services.SarifReportWriter.Generate(repositoryPath, outputPath, scan, Version, baseline); - var criticalHighCount = scan.Findings.Count(finding => finding.Severity is RiskSeverity.Critical or RiskSeverity.High); - - if (json) - { - var response = new Dictionary<string, object?> - { - ["schemaVersion"] = JsonSchemaVersion, - ["toolVersion"] = Version, - ["generatedAtUtc"] = services.Clock.UtcNow, - ["command"] = "sarif", - ["repositoryName"] = GetRepositoryName(repositoryPath), - ["riskSummary"] = ToRiskSummary(scan.Findings), - ["criticalHighCount"] = criticalHighCount, - ["sarif"] = ToGeneratedFileDto(result) - }; - AddBaselineDto(response, baselinePath, baseline); - WriteJson(response); - return ExitSuccess; - } - - PrintGeneratedResult(result, services.TextProvider, language); - Console.WriteLine($"{services.TextProvider.Get("sarifFindings", language)}: {scan.Findings.Count}"); - Console.WriteLine($"{services.TextProvider.Get("criticalHighFindings", language)}: {criticalHighCount}"); - if (baseline is not null) - { - PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); - } - return ExitSuccess; - } - - private static int RunReport(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - var outputPath = GetOption(args, "--output"); - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - string? baselinePath; - BaselineEvaluation? baseline; - try - { - (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); - } - catch (BaselineException ex) - { - return WriteBaselineError("report", ex, json, services.Clock.UtcNow); - } - - var result = services.HtmlReportGenerator.Generate(repositoryPath, outputPath, language, scan, baseline); - - if (json) - { - var response = new Dictionary<string, object?> - { - ["schemaVersion"] = JsonSchemaVersion, - ["toolVersion"] = Version, - ["generatedAtUtc"] = services.Clock.UtcNow, - ["command"] = "report", - ["repositoryPath"] = repositoryPath, - ["repositoryName"] = GetRepositoryName(repositoryPath), - ["riskSummary"] = ToRiskSummary(scan.Findings), - ["report"] = ToGeneratedFileDto(result) - }; - AddBaselineDto(response, baselinePath, baseline); - WriteJson(response); - return ExitSuccess; - } - - PrintGeneratedResult(result, services.TextProvider, language); - Console.WriteLine($"{services.TextProvider.Get("riskFindings", language)}: {scan.Findings.Count}"); - if (baseline is not null) - { - PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); - } - return ExitSuccess; - } - - private static int RunWebUi(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - var outputPath = GetOption(args, "--output"); - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - string? baselinePath; - BaselineEvaluation? baseline; - try - { - (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); - } - catch (BaselineException ex) - { - return WriteBaselineError("webui", ex, json, services.Clock.UtcNow); - } - - var result = services.WebUiGenerator.Generate(repositoryPath, outputPath, language, scan, baseline); - - if (json) - { - var response = new Dictionary<string, object?> - { - ["schemaVersion"] = JsonSchemaVersion, - ["toolVersion"] = Version, - ["generatedAtUtc"] = services.Clock.UtcNow, - ["command"] = "webui", - ["repositoryPath"] = repositoryPath, - ["repositoryName"] = GetRepositoryName(repositoryPath), - ["riskSummary"] = ToRiskSummary(scan.Findings), - ["webUi"] = ToGeneratedFileDto(result) - }; - AddBaselineDto(response, baselinePath, baseline); - WriteJson(response); - return ExitSuccess; - } - - PrintGeneratedResult(result, services.TextProvider, language); - Console.WriteLine($"{services.TextProvider.Get("riskFindings", language)}: {scan.Findings.Count}"); - if (baseline is not null) - { - PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); - } - return ExitSuccess; - } - - private static int RunPromptPack(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - var outputPath = GetOption(args, "--output"); - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - var result = services.PromptPackGenerator.Generate(repositoryPath, outputPath, language, scan); - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "prompt-pack", - repositoryPath, - repositoryName = GetRepositoryName(repositoryPath), - riskSummary = ToRiskSummary(scan.Findings), - promptPack = ToGeneratedFileDto(result) - }); - return ExitSuccess; - } - - PrintGeneratedResult(result, services.TextProvider, language); - Console.WriteLine(services.TextProvider.Get("noRemoteCall", language)); - Console.WriteLine($"{services.TextProvider.Get("riskFindings", language)}: {scan.Findings.Count}"); - return ExitSuccess; - } - - private static int RunContextExport(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - if (!HasFlag(args, "--approve")) - { - Console.Error.WriteLine(services.TextProvider.Get("contextExportRequiresApproval", language)); - return ExitError; - } - - var promptPackPath = GetOption(args, "--prompt-pack"); - if (string.IsNullOrWhiteSpace(promptPackPath)) - { - Console.Error.WriteLine(services.TextProvider.Get("contextExportRequiresPromptPack", language)); - return ExitError; - } - - var outputPath = GetOption(args, "--output"); - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - var result = services.ContextExportManifestGenerator.Generate( - repositoryPath, - new ContextExportSpec(promptPackPath, outputPath, "explicit-cli-flag", language), - scan); - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "context-export", - repositoryPath, - repositoryName = GetRepositoryName(repositoryPath), - riskSummary = ToRiskSummary(scan.Findings), - contextExport = ToGeneratedFileDto(result) - }); - return ExitSuccess; - } - - PrintGeneratedResult(result, services.TextProvider, language); - Console.WriteLine(services.TextProvider.Get("noRemoteCall", language)); - Console.WriteLine(services.TextProvider.Get("approvalRecorded", language)); - return ExitSuccess; - } - - private static int RunGenerate(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - var target = ParseTarget(GetOption(args, "--target")); - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - var results = services.AgentInstructionGenerator.Generate(repositoryPath, target, language, scan); - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "generate", - target = target.ToString(), - fileSummary = ToGeneratedFileSummary(results), - files = results.Select(ToGeneratedFileDto).ToArray() - }); - return ExitSuccess; - } - - foreach (var result in results) - { - PrintGeneratedResult(result, services.TextProvider, language); - } - - return ExitSuccess; - } - - private static int RunTask(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) - { - var title = GetTaskTitle(args); - if (string.IsNullOrWhiteSpace(title)) - { - Console.Error.WriteLine(services.TextProvider.Get("taskRequiresTitle", language)); - Console.Error.WriteLine(services.TextProvider.Get("taskExample", language)); - return ExitError; - } - - var result = services.TaskFileGenerator.CreateTask(repositoryPath, new TaskSpec(title, language)); - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "task", - file = ToGeneratedFileDto(result) - }); - return ExitSuccess; - } - - PrintGeneratedResult(result, services.TextProvider, language); - return ExitSuccess; - } - - private static int RunRedactCheck(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - var profile = GetOption(args, "--profile") ?? "default"; - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - var findings = scan.Findings - .Where(finding => finding.Category is RiskCategory.Secret or RiskCategory.Pii or RiskCategory.Brand or RiskCategory.LocalPath or RiskCategory.ProductionConfig) - .ToArray(); - - var exitCode = findings.Any(finding => finding.Severity == RiskSeverity.Critical) - ? ExitCritical - : findings.Length > 0 ? ExitError : ExitSuccess; - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "redact-check", - repositoryPath, - repositoryName = GetRepositoryName(repositoryPath), - profile, - exitCode, - riskSummary = ToRiskSummary(findings), - findings = findings.Select(ToRiskFindingDto).ToArray() - }); - return exitCode; - } - - PrintFindings(findings, language, services); - return exitCode; - } - - private static int RunDoctor(string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - var scan = services.RepositoryScanner.Scan(repositoryPath, config); - var result = services.Doctor.Check(repositoryPath, scan); - var exitCode = result.Checks.Any(check => !check.Passed && check.Severity >= RiskSeverity.High) - ? ExitError - : ExitSuccess; - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = services.Clock.UtcNow, - command = "doctor", - repositoryPath, - repositoryName = GetRepositoryName(repositoryPath), - exitCode, - checkSummary = ToDoctorCheckSummary(result.Checks), - checks = result.Checks.Select(ToDoctorCheckDto).ToArray() - }); - return exitCode; - } - - Console.WriteLine(services.TextProvider.Get("doctor", language)); - foreach (var check in result.Checks) - { - var status = check.Passed ? "PASS" : "FAIL"; - Console.WriteLine($"- {status} [{check.Severity}] {check.Name}: {check.Message}"); - } - - return exitCode; - } - - private static int RunHooks(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) - { - var install = HasFlag(args, "--install"); - var dryRun = HasFlag(args, "--dry-run"); - var shell = GetOption(args, "--shell")?.Trim().ToLowerInvariant() ?? "sh"; - var output = GetOption(args, "--output"); - var target = ParseHookTarget(GetOption(args, "--target")); - var textProvider = services.TextProvider; - - if (shell is not ("pwsh" or "sh")) - { - var message = $"--shell must be pwsh or sh. Got: {shell}"; - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = message }); - } - else - { - Console.Error.WriteLine(message); - } - - return ExitError; - } - - if (target == AgentTarget.Anthropic && - shell == "sh" && - OperatingSystem.IsWindows() && - !CommandExistsOnPath("sh")) - { - const string message = "--target anthropic with --shell sh requires sh to be available on PATH on Windows. Use --shell pwsh or install sh."; - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = message }); - } - else - { - Console.Error.WriteLine(message); - } - - return ExitError; - } - - var targets = HookFileBuilder.Build(target, shell); - var isGitRepo = Directory.Exists(Path.Combine(repositoryPath, ".git")); - if (!isGitRepo && output is null && targets.Any(file => file.Path.StartsWith(".git/hooks/", StringComparison.Ordinal))) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = textProvider.Get("hooksNotGitRepo", language) }); - } - else - { - Console.Error.WriteLine(textProvider.Get("hooksNotGitRepo", language)); - } - return ExitError; - } - - string baseDir; - try - { - baseDir = GetHookBaseDirectory(repositoryPath, output); - } - catch (InvalidOperationException ex) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = ex.Message }); - } - else - { - Console.Error.WriteLine(ex.Message); - } - - return ExitError; - } - - var items = new List<object>(); - foreach (var (relPath, content) in targets) - { - var writePath = GetHookWritePath(output, relPath); - var fullPath = Path.Combine(baseDir, writePath.Replace('/', Path.DirectorySeparatorChar)); - var exists = File.Exists(fullPath); - string status; - if (dryRun) - { - status = textProvider.Get("hooksDryRun", language); - } - else if (install && !exists) - { - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); - File.WriteAllText(fullPath, content); - if (ShouldSetExecutable(shell, relPath) && (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())) - { - try { File.SetUnixFileMode(fullPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherExecute); } catch { } - } - status = textProvider.Get("hooksInstalled", language); - } - else if (exists) - { - status = textProvider.Get("hooksSkipped", language); - } - else - { - status = textProvider.Get("hooksWouldWrite", language); - } - - items.Add(new { path = writePath, status, contentLength = content.Length }); - } - - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", target = target.ToString(), install, dryRun, shell, mode = dryRun ? "dry-run" : install ? "install" : "preview", exitCode = ExitSuccess, files = items }); - } - else if (dryRun || !install) - { - Console.WriteLine(textProvider.Get("hooksPreview", language)); - foreach (var item in items) - { - var dyn = (dynamic)item; - Console.WriteLine($" {dyn.path}: {dyn.status} ({dyn.contentLength} chars)"); - } - } - else - { - foreach (var item in items) - { - var dyn = (dynamic)item; - Console.WriteLine($" {dyn.path}: {dyn.status} ({dyn.contentLength} chars)"); - } - } - return ExitSuccess; - } - - private static string GetHookBaseDirectory(string repositoryPath, string? output) - { - if (string.IsNullOrWhiteSpace(output)) - { - return repositoryPath; - } - - var normalized = output.Trim().Replace('\\', '/'); - if (Path.IsPathRooted(normalized)) - { - throw new InvalidOperationException("Hooks output path must be repository-relative."); - } - - var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); - if (segments.Length == 0 || segments.Any(segment => segment is "." or "..")) - { - throw new InvalidOperationException("Hooks output path must stay inside the repository."); - } - - var repositoryFullPath = Path.GetFullPath(repositoryPath); - var outputFullPath = Path.GetFullPath(Path.Combine(repositoryFullPath, normalized.Replace('/', Path.DirectorySeparatorChar))); - var repositoryPrefix = repositoryFullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; - - if (!outputFullPath.StartsWith(repositoryPrefix, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("Hooks output path must stay inside the repository."); - } - - return outputFullPath; - } - - private static string GetHookWritePath(string? output, string relativePath) - { - if (output is null) - { - return relativePath; - } - - return relativePath.StartsWith(".git/hooks/", StringComparison.Ordinal) - ? relativePath.Replace(".git/hooks/", string.Empty, StringComparison.Ordinal) - : relativePath; - } - - private static bool ShouldSetExecutable(string shell, string relativePath) - { - return shell == "sh" && - (relativePath.StartsWith(".git/hooks/", StringComparison.Ordinal) || - relativePath.EndsWith(".sh", StringComparison.OrdinalIgnoreCase)); - } - - private static int RunMcp(string[] args, string repositoryPath, LanguageCode language, Services services) - { - if (HasFlag(args, "--help") || HasFlag(args, "-h")) - { - Console.WriteLine("AgentContextKit MCP stdio transport"); - Console.WriteLine(); - Console.WriteLine("Usage:"); - Console.WriteLine(" ackit mcp --stdio-server [--repo <path>] [--lang en|tr]"); - Console.WriteLine(" ackit mcp --stdio <json-request> [--output <repo-relative.jsonl>] [--lang en|tr]"); - Console.WriteLine(" ackit mcp --help"); - Console.WriteLine(); - Console.WriteLine("Modes:"); - Console.WriteLine(" --stdio-server Real stdio loop. Reads JSON-RPC 2.0 line-delimited messages from"); - Console.WriteLine(" Console.In and writes JSON-RPC responses to Console.Out."); - Console.WriteLine(" Diagnostics go to Console.Error. Exits 0 on EOF or"); - Console.WriteLine(" notifications/exit|shutdown. No network, no source mutation."); - Console.WriteLine(" --stdio <json> One-shot JSON-RPC round-trip (test seam; kept for backward"); - Console.WriteLine(" compatibility). Writes the single response to Console.Out or"); - Console.WriteLine(" to the file passed with --output."); - Console.WriteLine(); - Console.WriteLine("Methods:"); - Console.WriteLine(" initialize, tools/list, tools/call, notifications/initialized, ping,"); - Console.WriteLine(" notifications/exit (or notifications/shutdown)."); - Console.WriteLine(); - Console.WriteLine("Tools:"); - Console.WriteLine(" ackit.scan, ackit.findings, ackit.context, ackit.health."); - return ExitSuccess; - } - - if (HasFlag(args, "--stdio-server")) - { - return RunMcpStdioServer(args, repositoryPath, language, services); - } - - var input = GetOption(args, "--stdio"); - if (string.IsNullOrWhiteSpace(input)) - { - Console.Error.WriteLine(services.TextProvider.Get("mcpRequiresStdio", language)); - return ExitError; - } - - var response = services.McpServer.HandleJson(input); - var output = GetOption(args, "--output"); - if (string.IsNullOrWhiteSpace(output)) - { - Console.WriteLine(response); - return ExitSuccess; - } - - try - { - var outputPath = NormalizeMcpOutputPath(repositoryPath, output); - services.FileSystem.WriteAllText(outputPath, response.TrimEnd() + Environment.NewLine); - Console.WriteLine(language.Value == "tr" - ? "MCP yaniti yazildi." - : "MCP response written."); - return ExitSuccess; - } - catch (InvalidOperationException ex) - { - Console.Error.WriteLine(ex.Message); - return ExitError; - } - } - - private static int RunMcpStdioServer(string[] args, string repositoryPath, LanguageCode language, Services services) - { - var defaultRepo = repositoryPath; - var repoOverride = GetOption(args, "--repo"); - if (!string.IsNullOrWhiteSpace(repoOverride)) - { - var trimmed = repoOverride.Trim(); - if (trimmed.Contains("://", StringComparison.Ordinal) || - trimmed.StartsWith("file:", StringComparison.OrdinalIgnoreCase) || - trimmed.StartsWith(@"\\", StringComparison.Ordinal) || - trimmed.StartsWith("//", StringComparison.Ordinal)) - { - Console.Error.WriteLine(services.TextProvider.Get("mcpInvalidRepo", language)); - return ExitCritical; - } - try - { - var fullPath = Path.GetFullPath(trimmed); - if (!services.FileSystem.DirectoryExists(fullPath)) - { - Console.Error.WriteLine(services.TextProvider.Get("mcpRepoNotDirectory", language)); - return ExitCritical; - } - defaultRepo = fullPath; - } - catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) - { - Console.Error.WriteLine(services.TextProvider.Get("mcpInvalidRepoPath", language)); - return ExitCritical; - } - } - - var options = new McpStdioOptions { DefaultRepositoryPath = defaultRepo }; - var transport = new McpStdioTransport(services.McpServer, Console.In, Console.Out, Console.Error, options); - - try - { - return transport.RunAsync().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - Console.Error.WriteLine(services.TextProvider.Get("mcpServerCrashed", language).Replace("{kind}", ex.GetType().Name)); - return ExitError; - } - } - - private static string NormalizeMcpOutputPath(string repositoryPath, string output) - { - var normalized = output.Trim().Replace('\\', '/'); - if (Path.IsPathRooted(normalized)) - { - throw new InvalidOperationException("MCP output path must be repository-relative."); - } - - var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); - if (segments.Length == 0 || segments.Any(segment => segment is "." or "..")) - { - throw new InvalidOperationException("MCP output path must stay inside the repository."); - } - - if (!normalized.EndsWith(".json", StringComparison.OrdinalIgnoreCase) && - !normalized.EndsWith(".jsonl", StringComparison.OrdinalIgnoreCase) && - !normalized.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("MCP output path must end with .json, .jsonl, or .txt."); - } - - var repositoryFullPath = Path.GetFullPath(repositoryPath); - var outputFullPath = Path.GetFullPath(Path.Combine(repositoryFullPath, normalized.Replace('/', Path.DirectorySeparatorChar))); - var repositoryPrefix = repositoryFullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; - - if (!outputFullPath.StartsWith(repositoryPrefix, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("MCP output path must stay inside the repository."); - } - - return outputFullPath; - } - - private static int RunDiff(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) - { - var from = GetOption(args, "--from"); - var to = GetOption(args, "--to"); - if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to)) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, command = "diff", exitCode = ExitError, error = "Missing --from or --to" }); - } - else - { - Console.Error.WriteLine("ackit diff requires --from and --to <repo-relative.json>."); - } - return ExitError; - } - - try - { - var fromPath = Path.Combine(repositoryPath, from.Replace('/', Path.DirectorySeparatorChar)); - var toPath = Path.Combine(repositoryPath, to.Replace('/', Path.DirectorySeparatorChar)); - if (!File.Exists(fromPath) || !File.Exists(toPath)) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, command = "diff", exitCode = ExitError, error = "Baseline file not found" }); - } - else - { - Console.Error.WriteLine("Baseline file not found."); - } - return ExitError; - } - - var fromJson = File.ReadAllText(fromPath); - var toJson = File.ReadAllText(toPath); - var fromManifest = BaselineSerializer.Deserialize(fromJson); - var toManifest = BaselineSerializer.Deserialize(toJson); - var diff = BaselineDiffCalculator.Compare(fromManifest, toManifest); - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - command = "diff", - exitCode = ExitSuccess, - fromBaseline = from, - toBaseline = to, - addedCount = diff.Added.Count, - removedCount = diff.Removed.Count, - unchangedCount = diff.Unchanged.Count, - severityChangedCount = diff.SeverityChanged.Count - }); - } - else - { - Console.WriteLine($"added: {diff.Added.Count}"); - Console.WriteLine($"removed: {diff.Removed.Count}"); - Console.WriteLine($"unchanged: {diff.Unchanged.Count}"); - Console.WriteLine($"severityChanged: {diff.SeverityChanged.Count}"); - } - return ExitSuccess; - } - catch (Exception ex) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, command = "diff", exitCode = ExitError, error = ex.Message }); - } - else - { - Console.Error.WriteLine($"ackit diff failed: {ex.Message}"); - } - return ExitError; - } - } - - private static int RunTrim(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) - { - var input = GetOption(args, "--input"); - var output = GetOption(args, "--output"); - var maxCharsStr = GetOption(args, "--max-chars"); - - if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(output) || string.IsNullOrWhiteSpace(maxCharsStr)) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Missing --input, --output, or --max-chars" }); - } - else - { - Console.Error.WriteLine(services.TextProvider.Get("trimRequiresArgs", language)); - } - return ExitError; - } - - if (!int.TryParse(maxCharsStr, out var maxChars) || maxChars <= 0) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Invalid --max-chars" }); - } - else - { - Console.Error.WriteLine(services.TextProvider.Get("trimInvalidMaxChars", language)); - } - return ExitError; - } - - var inputFull = Path.Combine(repositoryPath, input.Replace('/', Path.DirectorySeparatorChar)); - var outputFull = Path.Combine(repositoryPath, output.Replace('/', Path.DirectorySeparatorChar)); - if (string.Equals(Path.GetFullPath(inputFull), Path.GetFullPath(outputFull), StringComparison.OrdinalIgnoreCase)) - { - if (json) - { - WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Input and output must differ" }); - } - else - { - Console.Error.WriteLine(services.TextProvider.Get("trimInputOutputMustDiffer", language)); - } - return ExitError; - } - - try - { - if (!File.Exists(inputFull)) - { - if (json) WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Input file not found" }); - else Console.Error.WriteLine(services.TextProvider.Get("trimInputNotFound", language)); - return ExitError; - } - var content = File.ReadAllText(inputFull); - var originalChars = content.Length; - var trimmed = TextTrimmer.Trim(content, maxChars); - Directory.CreateDirectory(Path.GetDirectoryName(outputFull)!); - File.WriteAllText(outputFull, trimmed); - - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - command = "trim", - exitCode = ExitSuccess, - input, - output, - maxChars, - originalChars, - trimmedChars = trimmed.Length - }); - } - else - { - Console.WriteLine($"original: {originalChars}"); - Console.WriteLine($"trimmed: {trimmed.Length}"); - Console.WriteLine($"max-chars: {maxChars}"); - } - return ExitSuccess; - } - catch (Exception ex) - { - if (json) WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = ex.Message }); - else Console.Error.WriteLine($"ackit trim failed: {ex.Message}"); - return ExitError; - } - } - - private static int RunWatch(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) - { - int debounceMs; - bool once; - int maxRuntimeMs; - try - { - debounceMs = ParsePositiveInt(args, "--debounce-ms", defaultValue: 500); - once = HasFlag(args, "--once"); - maxRuntimeMs = ParsePositiveInt(args, "--max-runtime-ms", defaultValue: 0); - } - catch (ArgumentException ex) - { - return WriteInvalidArgumentError("watch", ex.Message, json, language, services); - } - - var startupMessage = services.TextProvider.Get("watchWatching", language) - .Replace("{repo}", GetRepositoryName(repositoryPath)) - .Replace("{ms}", debounceMs.ToString(System.Globalization.CultureInfo.InvariantCulture)); - if (json) - { - Console.Error.WriteLine(startupMessage); - } - else - { - Console.WriteLine(startupMessage); - } - - var options = new WatchOptions( - Debounce: TimeSpan.FromMilliseconds(debounceMs), - MaxRuntime: TimeSpan.FromMilliseconds(maxRuntimeMs), - OneShot: once, - Language: language, - EmitJson: json, - RepositoryPath: repositoryPath, - Config: config, - Clock: () => services.Clock.UtcNow); - - IFileWatcher watcher = new PhysicalFileWatcher(repositoryPath); - if (HasFlag(args, "--help") || HasFlag(args, "-h")) - { - Console.WriteLine("ackit watch -- local file-system change watcher"); - Console.WriteLine("Usage:"); - Console.WriteLine(" ackit watch [--debounce-ms <N>] [--once] [--max-runtime-ms <N>] [--json] [--lang en|tr]"); - Console.WriteLine("Options:"); - Console.WriteLine(" --debounce-ms <N> Minimum interval between re-runs (default 500)."); - Console.WriteLine(" --once Run a single scan and exit."); - Console.WriteLine(" --max-runtime-ms <N> Wall-clock cap (0 = unlimited)."); - Console.WriteLine(" --json Emit JSON change reports."); - Console.WriteLine(" --lang en|tr Human output language (default en)."); - ((IDisposable)watcher).Dispose(); - return ExitSuccess; - } - - if (HasFlag(args, "--help") || HasFlag(args, "-h")) - { - return ExitSuccess; - } - - WatchResult result; - try - { - result = WatchRunner.Run(watcher, services.RepositoryScanner, options); - } - finally - { - ((IDisposable)watcher).Dispose(); - } - - if (result.LastReport is not null) - { - try - { - WriteWatchReport(repositoryPath, result.LastReport, language, json); - } - catch (Exception ex) - { - Console.Error.WriteLine(services.TextProvider.Get("watchReportFailed", language).Replace("{kind}", ex.GetType().Name)); - } - } - - return ExitSuccess; - } - - private static void WriteWatchReport(string repositoryPath, ScanChangeReport report, LanguageCode language, bool json) - { - if (json) - { - var dto = new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc = DateTimeOffset.UtcNow, - command = "watch", - repositoryName = GetRepositoryName(repositoryPath), - addedCount = report.AddedCount, - removedCount = report.RemovedCount, - unchangedCount = report.UnchangedCount, - severityChangedCount = report.SeverityChangedCount, - addedSample = report.AddedSample.Select(finding => new - { - ruleId = RiskRuleCatalog.GetRuleId(finding), - severity = finding.Severity.ToString(), - path = finding.Path, - message = finding.Message - }).ToArray(), - removedSample = report.RemovedSample.Select(finding => new - { - ruleId = RiskRuleCatalog.GetRuleId(finding), - severity = finding.Severity.ToString(), - path = finding.Path, - message = finding.Message - }).ToArray(), - severityChangedSample = report.SeverityChangedSample.Select(sample => new - { - ruleId = sample.RuleId, - path = sample.Path, - fromSeverity = sample.FromSeverity, - toSeverity = sample.ToSeverity - }).ToArray() - }; - WriteJson(dto); - return; - } - - var title = language.Value == "tr" ? "degisiklik" : "change"; - Console.WriteLine($"{title}: +{report.AddedCount} -{report.RemovedCount} ~{report.SeverityChangedCount} (unchanged {report.UnchangedCount})"); - } - - private static int ParsePositiveInt(string[] args, string name, int defaultValue) - { - var raw = GetOption(args, name); - if (string.IsNullOrWhiteSpace(raw)) - { - return defaultValue; - } - if (!int.TryParse(raw, out var value)) - { - throw new ArgumentException($"{name} must be a positive integer."); - } - if (value <= 0 && name != "--max-runtime-ms") - { - throw new ArgumentException($"{name} must be a positive integer."); - } - if (value < 0) - { - throw new ArgumentException($"{name} must not be negative."); - } - return value; - } - - private static int RunUnknown(string command, LanguageCode language, ITextProvider textProvider) - { - Console.Error.WriteLine($"{textProvider.Get("unknownCommand", language)}: {command}"); - RunHelp(language, textProvider); - return ExitError; - } - - private static void PrintScan(ScanResult scan, LanguageCode language, Services services) - { - Console.WriteLine(services.TextProvider.Get("scanSummary", language)); - Console.WriteLine($"{services.TextProvider.Get("repository", language)}: {scan.RepositoryPath}"); - Console.WriteLine($"{services.TextProvider.Get("files", language)}: {scan.Files.Count}"); - - Console.WriteLine(); - Console.WriteLine(services.TextProvider.Get("stacks", language)); - if (scan.Stacks.Count == 0) - { - Console.WriteLine($"- {services.TextProvider.Get("unknown", language)}"); - } - else - { - foreach (var stack in scan.Stacks) - { - Console.WriteLine($"- {stack.Name}: {stack.Signal}"); - } - } - - Console.WriteLine(); - Console.WriteLine(services.TextProvider.Get("repositoryHealth", language)); - Console.WriteLine($"- README: {YesNo(scan.HasReadme, language, services.TextProvider)}"); - Console.WriteLine($"- LICENSE: {YesNo(scan.HasLicense, language, services.TextProvider)}"); - Console.WriteLine($"- SECURITY: {YesNo(scan.HasSecurityPolicy, language, services.TextProvider)}"); - Console.WriteLine($"- {services.TextProvider.Get("tests", language)}: {YesNo(scan.HasTests, language, services.TextProvider)}"); - Console.WriteLine($"- CI: {YesNo(scan.HasCi, language, services.TextProvider)}"); - Console.WriteLine($"- Docker: {YesNo(scan.HasDocker, language, services.TextProvider)}"); - Console.WriteLine($"- {services.TextProvider.Get("agentInstructions", language)}: {YesNo(scan.HasAgentInstructions, language, services.TextProvider)}"); - - Console.WriteLine(); - PrintFindings(scan.Findings, language, services); - PrintSuppressions(scan.Suppressions, language, services.TextProvider); - } - - private static void PrintBaselineClassification( - string baselinePath, - BaselineEvaluation baseline, - LanguageCode language, - ITextProvider textProvider) - { - Console.WriteLine(); - Console.WriteLine(textProvider.Get("baselineClassification", language)); - Console.WriteLine($"- {textProvider.Get("file", language)}: {baselinePath}"); - Console.WriteLine($"- {textProvider.Get("existingFindings", language)}: {baseline.Existing.Count}"); - Console.WriteLine($"- {textProvider.Get("newFindings", language)}: {baseline.New.Count}"); - foreach (var finding in baseline.Findings.Take(25)) - { - Console.WriteLine($"- {finding.Status}: {finding.Finding.Path} {RiskRuleCatalog.GetRuleId(finding.Finding)} [{finding.Finding.Severity}] {textProvider.Get("occurrence", language)} {finding.Occurrence}"); - } - - if (baseline.Findings.Count > 25) - { - Console.WriteLine($"- ... {baseline.Findings.Count - 25} {textProvider.Get("more", language)}"); - } - } - - private static void PrintSuppressions( - IReadOnlyList<RiskSuppression> suppressions, - LanguageCode language, - ITextProvider textProvider) - { - if (suppressions.Count == 0) - { - return; - } - - Console.WriteLine(); - Console.WriteLine($"{textProvider.Get("suppressedFindings", language)}: {suppressions.Count}"); - foreach (var suppression in suppressions.Take(25)) - { - Console.WriteLine($"- {suppression.Path}: {suppression.RuleId} [{suppression.Severity}/{suppression.Category}] {textProvider.Get("via", language)} {ToSuppressionReason(suppression.Reason)}"); - } - - if (suppressions.Count > 25) - { - Console.WriteLine($"- ... {suppressions.Count - 25} {textProvider.Get("more", language)}"); - } - } - - private static void PrintFindings(IReadOnlyList<RiskFinding> findings, LanguageCode language, Services services) - { - if (findings.Count == 0) - { - Console.WriteLine(services.TextProvider.Get("noFindings", language)); - return; - } - - foreach (var severity in new[] { RiskSeverity.Critical, RiskSeverity.High, RiskSeverity.Medium, RiskSeverity.Low, RiskSeverity.Info }) - { - var group = findings.Where(finding => finding.Severity == severity).Take(25).ToArray(); - if (group.Length == 0) - { - continue; - } - - Console.WriteLine($"{severity}:"); - foreach (var finding in group) - { - Console.WriteLine($"- {finding.Path}: {finding.Message}"); - } - - var omitted = findings.Count(finding => finding.Severity == severity) - group.Length; - if (omitted > 0) - { - Console.WriteLine($"- ... {omitted} more"); - } - } - } - - private static void PrintGeneratedResult(GeneratedFileResult result, ITextProvider textProvider, LanguageCode language) - { - var status = result.Created - ? textProvider.Get("created", language) - : textProvider.Get("skipped", language); - - Console.WriteLine($"- {result.Path}: {status}"); - } - - private static void WriteJson(object value) - { - Console.WriteLine(JsonSerializer.Serialize(value, new JsonSerializerOptions - { - WriteIndented = true - })); - } - - private static object ToScanDto( - string command, - ScanResult scan, - DateTimeOffset generatedAtUtc, - bool ciMode, - int exitCode, - string? baselinePath = null, - BaselineEvaluation? baseline = null) - { - var result = new Dictionary<string, object?> - { - ["schemaVersion"] = JsonSchemaVersion, - ["toolVersion"] = Version, - ["generatedAtUtc"] = generatedAtUtc, - ["command"] = command, - ["ciMode"] = ciMode, - ["exitCode"] = exitCode, - ["repositoryPath"] = scan.RepositoryPath, - ["repositoryName"] = GetRepositoryName(scan.RepositoryPath), - ["fileCount"] = scan.Files.Count, - ["stacks"] = scan.Stacks.Select(stack => new - { - name = stack.Name, - signal = stack.Signal - }).ToArray(), - ["health"] = new - { - hasReadme = scan.HasReadme, - hasLicense = scan.HasLicense, - hasSecurityPolicy = scan.HasSecurityPolicy, - hasContributing = scan.HasContributing, - hasCodeOfConduct = scan.HasCodeOfConduct, - hasChangelog = scan.HasChangelog, - hasTests = scan.HasTests, - hasCi = scan.HasCi, - hasDocker = scan.HasDocker, - hasAgentInstructions = scan.HasAgentInstructions - }, - ["riskSummary"] = ToRiskSummary(scan.Findings), - ["findings"] = scan.Findings.Select(ToRiskFindingDto).ToArray(), - ["suppressionSummary"] = new - { - total = scan.Suppressions.Count, - safeDomains = scan.Suppressions.Count(suppression => suppression.Reason == RiskSuppressionReason.SafeDomain), - ignoredPaths = scan.Suppressions.Count(suppression => suppression.Reason == RiskSuppressionReason.IgnoredPath), - ignoredFindingIds = scan.Suppressions.Count(suppression => suppression.Reason == RiskSuppressionReason.IgnoredFindingId) - }, - ["suppressions"] = scan.Suppressions.Select(suppression => new - { - ruleId = suppression.RuleId, - severity = suppression.Severity.ToString(), - category = suppression.Category.ToString(), - path = suppression.Path, - reason = ToSuppressionReason(suppression.Reason) - }).ToArray() - }; - - AddBaselineDto(result, baselinePath, baseline); - - return result; - } - - private static void AddBaselineDto( - IDictionary<string, object?> result, - string? baselinePath, - BaselineEvaluation? baseline) - { - if (baseline is null) - { - return; - } - - result["baseline"] = new - { - path = baselinePath, - schemaVersion = BaselineSchema.CurrentVersion, - fingerprintAlgorithm = BaselineSchema.FingerprintAlgorithm, - entryCount = baseline.BaselineEntryCount, - existing = baseline.Existing.Count, - @new = baseline.New.Count, - classifiedFindings = baseline.Findings.Select(finding => new - { - ruleId = RiskRuleCatalog.GetRuleId(finding.Finding), - severity = finding.Finding.Severity.ToString(), - path = finding.Finding.Path, - fingerprint = finding.Fingerprint, - status = finding.Status.ToString().ToLowerInvariant(), - occurrence = finding.Occurrence - }).ToArray() - }; - } - - private static string ToSuppressionReason(RiskSuppressionReason reason) - { - return reason switch - { - RiskSuppressionReason.SafeDomain => "safeDomains", - RiskSuppressionReason.IgnoredPath => "ignoredPaths", - RiskSuppressionReason.IgnoredFindingId => "ignoredFindingIds", - _ => "unknown" - }; - } - - private static object ToRiskFindingDto(RiskFinding finding) - { - return new - { - ruleId = RiskRuleCatalog.GetRuleId(finding), - severity = finding.Severity.ToString(), - category = finding.Category.ToString(), - path = finding.Path, - message = finding.Message, - match = (string?)null - }; - } - - private static object ToDoctorCheckDto(DoctorCheck check) - { - return new - { - name = check.Name, - severity = check.Severity.ToString(), - passed = check.Passed, - message = check.Message - }; - } - - private static object ToConfigDiagnosticDto(ConfigDiagnostic diagnostic) - { - return new - { - code = diagnostic.Code, - severity = diagnostic.Severity.ToString(), - line = diagnostic.Line, - key = diagnostic.Key, - message = diagnostic.Message - }; - } - - private static object ToGeneratedFileDto(GeneratedFileResult result) - { - return new - { - path = result.Path, - status = result.Status.ToString(), - created = result.Created, - message = result.Message - }; - } - - private static object ToRiskSummary(IReadOnlyList<RiskFinding> findings) - { - return new - { - total = findings.Count, - critical = findings.Count(finding => finding.Severity == RiskSeverity.Critical), - high = findings.Count(finding => finding.Severity == RiskSeverity.High), - medium = findings.Count(finding => finding.Severity == RiskSeverity.Medium), - low = findings.Count(finding => finding.Severity == RiskSeverity.Low), - info = findings.Count(finding => finding.Severity == RiskSeverity.Info) - }; - } - - private static object ToDoctorCheckSummary(IReadOnlyList<DoctorCheck> checks) - { - return new - { - total = checks.Count, - passed = checks.Count(check => check.Passed), - failed = checks.Count(check => !check.Passed), - failedHighOrCritical = checks.Count(check => !check.Passed && check.Severity >= RiskSeverity.High) - }; - } - - private static object ToGeneratedFileSummary(IReadOnlyList<GeneratedFileResult> results) - { - return new - { - total = results.Count, - created = results.Count(result => result.Created), - skipped = results.Count(result => !result.Created) - }; - } - - private static string GetRepositoryName(string repositoryPath) - { - var trimmed = repositoryPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - return Path.GetFileName(trimmed); - } - - private static string YesNo(bool value, LanguageCode language, ITextProvider textProvider) - { - return textProvider.Get(value ? "yes" : "no", language); - } - - private static int GetScanExitCode(ScanResult scan, bool ci) - { - if (!ci) - { - return ExitSuccess; - } - - if (scan.Findings.Any(finding => finding.Severity == RiskSeverity.Critical)) - { - return ExitCritical; - } - - if (scan.Findings.Any(finding => finding.Severity == RiskSeverity.High)) - { - return ExitError; - } - - return ExitSuccess; - } - - private static int GetBaselineScanExitCode(BaselineEvaluation baseline, bool ci) - { - if (!ci) - { - return ExitSuccess; - } - - if (baseline.New.Any(finding => finding.Finding.Severity == RiskSeverity.Critical)) - { - return ExitCritical; - } - - if (baseline.New.Any(finding => finding.Finding.Severity == RiskSeverity.High)) - { - return ExitError; - } - - return ExitSuccess; - } - - private static int WriteBaselineError(string command, BaselineException exception, bool json, DateTimeOffset generatedAtUtc) - { - if (json) - { - WriteJson(new - { - schemaVersion = JsonSchemaVersion, - toolVersion = Version, - generatedAtUtc, - command, - exitCode = ExitError, - error = new - { - code = exception.Code, - message = exception.Message - } - }); - return ExitError; - } - - Console.Error.WriteLine($"{exception.Code}: {exception.Message}"); - return ExitError; - } - - private static (string? Path, BaselineEvaluation? Evaluation) LoadBaseline( - string[] args, - string repositoryPath, - ScanResult scan, - Services services) - { - var requestedPath = GetOption(args, "--baseline"); - if (string.IsNullOrWhiteSpace(requestedPath)) - { - return (null, null); - } - - var manifest = services.BaselineStore.Load(repositoryPath, requestedPath); - var normalizedPath = BaselineFingerprint.NormalizeRelativePath(requestedPath); - return (normalizedPath, services.BaselineClassifier.Classify(scan.Findings, manifest)); - } - - private static AgentTarget ParseTarget(string? value) - { - return value?.Trim().ToLowerInvariant() switch - { - "codex" => AgentTarget.Codex, - "claude" => AgentTarget.Claude, - "anthropic" => AgentTarget.Anthropic, - "cursor" => AgentTarget.Cursor, - "copilot" => AgentTarget.Copilot, - "continue" => AgentTarget.Continue, - "all" or null or "" => AgentTarget.All, - _ => AgentTarget.All - }; - } - - private static AgentTarget ParseHookTarget(string? value) - { - return value?.Trim().ToLowerInvariant() switch - { - "claude" => AgentTarget.Claude, - "anthropic" => AgentTarget.Anthropic, - "continue" => AgentTarget.Continue, - "all" => AgentTarget.All, - "codex" or null or "" => AgentTarget.Codex, - _ => AgentTarget.Codex - }; - } - - private static bool CommandExistsOnPath(string command) - { - var path = Environment.GetEnvironmentVariable("PATH"); - if (string.IsNullOrWhiteSpace(path)) - { - return false; - } - - var extensions = OperatingSystem.IsWindows() - ? new[] { ".exe", ".cmd", ".bat", string.Empty } - : new[] { string.Empty }; - - foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) - { - foreach (var extension in extensions) - { - var candidate = Path.Combine(directory.Trim(), command + extension); - if (File.Exists(candidate)) - { - return true; - } - } - } - - return false; - } - - private static string? GetOption(string[] args, string name) - { - for (var index = 0; index < args.Length; index++) - { - var current = args[index]; - if (current.StartsWith(name + "=", StringComparison.OrdinalIgnoreCase)) - { - return current[(name.Length + 1)..]; - } - - if (string.Equals(current, name, StringComparison.OrdinalIgnoreCase) && index + 1 < args.Length) - { - return args[index + 1]; - } - } - - return null; - } - - private static bool HasFlag(string[] args, string name) - { - return args.Any(arg => string.Equals(arg, name, StringComparison.OrdinalIgnoreCase)); - } - - private static string GetTaskTitle(string[] args) - { - var parts = new List<string>(); - - for (var index = 1; index < args.Length; index++) - { - var current = args[index]; - if (current.StartsWith("--", StringComparison.Ordinal)) - { - if (OptionConsumesValue(current) && !current.Contains('=', StringComparison.Ordinal) && index + 1 < args.Length) - { - index++; - } - - continue; - } - - parts.Add(current); - } - - return string.Join(' ', parts).Trim(); - } - - private static bool OptionConsumesValue(string option) - { - return string.Equals(option, "--lang", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--target", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--profile", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--baseline", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--prompt-pack", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--stdio", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--output", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--include", StringComparison.OrdinalIgnoreCase) || - string.Equals(option, "--exclude", StringComparison.OrdinalIgnoreCase); - } - - private static Services CreateServices() - { - var fileSystem = new PhysicalFileSystem(); - var secretScanner = new SecretScanner(); - var brandPiiScanner = new BrandPiiScanner(); - var riskScanner = new RiskScanner(fileSystem, secretScanner, brandPiiScanner); - var stackDetector = new StackDetector(fileSystem); - var repositoryScanner = new RepositoryScanner(fileSystem, stackDetector, riskScanner); - var templateRenderer = new TemplateRenderer(); - var textProvider = new TextProvider(); - var clock = new SystemClock(); - var configReader = new AckitConfigReader(fileSystem); - var doctor = new RepositoryDoctor(fileSystem); - var mcpServer = new McpRouter( - fileSystem, - configReader, - repositoryScanner, - new RepositoryDoctorHealthProbe(doctor), - Version); - - return new Services( - fileSystem, - configReader, - new AckitConfigValidator(), - new AckitConfigWriter(fileSystem), - new BaselineStore(fileSystem), - new BaselineClassifier(), - repositoryScanner, - new AgentInstructionGenerator(fileSystem, templateRenderer, clock), - new HtmlReportGenerator(fileSystem, clock), - new WebUiGenerator(fileSystem, clock), - new PromptPackGenerator(fileSystem, clock), - new ContextExportManifestGenerator(fileSystem, clock), - new SarifReportWriter(fileSystem), - new TaskFileGenerator(fileSystem, templateRenderer), - doctor, - mcpServer, - clock, - textProvider); - } - - private sealed record Services( - IFileSystem FileSystem, - IAckitConfigReader ConfigReader, - IAckitConfigValidator ConfigValidator, - IAckitConfigWriter ConfigWriter, - IBaselineStore BaselineStore, - IBaselineClassifier BaselineClassifier, - IRepositoryScanner RepositoryScanner, - IAgentInstructionGenerator AgentInstructionGenerator, - IHtmlReportGenerator HtmlReportGenerator, - IWebUiGenerator WebUiGenerator, - IPromptPackGenerator PromptPackGenerator, - IContextExportManifestGenerator ContextExportManifestGenerator, - ISarifReportWriter SarifReportWriter, - ITaskFileGenerator TaskFileGenerator, - RepositoryDoctor Doctor, - IMcpServer McpServer, - IClock Clock, - ITextProvider TextProvider); -} From 3447870e25d2cce1eaf0914e8bb889b467db5f8f Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:00:05 +0300 Subject: [PATCH 04/20] restore --- src/AgentContextKit.Cli/Program.cs | 1927 +++++++++++++++++++++++++++- 1 file changed, 1925 insertions(+), 2 deletions(-) diff --git a/src/AgentContextKit.Cli/Program.cs b/src/AgentContextKit.Cli/Program.cs index d7e9878..2755799 100644 --- a/src/AgentContextKit.Cli/Program.cs +++ b/src/AgentContextKit.Cli/Program.cs @@ -5,7 +5,7 @@ namespace AgentContextKit.Cli; public static class Program { - private const string Version = "0.2.0-alpha.4"; + private const string Version = "0.2.0-alpha.3"; private const string DefaultBaselinePath = ".ackit-baseline.json"; private const int JsonSchemaVersion = 2; private const int ExitSuccess = 0; @@ -38,7 +38,7 @@ public static int Main(string[] args) "webui" => RunWebUi(args, repositoryPath, config, language, json, services), "prompt-pack" => RunPromptPack(args, repositoryPath, config, language, json, services), "context-export" => RunContextExport(args, repositoryPath, config, language, json, services), - "generate" => RunGenerate(args, repositoryPath, language, json, services), + "generate" => RunGenerate(args, repositoryPath, config, language, json, services), "task" => RunTask(args, repositoryPath, language, json, services), "redact-check" => RunRedactCheck(args, repositoryPath, config, language, json, services), "doctor" => RunDoctor(repositoryPath, config, language, json, services), @@ -58,3 +58,1926 @@ public static int Main(string[] args) return ExitError; } } + + private static int RunHelp(LanguageCode language, ITextProvider textProvider) + { + Console.WriteLine(textProvider.Get("help", language)); + Console.WriteLine(); + Console.WriteLine(textProvider.Get("usage", language)); + Console.WriteLine(" ackit init [--lang en|tr] [--json]"); + Console.WriteLine(" ackit config-check [--lang en|tr] [--json]"); + Console.WriteLine(" ackit scan [--baseline <repo-relative.json>] [--include <glob>] [--exclude <glob>] [--lang en|tr] [--json] [--ci]"); + Console.WriteLine(" ackit baseline [--output <repo-relative.json>] [--update] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit sarif --output <repo-relative.sarif> [--baseline <repo-relative.json>] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit report [--output <repo-relative.html>] [--baseline <repo-relative.json>] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit webui [--output <repo-relative.html>] [--baseline <repo-relative.json>] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit prompt-pack [--output <repo-relative.md>] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit context-export --prompt-pack <repo-relative.md> --approve [--output <repo-relative.json>] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit generate [--target codex|claude|anthropic|cursor|copilot|continue|all] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit task \"<title>\" [--lang en|tr] [--json]"); + Console.WriteLine(" ackit redact-check [--profile public-release] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit doctor [--lang en|tr] [--json]"); + Console.WriteLine(" ackit hooks [--target codex|claude|anthropic|continue] [--shell pwsh|sh] [--install|--dry-run] [--output <repo-relative-dir>] [--lang en|tr] [--json]"); + Console.WriteLine(" ackit mcp --stdio-server [--repo <path>] [--lang en|tr]"); + Console.WriteLine(" ackit mcp --stdio <json-request> [--output <repo-relative.jsonl>] [--lang en|tr]"); + Console.WriteLine(" ackit diff --from <from.json> --to <to.json> [--lang en|tr] [--json]"); + Console.WriteLine(" ackit trim --input <repo-relative.md|json> --output <repo-relative.md|json> --max-chars <N> [--lang en|tr] [--json]"); + Console.WriteLine(" ackit watch [--debounce-ms <N>] [--once] [--max-runtime-ms <N>] [--json] [--lang en|tr]"); + Console.WriteLine(" ackit version"); + return ExitSuccess; + } + + private static int RunVersion() + { + Console.WriteLine($"AgentContextKit {Version}"); + return ExitSuccess; + } + + private static int RunInit(string repositoryPath, LanguageCode language, bool json, Services services) + { + var result = services.ConfigWriter.WriteDefaultIfMissing(repositoryPath, language); + var agentFiles = new[] { "AGENTS.md", "CLAUDE.md", ".cursor/rules/project.mdc", ".github/copilot-instructions.md" } + .Select(file => + { + var fullPath = Path.Combine(repositoryPath, file.Replace('/', Path.DirectorySeparatorChar)); + return new + { + path = file, + exists = services.FileSystem.FileExists(fullPath) + }; + }) + .ToArray(); + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "init", + config = ToGeneratedFileDto(result), + agentInstructionFiles = agentFiles + }); + return ExitSuccess; + } + + PrintGeneratedResult(result, services.TextProvider, language); + + Console.WriteLine(); + Console.WriteLine(services.TextProvider.Get("detectedAgentInstructionFiles", language)); + foreach (var file in agentFiles) + { + var status = services.TextProvider.Get(file.exists ? "found" : "missing", language); + Console.WriteLine($"- {file.path}: {status}"); + } + + return ExitSuccess; + } + + private static int RunConfigCheck( + string repositoryPath, + LanguageCode language, + bool json, + Services services) + { + const string relativePath = ".ackit/config.yml"; + var fullPath = Path.Combine(repositoryPath, ".ackit", "config.yml"); + var exists = services.FileSystem.FileExists(fullPath); + var result = exists + ? services.ConfigValidator.Validate(services.FileSystem.ReadAllText(fullPath)) + : new ConfigValidationResult(Array.Empty<ConfigDiagnostic>()); + var errorCount = result.Diagnostics.Count(diagnostic => diagnostic.Severity == ConfigDiagnosticSeverity.Error); + var warningCount = result.Diagnostics.Count(diagnostic => diagnostic.Severity == ConfigDiagnosticSeverity.Warning); + var infoCount = result.Diagnostics.Count(diagnostic => diagnostic.Severity == ConfigDiagnosticSeverity.Info); + var migrationRequired = result.Diagnostics.Any(diagnostic => + diagnostic.Code is ConfigDiagnosticCodes.ObsoleteKey or ConfigDiagnosticCodes.InvalidSchemaVersion); + var status = !exists + ? "default" + : errorCount > 0 + ? "errors" + : warningCount > 0 + ? "warnings" + : "valid"; + var exitCode = errorCount > 0 ? ExitError : ExitSuccess; + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "config-check", + repositoryName = GetRepositoryName(repositoryPath), + exitCode, + config = new + { + path = relativePath, + exists, + status, + supportedSchemaVersion = AckitConfig.Default.SchemaVersion, + migrationRequired + }, + diagnosticSummary = new + { + total = result.Diagnostics.Count, + info = infoCount, + warnings = warningCount, + errors = errorCount + }, + diagnostics = result.Diagnostics.Select(ToConfigDiagnosticDto).ToArray() + }); + return exitCode; + } + + var turkish = string.Equals(language.Value, LanguageCode.Turkish.Value, StringComparison.OrdinalIgnoreCase); + Console.WriteLine(turkish ? "Yapılandırma kontrolü" : "Configuration check"); + Console.WriteLine($"- {(turkish ? "Yol" : "Path")}: {relativePath}"); + Console.WriteLine($"- {(turkish ? "Durum" : "Status")}: {ToConfigStatusLabel(status, turkish)}"); + Console.WriteLine(turkish + ? $"- Tanılar: {result.Diagnostics.Count} ({errorCount} hata, {warningCount} uyarı)" + : $"- Diagnostics: {result.Diagnostics.Count} ({errorCount} errors, {warningCount} warnings)"); + + if (!exists) + { + Console.WriteLine(turkish + ? "Yapılandırma dosyası yok; varsayılan ayarlar geçerli." + : "Configuration file is missing; defaults are valid."); + return exitCode; + } + + if (result.Diagnostics.Count == 0) + { + Console.WriteLine(turkish ? "Yapılandırma tanısı yok." : "No configuration diagnostics."); + return exitCode; + } + + foreach (var diagnostic in result.Diagnostics) + { + var key = diagnostic.Key is null + ? "" + : turkish + ? $" anahtar {diagnostic.Key}" + : $" key {diagnostic.Key}"; + var severity = turkish ? ToTurkishConfigSeverity(diagnostic.Severity) : diagnostic.Severity.ToString(); + var line = turkish ? "satır" : "line"; + Console.WriteLine($"- {severity} {diagnostic.Code} {line} {diagnostic.Line}{key}: {diagnostic.Message}"); + } + + if (migrationRequired) + { + Console.WriteLine(turkish + ? "Geçiş incelemesi gerekli; dosya otomatik olarak değiştirilmedi." + : "Migration review is required; the file was not changed automatically."); + } + + return exitCode; + } + + private static string ToConfigStatusLabel(string status, bool turkish) + { + if (!turkish) + { + return status; + } + + return status switch + { + "default" => "varsayılan", + "valid" => "geçerli", + "warnings" => "uyarılar", + "errors" => "hatalar", + _ => status + }; + } + + private static string ToTurkishConfigSeverity(ConfigDiagnosticSeverity severity) + { + return severity switch + { + ConfigDiagnosticSeverity.Info => "Bilgi", + ConfigDiagnosticSeverity.Warning => "Uyarı", + ConfigDiagnosticSeverity.Error => "Hata", + _ => severity.ToString() + }; + } + + private static int RunScan(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, bool ci, Services services) + { + IReadOnlyList<string> includeGlobs; + IReadOnlyList<string> excludeGlobs; + try + { + includeGlobs = ParseGlobList(args, "--include"); + excludeGlobs = ParseGlobList(args, "--exclude"); + } + catch (ArgumentException ex) + { + return WriteInvalidArgumentError("scan", ex.Message, json, language, services); + } + + var scan = services.RepositoryScanner.Scan(repositoryPath, config, includeGlobs, excludeGlobs); + string? baselinePath; + BaselineEvaluation? baseline; + try + { + (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); + } + catch (BaselineException ex) + { + return WriteBaselineError("scan", ex, json, services.Clock.UtcNow); + } + + var exitCode = baseline is null + ? GetScanExitCode(scan, ci) + : GetBaselineScanExitCode(baseline, ci); + if (json) + { + WriteJson(ToScanDto("scan", scan, services.Clock.UtcNow, ci, exitCode, baselinePath, baseline)); + return exitCode; + } + + PrintScan(scan, language, services); + if (baseline is not null) + { + PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); + } + + return exitCode; + } + + private static IReadOnlyList<string> ParseGlobList(string[] args, string name) + { + var values = new List<string>(); + for (var index = 0; index < args.Length; index++) + { + var current = args[index]; + string? value = null; + if (current.StartsWith(name + "=", StringComparison.OrdinalIgnoreCase)) + { + value = current[(name.Length + 1)..]; + } + else if (string.Equals(current, name, StringComparison.OrdinalIgnoreCase) && index + 1 < args.Length) + { + value = args[index + 1]; + index++; + } + + if (value is null) + { + continue; + } + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException($"{name} glob must not be empty or whitespace."); + } + + values.Add(value.Trim()); + } + + return values; + } + + private static int WriteInvalidArgumentError(string command, string message, bool json, LanguageCode language, Services services) + { + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command, + error = "InvalidArgument", + message + }); + } + else + { + var localized = services.TextProvider.Get("invalidArgument", language); + Console.Error.WriteLine($"{localized}: {message}"); + } + + return ExitError; + } + + private static int RunBaseline( + string[] args, + string repositoryPath, + AckitConfig config, + LanguageCode language, + bool json, + Services services) + { + var outputPath = GetOption(args, "--output") ?? DefaultBaselinePath; + try + { + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + var manifest = services.BaselineClassifier.CreateManifest(scan.Findings); + var result = services.BaselineStore.Write(repositoryPath, outputPath, manifest, HasFlag(args, "--update")); + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "baseline", + repositoryName = GetRepositoryName(repositoryPath), + baseline = new + { + path = result.Path, + status = result.Status.ToString(), + schemaVersion = manifest.SchemaVersion, + fingerprintAlgorithm = manifest.FingerprintAlgorithm, + entryCount = result.EntryCount + } + }); + return ExitSuccess; + } + + var statusKey = result.Status == BaselineFileStatus.Created ? "baselineCreated" : "baselineUpdated"; + Console.WriteLine($"{services.TextProvider.Get(statusKey, language)}: {result.Path}"); + Console.WriteLine($"{services.TextProvider.Get("entries", language)}: {result.EntryCount}"); + Console.WriteLine(services.TextProvider.Get("baselineReview", language)); + return ExitSuccess; + } + catch (BaselineException ex) + { + return WriteBaselineError("baseline", ex, json, services.Clock.UtcNow); + } + } + + private static int RunSarif(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + var outputPath = GetOption(args, "--output"); + if (string.IsNullOrWhiteSpace(outputPath)) + { + Console.Error.WriteLine(services.TextProvider.Get("sarifRequiresOutput", language)); + return ExitError; + } + + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + string? baselinePath; + BaselineEvaluation? baseline; + try + { + (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); + } + catch (BaselineException ex) + { + return WriteBaselineError("sarif", ex, json, services.Clock.UtcNow); + } + + var result = services.SarifReportWriter.Generate(repositoryPath, outputPath, scan, Version, baseline); + var criticalHighCount = scan.Findings.Count(finding => finding.Severity is RiskSeverity.Critical or RiskSeverity.High); + + if (json) + { + var response = new Dictionary<string, object?> + { + ["schemaVersion"] = JsonSchemaVersion, + ["toolVersion"] = Version, + ["generatedAtUtc"] = services.Clock.UtcNow, + ["command"] = "sarif", + ["repositoryName"] = GetRepositoryName(repositoryPath), + ["riskSummary"] = ToRiskSummary(scan.Findings), + ["criticalHighCount"] = criticalHighCount, + ["sarif"] = ToGeneratedFileDto(result) + }; + AddBaselineDto(response, baselinePath, baseline); + WriteJson(response); + return ExitSuccess; + } + + PrintGeneratedResult(result, services.TextProvider, language); + Console.WriteLine($"{services.TextProvider.Get("sarifFindings", language)}: {scan.Findings.Count}"); + Console.WriteLine($"{services.TextProvider.Get("criticalHighFindings", language)}: {criticalHighCount}"); + if (baseline is not null) + { + PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); + } + return ExitSuccess; + } + + private static int RunReport(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + var outputPath = GetOption(args, "--output"); + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + string? baselinePath; + BaselineEvaluation? baseline; + try + { + (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); + } + catch (BaselineException ex) + { + return WriteBaselineError("report", ex, json, services.Clock.UtcNow); + } + + var result = services.HtmlReportGenerator.Generate(repositoryPath, outputPath, language, scan, baseline); + + if (json) + { + var response = new Dictionary<string, object?> + { + ["schemaVersion"] = JsonSchemaVersion, + ["toolVersion"] = Version, + ["generatedAtUtc"] = services.Clock.UtcNow, + ["command"] = "report", + ["repositoryPath"] = repositoryPath, + ["repositoryName"] = GetRepositoryName(repositoryPath), + ["riskSummary"] = ToRiskSummary(scan.Findings), + ["report"] = ToGeneratedFileDto(result) + }; + AddBaselineDto(response, baselinePath, baseline); + WriteJson(response); + return ExitSuccess; + } + + PrintGeneratedResult(result, services.TextProvider, language); + Console.WriteLine($"{services.TextProvider.Get("riskFindings", language)}: {scan.Findings.Count}"); + if (baseline is not null) + { + PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); + } + return ExitSuccess; + } + + private static int RunWebUi(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + var outputPath = GetOption(args, "--output"); + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + string? baselinePath; + BaselineEvaluation? baseline; + try + { + (baselinePath, baseline) = LoadBaseline(args, repositoryPath, scan, services); + } + catch (BaselineException ex) + { + return WriteBaselineError("webui", ex, json, services.Clock.UtcNow); + } + + var result = services.WebUiGenerator.Generate(repositoryPath, outputPath, language, scan, baseline); + + if (json) + { + var response = new Dictionary<string, object?> + { + ["schemaVersion"] = JsonSchemaVersion, + ["toolVersion"] = Version, + ["generatedAtUtc"] = services.Clock.UtcNow, + ["command"] = "webui", + ["repositoryPath"] = repositoryPath, + ["repositoryName"] = GetRepositoryName(repositoryPath), + ["riskSummary"] = ToRiskSummary(scan.Findings), + ["webUi"] = ToGeneratedFileDto(result) + }; + AddBaselineDto(response, baselinePath, baseline); + WriteJson(response); + return ExitSuccess; + } + + PrintGeneratedResult(result, services.TextProvider, language); + Console.WriteLine($"{services.TextProvider.Get("riskFindings", language)}: {scan.Findings.Count}"); + if (baseline is not null) + { + PrintBaselineClassification(baselinePath!, baseline, language, services.TextProvider); + } + return ExitSuccess; + } + + private static int RunPromptPack(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + var outputPath = GetOption(args, "--output"); + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + var result = services.PromptPackGenerator.Generate(repositoryPath, outputPath, language, scan); + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "prompt-pack", + repositoryPath, + repositoryName = GetRepositoryName(repositoryPath), + riskSummary = ToRiskSummary(scan.Findings), + promptPack = ToGeneratedFileDto(result) + }); + return ExitSuccess; + } + + PrintGeneratedResult(result, services.TextProvider, language); + Console.WriteLine(services.TextProvider.Get("noRemoteCall", language)); + Console.WriteLine($"{services.TextProvider.Get("riskFindings", language)}: {scan.Findings.Count}"); + return ExitSuccess; + } + + private static int RunContextExport(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + if (!HasFlag(args, "--approve")) + { + Console.Error.WriteLine(services.TextProvider.Get("contextExportRequiresApproval", language)); + return ExitError; + } + + var promptPackPath = GetOption(args, "--prompt-pack"); + if (string.IsNullOrWhiteSpace(promptPackPath)) + { + Console.Error.WriteLine(services.TextProvider.Get("contextExportRequiresPromptPack", language)); + return ExitError; + } + + var outputPath = GetOption(args, "--output"); + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + var result = services.ContextExportManifestGenerator.Generate( + repositoryPath, + new ContextExportSpec(promptPackPath, outputPath, "explicit-cli-flag", language), + scan); + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "context-export", + repositoryPath, + repositoryName = GetRepositoryName(repositoryPath), + riskSummary = ToRiskSummary(scan.Findings), + contextExport = ToGeneratedFileDto(result) + }); + return ExitSuccess; + } + + PrintGeneratedResult(result, services.TextProvider, language); + Console.WriteLine(services.TextProvider.Get("noRemoteCall", language)); + Console.WriteLine(services.TextProvider.Get("approvalRecorded", language)); + return ExitSuccess; + } + + private static int RunGenerate(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + var target = ParseTarget(GetOption(args, "--target")); + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + var results = services.AgentInstructionGenerator.Generate(repositoryPath, target, language, scan); + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "generate", + target = target.ToString(), + fileSummary = ToGeneratedFileSummary(results), + files = results.Select(ToGeneratedFileDto).ToArray() + }); + return ExitSuccess; + } + + foreach (var result in results) + { + PrintGeneratedResult(result, services.TextProvider, language); + } + + return ExitSuccess; + } + + private static int RunTask(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) + { + var title = GetTaskTitle(args); + if (string.IsNullOrWhiteSpace(title)) + { + Console.Error.WriteLine(services.TextProvider.Get("taskRequiresTitle", language)); + Console.Error.WriteLine(services.TextProvider.Get("taskExample", language)); + return ExitError; + } + + var result = services.TaskFileGenerator.CreateTask(repositoryPath, new TaskSpec(title, language)); + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "task", + file = ToGeneratedFileDto(result) + }); + return ExitSuccess; + } + + PrintGeneratedResult(result, services.TextProvider, language); + return ExitSuccess; + } + + private static int RunRedactCheck(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + var profile = GetOption(args, "--profile") ?? "default"; + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + var findings = scan.Findings + .Where(finding => finding.Category is RiskCategory.Secret or RiskCategory.Pii or RiskCategory.Brand or RiskCategory.LocalPath or RiskCategory.ProductionConfig) + .ToArray(); + + var exitCode = findings.Any(finding => finding.Severity == RiskSeverity.Critical) + ? ExitCritical + : findings.Length > 0 ? ExitError : ExitSuccess; + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "redact-check", + repositoryPath, + repositoryName = GetRepositoryName(repositoryPath), + profile, + exitCode, + riskSummary = ToRiskSummary(findings), + findings = findings.Select(ToRiskFindingDto).ToArray() + }); + return exitCode; + } + + PrintFindings(findings, language, services); + return exitCode; + } + + private static int RunDoctor(string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + var scan = services.RepositoryScanner.Scan(repositoryPath, config); + var result = services.Doctor.Check(repositoryPath, scan); + var exitCode = result.Checks.Any(check => !check.Passed && check.Severity >= RiskSeverity.High) + ? ExitError + : ExitSuccess; + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = services.Clock.UtcNow, + command = "doctor", + repositoryPath, + repositoryName = GetRepositoryName(repositoryPath), + exitCode, + checkSummary = ToDoctorCheckSummary(result.Checks), + checks = result.Checks.Select(ToDoctorCheckDto).ToArray() + }); + return exitCode; + } + + Console.WriteLine(services.TextProvider.Get("doctor", language)); + foreach (var check in result.Checks) + { + var status = check.Passed ? "PASS" : "FAIL"; + Console.WriteLine($"- {status} [{check.Severity}] {check.Name}: {check.Message}"); + } + + return exitCode; + } + + private static int RunHooks(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) + { + var install = HasFlag(args, "--install"); + var dryRun = HasFlag(args, "--dry-run"); + var shell = GetOption(args, "--shell")?.Trim().ToLowerInvariant() ?? "sh"; + var output = GetOption(args, "--output"); + var target = ParseHookTarget(GetOption(args, "--target")); + var textProvider = services.TextProvider; + + if (shell is not ("pwsh" or "sh")) + { + var message = $"--shell must be pwsh or sh. Got: {shell}"; + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = message }); + } + else + { + Console.Error.WriteLine(message); + } + + return ExitError; + } + + if (target == AgentTarget.Anthropic && + shell == "sh" && + OperatingSystem.IsWindows() && + !CommandExistsOnPath("sh")) + { + const string message = "--target anthropic with --shell sh requires sh to be available on PATH on Windows. Use --shell pwsh or install sh."; + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = message }); + } + else + { + Console.Error.WriteLine(message); + } + + return ExitError; + } + + var targets = HookFileBuilder.Build(target, shell); + var isGitRepo = Directory.Exists(Path.Combine(repositoryPath, ".git")); + if (!isGitRepo && output is null && targets.Any(file => file.Path.StartsWith(".git/hooks/", StringComparison.Ordinal))) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = textProvider.Get("hooksNotGitRepo", language) }); + } + else + { + Console.Error.WriteLine(textProvider.Get("hooksNotGitRepo", language)); + } + return ExitError; + } + + string baseDir; + try + { + baseDir = GetHookBaseDirectory(repositoryPath, output); + } + catch (InvalidOperationException ex) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", exitCode = ExitError, error = ex.Message }); + } + else + { + Console.Error.WriteLine(ex.Message); + } + + return ExitError; + } + + var items = new List<object>(); + foreach (var (relPath, content) in targets) + { + var writePath = GetHookWritePath(output, relPath); + var fullPath = Path.Combine(baseDir, writePath.Replace('/', Path.DirectorySeparatorChar)); + var exists = File.Exists(fullPath); + string status; + if (dryRun) + { + status = textProvider.Get("hooksDryRun", language); + } + else if (install && !exists) + { + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); + File.WriteAllText(fullPath, content); + if (ShouldSetExecutable(shell, relPath) && (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())) + { + try { File.SetUnixFileMode(fullPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherExecute); } catch { } + } + status = textProvider.Get("hooksInstalled", language); + } + else if (exists) + { + status = textProvider.Get("hooksSkipped", language); + } + else + { + status = textProvider.Get("hooksWouldWrite", language); + } + + items.Add(new { path = writePath, status, contentLength = content.Length }); + } + + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, toolVersion = Version, generatedAtUtc = services.Clock.UtcNow, command = "hooks", target = target.ToString(), install, dryRun, shell, mode = dryRun ? "dry-run" : install ? "install" : "preview", exitCode = ExitSuccess, files = items }); + } + else if (dryRun || !install) + { + Console.WriteLine(textProvider.Get("hooksPreview", language)); + foreach (var item in items) + { + var dyn = (dynamic)item; + Console.WriteLine($" {dyn.path}: {dyn.status} ({dyn.contentLength} chars)"); + } + } + else + { + foreach (var item in items) + { + var dyn = (dynamic)item; + Console.WriteLine($" {dyn.path}: {dyn.status} ({dyn.contentLength} chars)"); + } + } + return ExitSuccess; + } + + private static string GetHookBaseDirectory(string repositoryPath, string? output) + { + if (string.IsNullOrWhiteSpace(output)) + { + return repositoryPath; + } + + var normalized = output.Trim().Replace('\\', '/'); + if (Path.IsPathRooted(normalized)) + { + throw new InvalidOperationException("Hooks output path must be repository-relative."); + } + + var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0 || segments.Any(segment => segment is "." or "..")) + { + throw new InvalidOperationException("Hooks output path must stay inside the repository."); + } + + var repositoryFullPath = Path.GetFullPath(repositoryPath); + var outputFullPath = Path.GetFullPath(Path.Combine(repositoryFullPath, normalized.Replace('/', Path.DirectorySeparatorChar))); + var repositoryPrefix = repositoryFullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + + if (!outputFullPath.StartsWith(repositoryPrefix, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("Hooks output path must stay inside the repository."); + } + + return outputFullPath; + } + + private static string GetHookWritePath(string? output, string relativePath) + { + if (output is null) + { + return relativePath; + } + + return relativePath.StartsWith(".git/hooks/", StringComparison.Ordinal) + ? relativePath.Replace(".git/hooks/", string.Empty, StringComparison.Ordinal) + : relativePath; + } + + private static bool ShouldSetExecutable(string shell, string relativePath) + { + return shell == "sh" && + (relativePath.StartsWith(".git/hooks/", StringComparison.Ordinal) || + relativePath.EndsWith(".sh", StringComparison.OrdinalIgnoreCase)); + } + + private static int RunMcp(string[] args, string repositoryPath, LanguageCode language, Services services) + { + if (HasFlag(args, "--help") || HasFlag(args, "-h")) + { + Console.WriteLine("AgentContextKit MCP stdio transport"); + Console.WriteLine(); + Console.WriteLine("Usage:"); + Console.WriteLine(" ackit mcp --stdio-server [--repo <path>] [--lang en|tr]"); + Console.WriteLine(" ackit mcp --stdio <json-request> [--output <repo-relative.jsonl>] [--lang en|tr]"); + Console.WriteLine(" ackit mcp --help"); + Console.WriteLine(); + Console.WriteLine("Modes:"); + Console.WriteLine(" --stdio-server Real stdio loop. Reads JSON-RPC 2.0 line-delimited messages from"); + Console.WriteLine(" Console.In and writes JSON-RPC responses to Console.Out."); + Console.WriteLine(" Diagnostics go to Console.Error. Exits 0 on EOF or"); + Console.WriteLine(" notifications/exit|shutdown. No network, no source mutation."); + Console.WriteLine(" --stdio <json> One-shot JSON-RPC round-trip (test seam; kept for backward"); + Console.WriteLine(" compatibility). Writes the single response to Console.Out or"); + Console.WriteLine(" to the file passed with --output."); + Console.WriteLine(); + Console.WriteLine("Methods:"); + Console.WriteLine(" initialize, tools/list, tools/call, notifications/initialized, ping,"); + Console.WriteLine(" notifications/exit (or notifications/shutdown)."); + Console.WriteLine(); + Console.WriteLine("Tools:"); + Console.WriteLine(" ackit.scan, ackit.findings, ackit.context, ackit.health."); + return ExitSuccess; + } + + if (HasFlag(args, "--stdio-server")) + { + return RunMcpStdioServer(args, repositoryPath, language, services); + } + + var input = GetOption(args, "--stdio"); + if (string.IsNullOrWhiteSpace(input)) + { + Console.Error.WriteLine(services.TextProvider.Get("mcpRequiresStdio", language)); + return ExitError; + } + + var response = services.McpServer.HandleJson(input); + var output = GetOption(args, "--output"); + if (string.IsNullOrWhiteSpace(output)) + { + Console.WriteLine(response); + return ExitSuccess; + } + + try + { + var outputPath = NormalizeMcpOutputPath(repositoryPath, output); + services.FileSystem.WriteAllText(outputPath, response.TrimEnd() + Environment.NewLine); + Console.WriteLine(language.Value == "tr" + ? "MCP yaniti yazildi." + : "MCP response written."); + return ExitSuccess; + } + catch (InvalidOperationException ex) + { + Console.Error.WriteLine(ex.Message); + return ExitError; + } + } + + private static int RunMcpStdioServer(string[] args, string repositoryPath, LanguageCode language, Services services) + { + var defaultRepo = repositoryPath; + var repoOverride = GetOption(args, "--repo"); + if (!string.IsNullOrWhiteSpace(repoOverride)) + { + var trimmed = repoOverride.Trim(); + if (trimmed.Contains("://", StringComparison.Ordinal) || + trimmed.StartsWith("file:", StringComparison.OrdinalIgnoreCase) || + trimmed.StartsWith(@"\\", StringComparison.Ordinal) || + trimmed.StartsWith("//", StringComparison.Ordinal)) + { + Console.Error.WriteLine(services.TextProvider.Get("mcpInvalidRepo", language)); + return ExitCritical; + } + try + { + var fullPath = Path.GetFullPath(trimmed); + if (!services.FileSystem.DirectoryExists(fullPath)) + { + Console.Error.WriteLine(services.TextProvider.Get("mcpRepoNotDirectory", language)); + return ExitCritical; + } + defaultRepo = fullPath; + } + catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) + { + Console.Error.WriteLine(services.TextProvider.Get("mcpInvalidRepoPath", language)); + return ExitCritical; + } + } + + var options = new McpStdioOptions { DefaultRepositoryPath = defaultRepo }; + var transport = new McpStdioTransport(services.McpServer, Console.In, Console.Out, Console.Error, options); + + try + { + return transport.RunAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.Error.WriteLine(services.TextProvider.Get("mcpServerCrashed", language).Replace("{kind}", ex.GetType().Name)); + return ExitError; + } + } + + private static string NormalizeMcpOutputPath(string repositoryPath, string output) + { + var normalized = output.Trim().Replace('\\', '/'); + if (Path.IsPathRooted(normalized)) + { + throw new InvalidOperationException("MCP output path must be repository-relative."); + } + + var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0 || segments.Any(segment => segment is "." or "..")) + { + throw new InvalidOperationException("MCP output path must stay inside the repository."); + } + + if (!normalized.EndsWith(".json", StringComparison.OrdinalIgnoreCase) && + !normalized.EndsWith(".jsonl", StringComparison.OrdinalIgnoreCase) && + !normalized.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("MCP output path must end with .json, .jsonl, or .txt."); + } + + var repositoryFullPath = Path.GetFullPath(repositoryPath); + var outputFullPath = Path.GetFullPath(Path.Combine(repositoryFullPath, normalized.Replace('/', Path.DirectorySeparatorChar))); + var repositoryPrefix = repositoryFullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + + if (!outputFullPath.StartsWith(repositoryPrefix, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("MCP output path must stay inside the repository."); + } + + return outputFullPath; + } + + private static int RunDiff(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) + { + var from = GetOption(args, "--from"); + var to = GetOption(args, "--to"); + if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to)) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, command = "diff", exitCode = ExitError, error = "Missing --from or --to" }); + } + else + { + Console.Error.WriteLine("ackit diff requires --from and --to <repo-relative.json>."); + } + return ExitError; + } + + try + { + var fromPath = Path.Combine(repositoryPath, from.Replace('/', Path.DirectorySeparatorChar)); + var toPath = Path.Combine(repositoryPath, to.Replace('/', Path.DirectorySeparatorChar)); + if (!File.Exists(fromPath) || !File.Exists(toPath)) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, command = "diff", exitCode = ExitError, error = "Baseline file not found" }); + } + else + { + Console.Error.WriteLine("Baseline file not found."); + } + return ExitError; + } + + var fromJson = File.ReadAllText(fromPath); + var toJson = File.ReadAllText(toPath); + var fromManifest = BaselineSerializer.Deserialize(fromJson); + var toManifest = BaselineSerializer.Deserialize(toJson); + var diff = BaselineDiffCalculator.Compare(fromManifest, toManifest); + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + command = "diff", + exitCode = ExitSuccess, + fromBaseline = from, + toBaseline = to, + addedCount = diff.Added.Count, + removedCount = diff.Removed.Count, + unchangedCount = diff.Unchanged.Count, + severityChangedCount = diff.SeverityChanged.Count + }); + } + else + { + Console.WriteLine($"added: {diff.Added.Count}"); + Console.WriteLine($"removed: {diff.Removed.Count}"); + Console.WriteLine($"unchanged: {diff.Unchanged.Count}"); + Console.WriteLine($"severityChanged: {diff.SeverityChanged.Count}"); + } + return ExitSuccess; + } + catch (Exception ex) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, command = "diff", exitCode = ExitError, error = ex.Message }); + } + else + { + Console.Error.WriteLine($"ackit diff failed: {ex.Message}"); + } + return ExitError; + } + } + + private static int RunTrim(string[] args, string repositoryPath, LanguageCode language, bool json, Services services) + { + var input = GetOption(args, "--input"); + var output = GetOption(args, "--output"); + var maxCharsStr = GetOption(args, "--max-chars"); + + if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(output) || string.IsNullOrWhiteSpace(maxCharsStr)) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Missing --input, --output, or --max-chars" }); + } + else + { + Console.Error.WriteLine(services.TextProvider.Get("trimRequiresArgs", language)); + } + return ExitError; + } + + if (!int.TryParse(maxCharsStr, out var maxChars) || maxChars <= 0) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Invalid --max-chars" }); + } + else + { + Console.Error.WriteLine(services.TextProvider.Get("trimInvalidMaxChars", language)); + } + return ExitError; + } + + var inputFull = Path.Combine(repositoryPath, input.Replace('/', Path.DirectorySeparatorChar)); + var outputFull = Path.Combine(repositoryPath, output.Replace('/', Path.DirectorySeparatorChar)); + if (string.Equals(Path.GetFullPath(inputFull), Path.GetFullPath(outputFull), StringComparison.OrdinalIgnoreCase)) + { + if (json) + { + WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Input and output must differ" }); + } + else + { + Console.Error.WriteLine(services.TextProvider.Get("trimInputOutputMustDiffer", language)); + } + return ExitError; + } + + try + { + if (!File.Exists(inputFull)) + { + if (json) WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = "Input file not found" }); + else Console.Error.WriteLine(services.TextProvider.Get("trimInputNotFound", language)); + return ExitError; + } + var content = File.ReadAllText(inputFull); + var originalChars = content.Length; + var trimmed = TextTrimmer.Trim(content, maxChars); + Directory.CreateDirectory(Path.GetDirectoryName(outputFull)!); + File.WriteAllText(outputFull, trimmed); + + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + command = "trim", + exitCode = ExitSuccess, + input, + output, + maxChars, + originalChars, + trimmedChars = trimmed.Length + }); + } + else + { + Console.WriteLine($"original: {originalChars}"); + Console.WriteLine($"trimmed: {trimmed.Length}"); + Console.WriteLine($"max-chars: {maxChars}"); + } + return ExitSuccess; + } + catch (Exception ex) + { + if (json) WriteJson(new { schemaVersion = JsonSchemaVersion, command = "trim", exitCode = ExitError, error = ex.Message }); + else Console.Error.WriteLine($"ackit trim failed: {ex.Message}"); + return ExitError; + } + } + + private static int RunWatch(string[] args, string repositoryPath, AckitConfig config, LanguageCode language, bool json, Services services) + { + int debounceMs; + bool once; + int maxRuntimeMs; + try + { + debounceMs = ParsePositiveInt(args, "--debounce-ms", defaultValue: 500); + once = HasFlag(args, "--once"); + maxRuntimeMs = ParsePositiveInt(args, "--max-runtime-ms", defaultValue: 0); + } + catch (ArgumentException ex) + { + return WriteInvalidArgumentError("watch", ex.Message, json, language, services); + } + + var startupMessage = services.TextProvider.Get("watchWatching", language) + .Replace("{repo}", GetRepositoryName(repositoryPath)) + .Replace("{ms}", debounceMs.ToString(System.Globalization.CultureInfo.InvariantCulture)); + if (json) + { + Console.Error.WriteLine(startupMessage); + } + else + { + Console.WriteLine(startupMessage); + } + + var options = new WatchOptions( + Debounce: TimeSpan.FromMilliseconds(debounceMs), + MaxRuntime: TimeSpan.FromMilliseconds(maxRuntimeMs), + OneShot: once, + Language: language, + EmitJson: json, + RepositoryPath: repositoryPath, + Config: config, + Clock: () => services.Clock.UtcNow); + + IFileWatcher watcher = new PhysicalFileWatcher(repositoryPath); + if (HasFlag(args, "--help") || HasFlag(args, "-h")) + { + Console.WriteLine("ackit watch -- local file-system change watcher"); + Console.WriteLine("Usage:"); + Console.WriteLine(" ackit watch [--debounce-ms <N>] [--once] [--max-runtime-ms <N>] [--json] [--lang en|tr]"); + Console.WriteLine("Options:"); + Console.WriteLine(" --debounce-ms <N> Minimum interval between re-runs (default 500)."); + Console.WriteLine(" --once Run a single scan and exit."); + Console.WriteLine(" --max-runtime-ms <N> Wall-clock cap (0 = unlimited)."); + Console.WriteLine(" --json Emit JSON change reports."); + Console.WriteLine(" --lang en|tr Human output language (default en)."); + ((IDisposable)watcher).Dispose(); + return ExitSuccess; + } + + if (HasFlag(args, "--help") || HasFlag(args, "-h")) + { + return ExitSuccess; + } + + WatchResult result; + try + { + result = WatchRunner.Run(watcher, services.RepositoryScanner, options); + } + finally + { + ((IDisposable)watcher).Dispose(); + } + + if (result.LastReport is not null) + { + try + { + WriteWatchReport(repositoryPath, result.LastReport, language, json); + } + catch (Exception ex) + { + Console.Error.WriteLine(services.TextProvider.Get("watchReportFailed", language).Replace("{kind}", ex.GetType().Name)); + } + } + + return ExitSuccess; + } + + private static void WriteWatchReport(string repositoryPath, ScanChangeReport report, LanguageCode language, bool json) + { + if (json) + { + var dto = new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc = DateTimeOffset.UtcNow, + command = "watch", + repositoryName = GetRepositoryName(repositoryPath), + addedCount = report.AddedCount, + removedCount = report.RemovedCount, + unchangedCount = report.UnchangedCount, + severityChangedCount = report.SeverityChangedCount, + addedSample = report.AddedSample.Select(finding => new + { + ruleId = RiskRuleCatalog.GetRuleId(finding), + severity = finding.Severity.ToString(), + path = finding.Path, + message = finding.Message + }).ToArray(), + removedSample = report.RemovedSample.Select(finding => new + { + ruleId = RiskRuleCatalog.GetRuleId(finding), + severity = finding.Severity.ToString(), + path = finding.Path, + message = finding.Message + }).ToArray(), + severityChangedSample = report.SeverityChangedSample.Select(sample => new + { + ruleId = sample.RuleId, + path = sample.Path, + fromSeverity = sample.FromSeverity, + toSeverity = sample.ToSeverity + }).ToArray() + }; + WriteJson(dto); + return; + } + + var title = language.Value == "tr" ? "degisiklik" : "change"; + Console.WriteLine($"{title}: +{report.AddedCount} -{report.RemovedCount} ~{report.SeverityChangedCount} (unchanged {report.UnchangedCount})"); + } + + private static int ParsePositiveInt(string[] args, string name, int defaultValue) + { + var raw = GetOption(args, name); + if (string.IsNullOrWhiteSpace(raw)) + { + return defaultValue; + } + if (!int.TryParse(raw, out var value)) + { + throw new ArgumentException($"{name} must be a positive integer."); + } + if (value <= 0 && name != "--max-runtime-ms") + { + throw new ArgumentException($"{name} must be a positive integer."); + } + if (value < 0) + { + throw new ArgumentException($"{name} must not be negative."); + } + return value; + } + + private static int RunUnknown(string command, LanguageCode language, ITextProvider textProvider) + { + Console.Error.WriteLine($"{textProvider.Get("unknownCommand", language)}: {command}"); + RunHelp(language, textProvider); + return ExitError; + } + + private static void PrintScan(ScanResult scan, LanguageCode language, Services services) + { + Console.WriteLine(services.TextProvider.Get("scanSummary", language)); + Console.WriteLine($"{services.TextProvider.Get("repository", language)}: {scan.RepositoryPath}"); + Console.WriteLine($"{services.TextProvider.Get("files", language)}: {scan.Files.Count}"); + + Console.WriteLine(); + Console.WriteLine(services.TextProvider.Get("stacks", language)); + if (scan.Stacks.Count == 0) + { + Console.WriteLine($"- {services.TextProvider.Get("unknown", language)}"); + } + else + { + foreach (var stack in scan.Stacks) + { + Console.WriteLine($"- {stack.Name}: {stack.Signal}"); + } + } + + Console.WriteLine(); + Console.WriteLine(services.TextProvider.Get("repositoryHealth", language)); + Console.WriteLine($"- README: {YesNo(scan.HasReadme, language, services.TextProvider)}"); + Console.WriteLine($"- LICENSE: {YesNo(scan.HasLicense, language, services.TextProvider)}"); + Console.WriteLine($"- SECURITY: {YesNo(scan.HasSecurityPolicy, language, services.TextProvider)}"); + Console.WriteLine($"- {services.TextProvider.Get("tests", language)}: {YesNo(scan.HasTests, language, services.TextProvider)}"); + Console.WriteLine($"- CI: {YesNo(scan.HasCi, language, services.TextProvider)}"); + Console.WriteLine($"- Docker: {YesNo(scan.HasDocker, language, services.TextProvider)}"); + Console.WriteLine($"- {services.TextProvider.Get("agentInstructions", language)}: {YesNo(scan.HasAgentInstructions, language, services.TextProvider)}"); + + Console.WriteLine(); + PrintFindings(scan.Findings, language, services); + PrintSuppressions(scan.Suppressions, language, services.TextProvider); + } + + private static void PrintBaselineClassification( + string baselinePath, + BaselineEvaluation baseline, + LanguageCode language, + ITextProvider textProvider) + { + Console.WriteLine(); + Console.WriteLine(textProvider.Get("baselineClassification", language)); + Console.WriteLine($"- {textProvider.Get("file", language)}: {baselinePath}"); + Console.WriteLine($"- {textProvider.Get("existingFindings", language)}: {baseline.Existing.Count}"); + Console.WriteLine($"- {textProvider.Get("newFindings", language)}: {baseline.New.Count}"); + foreach (var finding in baseline.Findings.Take(25)) + { + Console.WriteLine($"- {finding.Status}: {finding.Finding.Path} {RiskRuleCatalog.GetRuleId(finding.Finding)} [{finding.Finding.Severity}] {textProvider.Get("occurrence", language)} {finding.Occurrence}"); + } + + if (baseline.Findings.Count > 25) + { + Console.WriteLine($"- ... {baseline.Findings.Count - 25} {textProvider.Get("more", language)}"); + } + } + + private static void PrintSuppressions( + IReadOnlyList<RiskSuppression> suppressions, + LanguageCode language, + ITextProvider textProvider) + { + if (suppressions.Count == 0) + { + return; + } + + Console.WriteLine(); + Console.WriteLine($"{textProvider.Get("suppressedFindings", language)}: {suppressions.Count}"); + foreach (var suppression in suppressions.Take(25)) + { + Console.WriteLine($"- {suppression.Path}: {suppression.RuleId} [{suppression.Severity}/{suppression.Category}] {textProvider.Get("via", language)} {ToSuppressionReason(suppression.Reason)}"); + } + + if (suppressions.Count > 25) + { + Console.WriteLine($"- ... {suppressions.Count - 25} {textProvider.Get("more", language)}"); + } + } + + private static void PrintFindings(IReadOnlyList<RiskFinding> findings, LanguageCode language, Services services) + { + if (findings.Count == 0) + { + Console.WriteLine(services.TextProvider.Get("noFindings", language)); + return; + } + + foreach (var severity in new[] { RiskSeverity.Critical, RiskSeverity.High, RiskSeverity.Medium, RiskSeverity.Low, RiskSeverity.Info }) + { + var group = findings.Where(finding => finding.Severity == severity).Take(25).ToArray(); + if (group.Length == 0) + { + continue; + } + + Console.WriteLine($"{severity}:"); + foreach (var finding in group) + { + Console.WriteLine($"- {finding.Path}: {finding.Message}"); + } + + var omitted = findings.Count(finding => finding.Severity == severity) - group.Length; + if (omitted > 0) + { + Console.WriteLine($"- ... {omitted} more"); + } + } + } + + private static void PrintGeneratedResult(GeneratedFileResult result, ITextProvider textProvider, LanguageCode language) + { + var status = result.Created + ? textProvider.Get("created", language) + : textProvider.Get("skipped", language); + + Console.WriteLine($"- {result.Path}: {status}"); + } + + private static void WriteJson(object value) + { + Console.WriteLine(JsonSerializer.Serialize(value, new JsonSerializerOptions + { + WriteIndented = true + })); + } + + private static object ToScanDto( + string command, + ScanResult scan, + DateTimeOffset generatedAtUtc, + bool ciMode, + int exitCode, + string? baselinePath = null, + BaselineEvaluation? baseline = null) + { + var result = new Dictionary<string, object?> + { + ["schemaVersion"] = JsonSchemaVersion, + ["toolVersion"] = Version, + ["generatedAtUtc"] = generatedAtUtc, + ["command"] = command, + ["ciMode"] = ciMode, + ["exitCode"] = exitCode, + ["repositoryPath"] = scan.RepositoryPath, + ["repositoryName"] = GetRepositoryName(scan.RepositoryPath), + ["fileCount"] = scan.Files.Count, + ["stacks"] = scan.Stacks.Select(stack => new + { + name = stack.Name, + signal = stack.Signal + }).ToArray(), + ["health"] = new + { + hasReadme = scan.HasReadme, + hasLicense = scan.HasLicense, + hasSecurityPolicy = scan.HasSecurityPolicy, + hasContributing = scan.HasContributing, + hasCodeOfConduct = scan.HasCodeOfConduct, + hasChangelog = scan.HasChangelog, + hasTests = scan.HasTests, + hasCi = scan.HasCi, + hasDocker = scan.HasDocker, + hasAgentInstructions = scan.HasAgentInstructions + }, + ["riskSummary"] = ToRiskSummary(scan.Findings), + ["findings"] = scan.Findings.Select(ToRiskFindingDto).ToArray(), + ["suppressionSummary"] = new + { + total = scan.Suppressions.Count, + safeDomains = scan.Suppressions.Count(suppression => suppression.Reason == RiskSuppressionReason.SafeDomain), + ignoredPaths = scan.Suppressions.Count(suppression => suppression.Reason == RiskSuppressionReason.IgnoredPath), + ignoredFindingIds = scan.Suppressions.Count(suppression => suppression.Reason == RiskSuppressionReason.IgnoredFindingId) + }, + ["suppressions"] = scan.Suppressions.Select(suppression => new + { + ruleId = suppression.RuleId, + severity = suppression.Severity.ToString(), + category = suppression.Category.ToString(), + path = suppression.Path, + reason = ToSuppressionReason(suppression.Reason) + }).ToArray() + }; + + AddBaselineDto(result, baselinePath, baseline); + + return result; + } + + private static void AddBaselineDto( + IDictionary<string, object?> result, + string? baselinePath, + BaselineEvaluation? baseline) + { + if (baseline is null) + { + return; + } + + result["baseline"] = new + { + path = baselinePath, + schemaVersion = BaselineSchema.CurrentVersion, + fingerprintAlgorithm = BaselineSchema.FingerprintAlgorithm, + entryCount = baseline.BaselineEntryCount, + existing = baseline.Existing.Count, + @new = baseline.New.Count, + classifiedFindings = baseline.Findings.Select(finding => new + { + ruleId = RiskRuleCatalog.GetRuleId(finding.Finding), + severity = finding.Finding.Severity.ToString(), + path = finding.Finding.Path, + fingerprint = finding.Fingerprint, + status = finding.Status.ToString().ToLowerInvariant(), + occurrence = finding.Occurrence + }).ToArray() + }; + } + + private static string ToSuppressionReason(RiskSuppressionReason reason) + { + return reason switch + { + RiskSuppressionReason.SafeDomain => "safeDomains", + RiskSuppressionReason.IgnoredPath => "ignoredPaths", + RiskSuppressionReason.IgnoredFindingId => "ignoredFindingIds", + _ => "unknown" + }; + } + + private static object ToRiskFindingDto(RiskFinding finding) + { + return new + { + ruleId = RiskRuleCatalog.GetRuleId(finding), + severity = finding.Severity.ToString(), + category = finding.Category.ToString(), + path = finding.Path, + message = finding.Message, + match = (string?)null + }; + } + + private static object ToDoctorCheckDto(DoctorCheck check) + { + return new + { + name = check.Name, + severity = check.Severity.ToString(), + passed = check.Passed, + message = check.Message + }; + } + + private static object ToConfigDiagnosticDto(ConfigDiagnostic diagnostic) + { + return new + { + code = diagnostic.Code, + severity = diagnostic.Severity.ToString(), + line = diagnostic.Line, + key = diagnostic.Key, + message = diagnostic.Message + }; + } + + private static object ToGeneratedFileDto(GeneratedFileResult result) + { + return new + { + path = result.Path, + status = result.Status.ToString(), + created = result.Created, + message = result.Message + }; + } + + private static object ToRiskSummary(IReadOnlyList<RiskFinding> findings) + { + return new + { + total = findings.Count, + critical = findings.Count(finding => finding.Severity == RiskSeverity.Critical), + high = findings.Count(finding => finding.Severity == RiskSeverity.High), + medium = findings.Count(finding => finding.Severity == RiskSeverity.Medium), + low = findings.Count(finding => finding.Severity == RiskSeverity.Low), + info = findings.Count(finding => finding.Severity == RiskSeverity.Info) + }; + } + + private static object ToDoctorCheckSummary(IReadOnlyList<DoctorCheck> checks) + { + return new + { + total = checks.Count, + passed = checks.Count(check => check.Passed), + failed = checks.Count(check => !check.Passed), + failedHighOrCritical = checks.Count(check => !check.Passed && check.Severity >= RiskSeverity.High) + }; + } + + private static object ToGeneratedFileSummary(IReadOnlyList<GeneratedFileResult> results) + { + return new + { + total = results.Count, + created = results.Count(result => result.Created), + skipped = results.Count(result => !result.Created) + }; + } + + private static string GetRepositoryName(string repositoryPath) + { + var trimmed = repositoryPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return Path.GetFileName(trimmed); + } + + private static string YesNo(bool value, LanguageCode language, ITextProvider textProvider) + { + return textProvider.Get(value ? "yes" : "no", language); + } + + private static int GetScanExitCode(ScanResult scan, bool ci) + { + if (!ci) + { + return ExitSuccess; + } + + if (scan.Findings.Any(finding => finding.Severity == RiskSeverity.Critical)) + { + return ExitCritical; + } + + if (scan.Findings.Any(finding => finding.Severity == RiskSeverity.High)) + { + return ExitError; + } + + return ExitSuccess; + } + + private static int GetBaselineScanExitCode(BaselineEvaluation baseline, bool ci) + { + if (!ci) + { + return ExitSuccess; + } + + if (baseline.New.Any(finding => finding.Finding.Severity == RiskSeverity.Critical)) + { + return ExitCritical; + } + + if (baseline.New.Any(finding => finding.Finding.Severity == RiskSeverity.High)) + { + return ExitError; + } + + return ExitSuccess; + } + + private static int WriteBaselineError(string command, BaselineException exception, bool json, DateTimeOffset generatedAtUtc) + { + if (json) + { + WriteJson(new + { + schemaVersion = JsonSchemaVersion, + toolVersion = Version, + generatedAtUtc, + command, + exitCode = ExitError, + error = new + { + code = exception.Code, + message = exception.Message + } + }); + return ExitError; + } + + Console.Error.WriteLine($"{exception.Code}: {exception.Message}"); + return ExitError; + } + + private static (string? Path, BaselineEvaluation? Evaluation) LoadBaseline( + string[] args, + string repositoryPath, + ScanResult scan, + Services services) + { + var requestedPath = GetOption(args, "--baseline"); + if (string.IsNullOrWhiteSpace(requestedPath)) + { + return (null, null); + } + + var manifest = services.BaselineStore.Load(repositoryPath, requestedPath); + var normalizedPath = BaselineFingerprint.NormalizeRelativePath(requestedPath); + return (normalizedPath, services.BaselineClassifier.Classify(scan.Findings, manifest)); + } + + private static AgentTarget ParseTarget(string? value) + { + return value?.Trim().ToLowerInvariant() switch + { + "codex" => AgentTarget.Codex, + "claude" => AgentTarget.Claude, + "anthropic" => AgentTarget.Anthropic, + "cursor" => AgentTarget.Cursor, + "copilot" => AgentTarget.Copilot, + "continue" => AgentTarget.Continue, + "all" or null or "" => AgentTarget.All, + _ => AgentTarget.All + }; + } + + private static AgentTarget ParseHookTarget(string? value) + { + return value?.Trim().ToLowerInvariant() switch + { + "claude" => AgentTarget.Claude, + "anthropic" => AgentTarget.Anthropic, + "continue" => AgentTarget.Continue, + "all" => AgentTarget.All, + "codex" or null or "" => AgentTarget.Codex, + _ => AgentTarget.Codex + }; + } + + private static bool CommandExistsOnPath(string command) + { + var path = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + var extensions = OperatingSystem.IsWindows() + ? new[] { ".exe", ".cmd", ".bat", string.Empty } + : new[] { string.Empty }; + + foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) + { + foreach (var extension in extensions) + { + var candidate = Path.Combine(directory.Trim(), command + extension); + if (File.Exists(candidate)) + { + return true; + } + } + } + + return false; + } + + private static string? GetOption(string[] args, string name) + { + for (var index = 0; index < args.Length; index++) + { + var current = args[index]; + if (current.StartsWith(name + "=", StringComparison.OrdinalIgnoreCase)) + { + return current[(name.Length + 1)..]; + } + + if (string.Equals(current, name, StringComparison.OrdinalIgnoreCase) && index + 1 < args.Length) + { + return args[index + 1]; + } + } + + return null; + } + + private static bool HasFlag(string[] args, string name) + { + return args.Any(arg => string.Equals(arg, name, StringComparison.OrdinalIgnoreCase)); + } + + private static string GetTaskTitle(string[] args) + { + var parts = new List<string>(); + + for (var index = 1; index < args.Length; index++) + { + var current = args[index]; + if (current.StartsWith("--", StringComparison.Ordinal)) + { + if (OptionConsumesValue(current) && !current.Contains('=', StringComparison.Ordinal) && index + 1 < args.Length) + { + index++; + } + + continue; + } + + parts.Add(current); + } + + return string.Join(' ', parts).Trim(); + } + + private static bool OptionConsumesValue(string option) + { + return string.Equals(option, "--lang", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--target", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--profile", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--baseline", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--prompt-pack", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--stdio", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--output", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--include", StringComparison.OrdinalIgnoreCase) || + string.Equals(option, "--exclude", StringComparison.OrdinalIgnoreCase); + } + + private static Services CreateServices() + { + var fileSystem = new PhysicalFileSystem(); + var secretScanner = new SecretScanner(); + var brandPiiScanner = new BrandPiiScanner(); + var riskScanner = new RiskScanner(fileSystem, secretScanner, brandPiiScanner); + var stackDetector = new StackDetector(fileSystem); + var repositoryScanner = new RepositoryScanner(fileSystem, stackDetector, riskScanner); + var templateRenderer = new TemplateRenderer(); + var textProvider = new TextProvider(); + var clock = new SystemClock(); + var configReader = new AckitConfigReader(fileSystem); + var doctor = new RepositoryDoctor(fileSystem); + var mcpServer = new McpRouter( + fileSystem, + configReader, + repositoryScanner, + new RepositoryDoctorHealthProbe(doctor), + Version); + + return new Services( + fileSystem, + configReader, + new AckitConfigValidator(), + new AckitConfigWriter(fileSystem), + new BaselineStore(fileSystem), + new BaselineClassifier(), + repositoryScanner, + new AgentInstructionGenerator(fileSystem, templateRenderer, clock), + new HtmlReportGenerator(fileSystem, clock), + new WebUiGenerator(fileSystem, clock), + new PromptPackGenerator(fileSystem, clock), + new ContextExportManifestGenerator(fileSystem, clock), + new SarifReportWriter(fileSystem), + new TaskFileGenerator(fileSystem, templateRenderer), + doctor, + mcpServer, + clock, + textProvider); + } + + private sealed record Services( + IFileSystem FileSystem, + IAckitConfigReader ConfigReader, + IAckitConfigValidator ConfigValidator, + IAckitConfigWriter ConfigWriter, + IBaselineStore BaselineStore, + IBaselineClassifier BaselineClassifier, + IRepositoryScanner RepositoryScanner, + IAgentInstructionGenerator AgentInstructionGenerator, + IHtmlReportGenerator HtmlReportGenerator, + IWebUiGenerator WebUiGenerator, + IPromptPackGenerator PromptPackGenerator, + IContextExportManifestGenerator ContextExportManifestGenerator, + ISarifReportWriter SarifReportWriter, + ITaskFileGenerator TaskFileGenerator, + RepositoryDoctor Doctor, + IMcpServer McpServer, + IClock Clock, + ITextProvider TextProvider); +} From e065d09dce1ab74ee7c2dc069688b94420038281 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:04:10 +0300 Subject: [PATCH 05/20] build: keep package version while using NuGet README --- src/AgentContextKit.Cli/AgentContextKit.Cli.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj b/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj index d217fd1..bf256a3 100644 --- a/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj +++ b/src/AgentContextKit.Cli/AgentContextKit.Cli.csproj @@ -12,7 +12,7 @@ <PackAsTool>true</PackAsTool> <ToolCommandName>ackit</ToolCommandName> <PackageId>AgentContextKit</PackageId> - <Version>0.2.0-alpha.4</Version> + <Version>0.2.0-alpha.3</Version> <Authors>Cynrath</Authors> <Company>Cynrath</Company> <Copyright>Copyright (c) 2026 Cynrath</Copyright> @@ -20,7 +20,7 @@ <PackageReadmeFile>README.nuget.md</PackageReadmeFile> <PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageTags>ai;coding-agent;context;security;cli;oss</PackageTags> - <PackageReleaseNotes>NuGet README rendering fix with a dedicated pure-Markdown package README and package metadata guidance. No runtime feature changes.</PackageReleaseNotes> + <PackageReleaseNotes>MCP stdio server and ackit.rules metadata tool, local watch mode, diff/trim command stabilization, scan include/exclude filter documentation parity, README/CLI reference sync, and release hardening with RB-003/RB-008 evidence cleanup.</PackageReleaseNotes> <PackageProjectUrl>https://github.com/Cynrath/agent-context-kit</PackageProjectUrl> <RepositoryUrl>https://github.com/Cynrath/agent-context-kit</RepositoryUrl> <RepositoryType>git</RepositoryType> From a11de62cc23d8f0b7facf394a620a454939e0450 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:04:35 +0300 Subject: [PATCH 06/20] docs: keep NuGet README version-neutral --- README.nuget.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.nuget.md b/README.nuget.md index bcc49a7..89085c1 100644 --- a/README.nuget.md +++ b/README.nuget.md @@ -8,14 +8,16 @@ This NuGet README is intentionally plain Markdown so it renders consistently on ## Install +Install the current package version shown on nuget.org: + ```powershell -dotnet tool install --global AgentContextKit --version 0.2.0-alpha.4 +dotnet tool install --global AgentContextKit --version <package-version> ``` Update an existing global install: ```powershell -dotnet tool update --global AgentContextKit --version 0.2.0-alpha.4 +dotnet tool update --global AgentContextKit --version <package-version> ``` Verify the tool: From 2f4a2c086fbd75f52adc7566bee3f9611e838b18 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:05:33 +0300 Subject: [PATCH 07/20] test: check NuGet README package metadata --- scripts/check-package-metadata.ps1 | 120 +++++++---------------------- 1 file changed, 26 insertions(+), 94 deletions(-) diff --git a/scripts/check-package-metadata.ps1 b/scripts/check-package-metadata.ps1 index 0138094..b10db39 100644 --- a/scripts/check-package-metadata.ps1 +++ b/scripts/check-package-metadata.ps1 @@ -8,68 +8,37 @@ Set-StrictMode -Version Latest $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $projectPath = Join-Path $repoRoot "src\AgentContextKit.Cli\AgentContextKit.Cli.csproj" +$readmeFile = "README.nuget.md" $issues = New-Object System.Collections.Generic.List[string] -$warnings = New-Object System.Collections.Generic.List[string] $notes = New-Object System.Collections.Generic.List[string] -function Add-Issue { - param([string]$Message) - $issues.Add($Message) | Out-Null -} - -function Add-Warning { - param([string]$Message) - $warnings.Add($Message) | Out-Null -} - -function Add-Note { - param([string]$Message) - $notes.Add($Message) | Out-Null -} +function Add-Issue { param([string]$Message) $issues.Add($Message) | Out-Null } +function Add-Note { param([string]$Message) $notes.Add($Message) | Out-Null } function Get-MetadataValue { - param( - [xml]$Project, - [string]$Name - ) - + param([xml]$Project, [string]$Name) foreach ($group in $Project.Project.PropertyGroup) { $value = $group.$Name if ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string]$value)) { return [string]$value } } - return $null } function Require-Value { - param( - [xml]$Project, - [string]$Name, - [string]$Expected - ) - + param([xml]$Project, [string]$Name, [string]$Expected) $actual = Get-MetadataValue -Project $Project -Name $Name - if ($actual -ne $Expected) { - Add-Issue "$Name expected '$Expected' but found '$actual'." - } + if ($actual -ne $Expected) { Add-Issue "$Name expected '$Expected' but found '$actual'." } } function Test-PackedReadme { param([xml]$Project) - - $nodes = $Project.SelectNodes("//None") - foreach ($node in $nodes) { - if ( - $node.GetAttribute("Include") -eq "..\..\README.md" -and - $node.GetAttribute("Pack") -eq "true" -and - $node.GetAttribute("PackagePath") -eq "\" - ) { + foreach ($node in $Project.SelectNodes("//None")) { + if ($node.GetAttribute("Include") -eq "..\..\README.nuget.md" -and $node.GetAttribute("Pack") -eq "true" -and $node.GetAttribute("PackagePath") -eq "\") { return $true } } - return $false } @@ -88,92 +57,55 @@ else { Require-Value -Project $project -Name "Version" -Expected $ExpectedVersion Require-Value -Project $project -Name "Authors" -Expected "Cynrath" Require-Value -Project $project -Name "Company" -Expected "Cynrath" - Require-Value -Project $project -Name "PackageReadmeFile" -Expected "README.md" + Require-Value -Project $project -Name "PackageReadmeFile" -Expected $readmeFile Require-Value -Project $project -Name "PackageLicenseExpression" -Expected "MIT" Require-Value -Project $project -Name "RepositoryType" -Expected "git" Require-Value -Project $project -Name "PackageRequireLicenseAcceptance" -Expected "false" $version = Get-MetadataValue -Project $project -Name "Version" - if ([string]::IsNullOrWhiteSpace($version) -or $version -notmatch "^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$") { - Add-Issue "Version is missing or does not look like a SemVer package version." - } + if ([string]::IsNullOrWhiteSpace($version) -or $version -notmatch "^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$") { Add-Issue "Version is missing or does not look like a SemVer package version." } $description = Get-MetadataValue -Project $project -Name "Description" - if ([string]::IsNullOrWhiteSpace($description) -or $description.Length -lt 20) { - Add-Issue "Description is missing or too short." - } + if ([string]::IsNullOrWhiteSpace($description) -or $description.Length -lt 20) { Add-Issue "Description is missing or too short." } $tags = Get-MetadataValue -Project $project -Name "PackageTags" foreach ($requiredTag in @("ai", "coding-agent", "security", "cli", "oss")) { $tagValues = @() - if (-not [string]::IsNullOrWhiteSpace($tags)) { - $tagValues = $tags -split "[;\s]+" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } - } - - if ($tagValues -notcontains $requiredTag) { - Add-Issue "PackageTags does not include '$requiredTag'." - } - } - - $releaseNotes = Get-MetadataValue -Project $project -Name "PackageReleaseNotes" - if ([string]::IsNullOrWhiteSpace($releaseNotes)) { - Add-Issue "PackageReleaseNotes is missing." + if (-not [string]::IsNullOrWhiteSpace($tags)) { $tagValues = $tags -split "[;\s]+" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } } + if ($tagValues -notcontains $requiredTag) { Add-Issue "PackageTags does not include '$requiredTag'." } } - $repositoryUrl = Get-MetadataValue -Project $project -Name "RepositoryUrl" - $packageProjectUrl = Get-MetadataValue -Project $project -Name "PackageProjectUrl" - if ([string]::IsNullOrWhiteSpace($repositoryUrl) -or $repositoryUrl -match "TODO|example\.com|localhost") { - Add-Issue "RepositoryUrl is missing or still a placeholder." - } + if ([string]::IsNullOrWhiteSpace((Get-MetadataValue -Project $project -Name "PackageReleaseNotes"))) { Add-Issue "PackageReleaseNotes is missing." } - if ([string]::IsNullOrWhiteSpace($packageProjectUrl) -or $packageProjectUrl -match "TODO|example\.com|localhost") { - Add-Issue "PackageProjectUrl is missing or still a placeholder." + foreach ($urlField in @("RepositoryUrl", "PackageProjectUrl")) { + $url = Get-MetadataValue -Project $project -Name $urlField + if ([string]::IsNullOrWhiteSpace($url) -or $url -match "TODO|example\.com|localhost") { Add-Issue "$urlField is missing or still a placeholder." } } - $readmePath = Join-Path $repoRoot "README.md" - if (-not (Test-Path $readmePath)) { - Add-Issue "README.md was not found for package readme." - } - elseif (-not (Test-PackedReadme -Project $project)) { - Add-Issue "README.md exists but is not explicitly packed into the package root." - } - else { - Add-Note "README.md is present and explicitly packed into the package root." - } + $readmePath = Join-Path $repoRoot $readmeFile + if (-not (Test-Path $readmePath)) { Add-Issue "$readmeFile was not found for package readme." } + elseif (-not (Test-PackedReadme -Project $project)) { Add-Issue "$readmeFile exists but is not explicitly packed into the package root." } + else { Add-Note "$readmeFile is present and explicitly packed into the package root." } + Add-Note "GitHub README.md and NuGet README.nuget.md are intentionally separate files." Add-Note "Package project inspected: $projectPath" - Add-Note "Metadata guidance checked against Microsoft Learn NuGet package authoring guidance." } Write-Host "" -if ($issues.Count -eq 0) { - Write-Host "No package metadata issues detected." -} +if ($issues.Count -eq 0) { Write-Host "No package metadata issues detected." } else { Write-Host "Package metadata issues:" - foreach ($issue in $issues) { - Write-Host "- $issue" - } -} - -if ($warnings.Count -gt 0) { - Write-Host "" - Write-Host "Warnings:" - foreach ($warning in $warnings) { - Write-Host "- $warning" - } + foreach ($issue in $issues) { Write-Host "- $issue" } } if ($notes.Count -gt 0) { Write-Host "" Write-Host "Notes:" - foreach ($note in $notes) { - Write-Host "- $note" - } + foreach ($note in $notes) { Write-Host "- $note" } } Write-Host "" -Write-Host "This review is local-only. It does not pack, push, publish, tag, redact, delete, or create remotes." +Write-Host "This review is local-only and report-only unless -FailOnIssues is passed." if ($FailOnIssues -and $issues.Count -gt 0) { Write-Host "" From 6c22295bf556fb227c05d3726184c20abec4b78c Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:05:56 +0300 Subject: [PATCH 08/20] docs: clarify NuGet README ownership for agents --- AGENTS.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8100e0b..39e5ab1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AgentContextKit Agent Rules ## Default Workflow -- Read `README.md`, `docs/PRODUCT_SPEC.md`, `docs/ARCHITECTURE.md`, and the active task before changing code. +- Read `README.md`, `README.nuget.md`, `docs/PRODUCT_SPEC.md`, `docs/ARCHITECTURE.md`, and the active task before changing code or package metadata. - Use task-first workflow: every implementation change starts from a `docs/tasks/` record. - Do not code before a task file exists under `docs/tasks/`. - Continuous progress hard rule: when the user says to continue, do not ask whether to continue; proceed through the next documented task in order with task docs, implementation, verification, and commit. @@ -10,6 +10,15 @@ - Prefer safe, minimal, production-ready changes. - Do not commit generated `.ackit/`, SARIF, HTML, Web UI, prompt pack, context export, `bin/`, or `obj/` artifacts. +## NuGet Package README Boundary +- GitHub repository presentation lives in root `README.md`. +- NuGet package presentation lives in root `README.nuget.md`. +- `src/AgentContextKit.Cli/AgentContextKit.Cli.csproj` owns `PackageReadmeFile` and the explicit package-root packing entry for `README.nuget.md`. +- Changes intended to fix the nuget.org package page must update `README.nuget.md`, the CLI `.csproj` package metadata, `scripts/check-package-metadata.ps1`, `docs/PACKAGING.md`, and `docs/NUGET_METADATA.md` together. +- Keep `README.nuget.md` pure Markdown: no raw HTML blocks, no GitHub-only layout markup, no relative local image paths, and no generated report artifacts. +- Do not try to fix NuGet rendering by weakening or removing the GitHub README layout unless the GitHub README itself is the intended target. +- A published NuGet version is immutable for this purpose; visible package README corrections require a later authorized package publish. + ## Safety - Keep the MVP offline-first and local-only. - Do not upload repository content. @@ -59,8 +68,8 @@ - Previous release: `v0.2.0-alpha.2` published and verified; pushed, released, and published. - NuGet global tool install verification: completed. - GitHub Release page: completed. -- Published-package smoke workflow may still need a post-publish pin sync to `AgentContextKit` `0.2.0-alpha.3`. -- Source-package smoke workflow installs the local `AgentContextKit` `0.2.0-alpha.3` package. +- Published-package smoke workflow is pinned to `AgentContextKit` `0.2.0-alpha.3` and TASK-0214 recorded hosted pass evidence. +- Source-package smoke workflow installs the local `AgentContextKit` package built from source. ## Risk Summary - No risk findings in the latest local scan. @@ -71,6 +80,7 @@ - `dotnet test AgentContextKit.sln -c Release --no-build` - `dotnet run --project src/AgentContextKit.Cli/AgentContextKit.Cli.csproj -c Release --no-build -- scan --ci` - `dotnet run --project src/AgentContextKit.Cli/AgentContextKit.Cli.csproj -c Release --no-build -- doctor` +- `powershell -ExecutionPolicy Bypass -File scripts/check-package-metadata.ps1 -FailOnIssues` - `powershell -ExecutionPolicy Bypass -File scripts/verify-release.ps1` ## Handoff From 1a93421dc5aa88d412ed40ebb1c88fccac78f7ce Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:06:14 +0300 Subject: [PATCH 09/20] docs: point Cursor agents to NuGet README files --- .cursor/rules/project.mdc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.cursor/rules/project.mdc b/.cursor/rules/project.mdc index e183ea8..e524f8c 100644 --- a/.cursor/rules/project.mdc +++ b/.cursor/rules/project.mdc @@ -9,6 +9,15 @@ - Do not include model name, generator, or AI authorship in commit messages. - `0.2.0-alpha.3` is published and verified by TASK-0206; do not move the tag, replace assets, republish the version, or manually mutate the GitHub Release/NuGet package. +## NuGet Package README Boundary +- GitHub repository README: `README.md`. +- NuGet package README: `README.nuget.md`. +- Package metadata and package-root README packing live in `src/AgentContextKit.Cli/AgentContextKit.Cli.csproj`. +- NuGet README validation lives in `scripts/check-package-metadata.ps1`. +- Documentation mirrors live in `docs/PACKAGING.md` and `docs/NUGET_METADATA.md`. +- Keep `README.nuget.md` pure Markdown. Do not use raw HTML, GitHub-only alignment/layout, local relative images, or generated artifacts there. +- Published NuGet package versions cannot be edited in place; nuget.org README fixes become visible only after a later authorized package publish. + ## Commit Completeness Hard Rule - Before any push, run `git status` and confirm the working tree is clean. - Run `powershell -ExecutionPolicy Bypass -File scripts/check-tracked-vs-untracked-md.ps1 -FailOnIssues` to confirm no tracked source file is left untracked. @@ -31,4 +40,5 @@ - `dotnet test AgentContextKit.sln -c Release --no-build` - `dotnet run --project src/AgentContextKit.Cli/AgentContextKit.Cli.csproj -c Release --no-build -- scan --ci` - `dotnet run --project src/AgentContextKit.Cli/AgentContextKit.Cli.csproj -c Release --no-build -- doctor` +- `powershell -ExecutionPolicy Bypass -File scripts/check-package-metadata.ps1 -FailOnIssues` - `powershell -ExecutionPolicy Bypass -File scripts/verify-release.ps1` From f9d4b1f4ce9bc2eda586ab6949c0667f3e95a077 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:06:38 +0300 Subject: [PATCH 10/20] docs: document NuGet README packaging boundary --- docs/PACKAGING.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index df54966..049492f 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -11,11 +11,22 @@ Important fields: - `Version`: `0.2.0-alpha.3`; current published package is `0.2.0-alpha.3` - `Authors`: `Cynrath` - `PackageLicenseExpression`: `MIT` -- `PackageReadmeFile`: `README.md` +- `PackageReadmeFile`: `README.nuget.md` - `RepositoryType`: `git` - `RepositoryUrl`: `https://github.com/Cynrath/agent-context-kit` - `PackageProjectUrl`: `https://github.com/Cynrath/agent-context-kit` +## README Files + +The repository intentionally has two README surfaces: + +- `README.md` is the GitHub repository README and may use GitHub-supported layout markup. +- `README.nuget.md` is the NuGet package README and must stay pure Markdown so nuget.org renders it cleanly. + +The NuGet README is wired from `src/AgentContextKit.Cli/AgentContextKit.Cli.csproj` through `PackageReadmeFile` and an explicit package-root packing entry. If the nuget.org README page needs a correction, update `README.nuget.md`, the CLI `.csproj`, `scripts/check-package-metadata.ps1`, and `docs/NUGET_METADATA.md` together. + +Published NuGet versions are immutable for README corrections. A visible nuget.org README fix requires a later authorized package publish; do not move tags, replace release assets, or republish an existing version. + Run the dedicated metadata review before pack or publish checks: ```powershell From 557c7907b94e150877d616c845181d23a2ab9a76 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:07:04 +0300 Subject: [PATCH 11/20] docs: document dedicated NuGet README metadata --- docs/NUGET_METADATA.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/NUGET_METADATA.md b/docs/NUGET_METADATA.md index ba5bc49..748726d 100644 --- a/docs/NUGET_METADATA.md +++ b/docs/NUGET_METADATA.md @@ -30,6 +30,18 @@ This review follows Microsoft Learn NuGet package authoring guidance for package - https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices#package-metadata - https://learn.microsoft.com/nuget/reference/msbuild-targets#pack-target +## NuGet README Rendering Contract + +The repository intentionally separates the GitHub README and NuGet package README: + +- `README.md` is the GitHub repository README. +- `README.nuget.md` is the NuGet package README. +- `src/AgentContextKit.Cli/AgentContextKit.Cli.csproj` owns `PackageReadmeFile` and the explicit package-root packing entry. + +Keep `README.nuget.md` pure Markdown. Do not add raw HTML, GitHub-only alignment/layout markup, relative local image paths, generated reports, or package artifacts. If nuget.org rendering breaks, edit `README.nuget.md` and the package metadata/check/docs files together. + +A published NuGet version cannot be corrected in place for README rendering. The fix becomes visible on nuget.org only after a later authorized package publish. + ## Metadata Review Run report-only mode: @@ -69,7 +81,7 @@ AgentContextKit 0.2.0-alpha.3 - `Version`: `0.2.0-alpha.3` - `Authors`: `Cynrath` - `Company`: `Cynrath` -- `PackageReadmeFile`: `README.md` +- `PackageReadmeFile`: `README.nuget.md` - `PackageLicenseExpression`: `MIT` - `RepositoryType`: `git` - `RepositoryUrl`: `https://github.com/Cynrath/agent-context-kit` @@ -78,7 +90,7 @@ AgentContextKit 0.2.0-alpha.3 - `Description`: non-empty - `PackageTags`: includes `ai`, `coding-agent`, `security`, `cli`, and `oss` - `PackageReleaseNotes`: non-empty -- `README.md`: present and explicitly packed into the package root +- `README.nuget.md`: present and explicitly packed into the package root ## Future Publish Gates Before future public publish after `0.2.0-alpha.3`: From c127945e94cc3795459666c3d4b4bdc264253e46 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:07:21 +0300 Subject: [PATCH 12/20] docs: plan task 0215 NuGet README rendering --- .../tasks/TASK-0215-nuget-readme-rendering.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/tasks/TASK-0215-nuget-readme-rendering.md diff --git a/docs/tasks/TASK-0215-nuget-readme-rendering.md b/docs/tasks/TASK-0215-nuget-readme-rendering.md new file mode 100644 index 0000000..860178c --- /dev/null +++ b/docs/tasks/TASK-0215-nuget-readme-rendering.md @@ -0,0 +1,55 @@ +# TASK-0215: NuGet README rendering cleanup + +## Purpose +Fix the nuget.org package README rendering problem caused by using the GitHub README as the package README, and make the file ownership obvious to future coding agents. + +## Problem +The GitHub README can use GitHub-supported HTML/layout markup. nuget.org does not render that surface the same way, so raw HTML can appear on the NuGet package page. + +## Scope +- Add a dedicated root `README.nuget.md` for the NuGet package page. +- Keep root `README.md` as the GitHub repository README. +- Update `src/AgentContextKit.Cli/AgentContextKit.Cli.csproj` so `PackageReadmeFile` points at `README.nuget.md` and packs that file into the package root. +- Update `scripts/check-package-metadata.ps1` so the metadata gate validates `README.nuget.md`. +- Update agent-facing documentation so agents understand that NuGet README changes live in `README.nuget.md` plus the CLI package project metadata. +- Update packaging and NuGet metadata docs. + +## Out of scope +- No NuGet publish. +- No GitHub Release mutation. +- No tag creation, deletion, or movement. +- No republish of `0.2.0-alpha.3`. +- No release workflow dispatch. +- No broad GitHub README redesign. + +## Affected files +- `README.nuget.md` +- `src/AgentContextKit.Cli/AgentContextKit.Cli.csproj` +- `scripts/check-package-metadata.ps1` +- `AGENTS.md` +- `.cursor/rules/project.mdc` +- `docs/PACKAGING.md` +- `docs/NUGET_METADATA.md` +- `docs/tasks/TASK-0215-nuget-readme-rendering.md` + +## Acceptance criteria +- NuGet package README content is pure Markdown and contains no raw HTML layout blocks. +- `PackageReadmeFile` points to `README.nuget.md`. +- `README.nuget.md` is explicitly packed into the package root. +- Package metadata gate expects `README.nuget.md`. +- Agent documentation names the exact files/folder paths to edit for NuGet package page changes. +- Existing published package, tag, and GitHub Release state are not mutated. + +## Validation commands + +```powershell +powershell -ExecutionPolicy Bypass -File scripts/check-package-metadata.ps1 -FailOnIssues +dotnet build AgentContextKit.sln -c Release --no-restore +dotnet test AgentContextKit.sln -c Release --no-build +dotnet pack src/AgentContextKit.Cli/AgentContextKit.Cli.csproj -c Release +``` + +Package inspection should confirm `README.nuget.md` exists at package root. + +## Completion notes +Pending validation on a local checkout or hosted PR checks. This task intentionally prepares source metadata only; the visible nuget.org page will change after a later authorized package publish. From 32dbd17f01869986c15f66927955f0b38a58500c Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:08:15 +0300 Subject: [PATCH 13/20] docs: record NuGet README rendering fix --- CHANGELOG.md | 53 ++-------------------------------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b11880c..69f065a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This project follows Semantic Versioning where practical before `1.0.0`. ## [Unreleased] ### Added +- Added a dedicated pure-Markdown `README.nuget.md` package README and package metadata wiring so nuget.org does not render GitHub README HTML as raw text. +- Added agent-facing documentation for the split between GitHub `README.md` and NuGet `README.nuget.md` ownership. - Added two new stable scanner rule IDs: `ACKIT006` `ProductionConfigLike` (High) for production configuration, environment-specific appsettings, and live-service connection strings, and `ACKIT007` `DocumentationGap` (Medium) for documentation gaps surfaced by the scanner. Existing `ACKIT001` and `ACKIT005` descriptions were narrowed to reflect the new dedicated rules. - Added an `Ackit006Ackit007EndToEndTests` coverage class that exercises the Core `RepositoryScanner` on a synthetic `appsettings.Production.json` fixture, asserts the new `ACKIT006` ruleId flows into JSON and the redact-check filter, asserts the catalog mapping for `ACKIT007`, and asserts the SARIF rule catalog advertises the new ID. @@ -118,54 +120,3 @@ PROJECT-CONTROL-0107 planning commit `c249a13` opens with the post-0158 state sy - Scanner documentation and security model are updated for v0.2.0-alpha. ### Security -- Critical findings cannot be silently suppressed by config allowlist. -- SARIF output avoids raw secret matches and absolute local paths. - -## [0.1.0-alpha.2] - 2026-06-05 -### Added -- Added a cross-platform source smoke workflow that packs the current branch and installs `AgentContextKit` `0.1.0-alpha.2` from a temporary local package source on Windows, Ubuntu, and macOS. -- Added alpha.2 hardening tasks for scanner noise reduction, GitHub Actions Node 24 readiness, Turkish CLI output polish, and release preparation. -- Published `v0.1.0-alpha.2` on GitHub and NuGet and verified global tool installation. - -### Changed -- Reduced scanner noise with a conservative safe technical domain allowlist and fixture-only placeholder email handling. -- Added safe technical allowlist coverage for common platform/package domains while preserving Critical secret detection. -- Reduced fixture placeholder noise without suppressing real source/docs email or secret findings. -- Prepared GitHub Actions workflows for Node 24-ready official action majors and explicit Windows runner labeling. -- Polished Turkish human CLI output while preserving JSON schema fields. -- Bumped source/package metadata and CLI runtime version to `0.1.0-alpha.2`. -- Updated the published-package smoke workflow to install `AgentContextKit` `0.1.0-alpha.2`. -- Recorded successful cross-platform GitHub Actions smoke validation for the published NuGet global tool. -- Synced post-push GitHub release status docs after `master` and `v0.1.0-alpha.1` were pushed. -- Verified NuGet publication and global tool install for `AgentContextKit` version `0.1.0-alpha.1`. -- Verified NuGet global tool smoke test in a clean demo app. - -## [0.1.0-alpha.1] - 2026-06-04 -### Added -- Initial offline-first .NET CLI tool package with command name `ackit`. -- CLI commands: `init`, `scan`, `scan --ci`, `report`, `webui`, `prompt-pack`, `context-export`, `generate`, `task`, `redact-check`, `doctor`, `version`, and `help`. -- Repository scanner for docs, tests, CI, Docker, generated agent files, package metadata, and stack signals. -- Sample-aware main stack detection for `.NET`, `.NET CLI / .NET Tool`, and `GitHub Actions` without treating `samples/` stacks as the main product stack. -- Pattern-based secret, PII, brand, risky path, and risky extension scanning. -- JSON output with schema/tool metadata, generated timestamps, repository metadata, summaries, and CI mode fields. -- Task-first development document generation under `docs/tasks`. -- Agent instruction generation for Codex, Claude, Cursor, and GitHub Copilot. -- Offline static HTML report generation with safe repository-relative output handling. -- Offline static Web UI prototype generation for local scan review. -- Local-only dry-run prompt pack generation and explicit-approval context export manifests. -- English and Turkish output/template foundation. -- Config schema documentation and generated-file conventions. -- Focused xUnit test coverage and GitHub Actions CI. -- Local release verification, package metadata, public release audit, release blocker, public gate, and v1.0 readiness scripts. -- v1.0 final local readiness review documentation and gate script. -- Source archive hygiene docs and WinRAR exclude guidance for local ZIP/RAR sharing. -- OSS readiness, governance, privacy, support, security, package, release, and maintainer handoff documentation. - -### Changed -- Public package and docs metadata use the `Cynrath` persona. -- Package URLs point to `https://github.com/Cynrath/agent-context-kit`. -- Public release blockers track the completed GitHub Release and NuGet publication state, with Codex for OSS submission as the remaining follow-up. - -### Fixed -- Added NuGet package README metadata for local pack readiness. -- Refined self-scan stack accuracy so sample ASP.NET Core, Minimal API, TypeScript, and Tailwind CSS signals are not reported as the main repository stack. From bd9671b360e1c5530f858685d71c8cdb9afb40cd Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:08:58 +0300 Subject: [PATCH 14/20] test --- docs/notes/test.tmp | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/notes/test.tmp diff --git a/docs/notes/test.tmp b/docs/notes/test.tmp new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/docs/notes/test.tmp @@ -0,0 +1 @@ +x \ No newline at end of file From 39d400904e6a13cf66c0d267d5701554330be34f Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:09:23 +0300 Subject: [PATCH 15/20] chore: clear temporary note file --- docs/notes/test.tmp | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/notes/test.tmp b/docs/notes/test.tmp index c1b0730..e69de29 100644 --- a/docs/notes/test.tmp +++ b/docs/notes/test.tmp @@ -1 +0,0 @@ -x \ No newline at end of file From 7ccbf61244bfc6c62bb3a588a0308edd5742f33b Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:15:53 +0300 Subject: [PATCH 16/20] x --- dummy | 1 + 1 file changed, 1 insertion(+) create mode 100644 dummy diff --git a/dummy b/dummy new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/dummy @@ -0,0 +1 @@ +x \ No newline at end of file From cc98890480ed19b90658ed741b3455b0044b25e4 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:16:09 +0300 Subject: [PATCH 17/20] x --- dummy2 | 1 + 1 file changed, 1 insertion(+) create mode 100644 dummy2 diff --git a/dummy2 b/dummy2 new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/dummy2 @@ -0,0 +1 @@ +x \ No newline at end of file From 4b4a0495907f79b2c1273a1870f9e170061033d7 Mon Sep 17 00:00:00 2001 From: Cyranth <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:16:23 +0300 Subject: [PATCH 18/20] x --- dummy3 | 1 + 1 file changed, 1 insertion(+) create mode 100644 dummy3 diff --git a/dummy3 b/dummy3 new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/dummy3 @@ -0,0 +1 @@ +x \ No newline at end of file From 8590e585e4469cad1056044df069e8c066ca5cf4 Mon Sep 17 00:00:00 2001 From: c <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:50:01 +0300 Subject: [PATCH 19/20] chore: remove temporary and dummy files --- docs/notes/test.tmp | 0 dummy | 1 - dummy2 | 1 - dummy3 | 1 - 4 files changed, 3 deletions(-) delete mode 100644 docs/notes/test.tmp delete mode 100644 dummy delete mode 100644 dummy2 delete mode 100644 dummy3 diff --git a/docs/notes/test.tmp b/docs/notes/test.tmp deleted file mode 100644 index e69de29..0000000 diff --git a/dummy b/dummy deleted file mode 100644 index c1b0730..0000000 --- a/dummy +++ /dev/null @@ -1 +0,0 @@ -x \ No newline at end of file diff --git a/dummy2 b/dummy2 deleted file mode 100644 index c1b0730..0000000 --- a/dummy2 +++ /dev/null @@ -1 +0,0 @@ -x \ No newline at end of file diff --git a/dummy3 b/dummy3 deleted file mode 100644 index c1b0730..0000000 --- a/dummy3 +++ /dev/null @@ -1 +0,0 @@ -x \ No newline at end of file From d5ac187bf5c6c94fff893a1e4967f92f7160195b Mon Sep 17 00:00:00 2001 From: c <85012225+Cynrath@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:51:27 +0300 Subject: [PATCH 20/20] test: update NuGet README metadata expectation --- CHANGELOG.md | 51 +++++++++++++++++++ .../AgentContextKitBehaviorTests.cs | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f065a..f998943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,3 +120,54 @@ PROJECT-CONTROL-0107 planning commit `c249a13` opens with the post-0158 state sy - Scanner documentation and security model are updated for v0.2.0-alpha. ### Security +- Critical findings cannot be silently suppressed by config allowlist. +- SARIF output avoids raw secret matches and absolute local paths. + +## [0.1.0-alpha.2] - 2026-06-05 +### Added +- Added a cross-platform source smoke workflow that packs the current branch and installs `AgentContextKit` `0.1.0-alpha.2` from a temporary local package source on Windows, Ubuntu, and macOS. +- Added alpha.2 hardening tasks for scanner noise reduction, GitHub Actions Node 24 readiness, Turkish CLI output polish, and release preparation. +- Published `v0.1.0-alpha.2` on GitHub and NuGet and verified global tool installation. + +### Changed +- Reduced scanner noise with a conservative safe technical domain allowlist and fixture-only placeholder email handling. +- Added safe technical allowlist coverage for common platform/package domains while preserving Critical secret detection. +- Reduced fixture placeholder noise without suppressing real source/docs email or secret findings. +- Prepared GitHub Actions workflows for Node 24-ready official action majors and explicit Windows runner labeling. +- Polished Turkish human CLI output while preserving JSON schema fields. +- Bumped source/package metadata and CLI runtime version to `0.1.0-alpha.2`. +- Updated the published-package smoke workflow to install `AgentContextKit` `0.1.0-alpha.2`. +- Recorded successful cross-platform GitHub Actions smoke validation for the published NuGet global tool. +- Synced post-push GitHub release status docs after `master` and `v0.1.0-alpha.1` were pushed. +- Verified NuGet publication and global tool install for `AgentContextKit` version `0.1.0-alpha.1`. +- Verified NuGet global tool smoke test in a clean demo app. + +## [0.1.0-alpha.1] - 2026-06-04 +### Added +- Initial offline-first .NET CLI tool package with command name `ackit`. +- CLI commands: `init`, `scan`, `scan --ci`, `report`, `webui`, `prompt-pack`, `context-export`, `generate`, `task`, `redact-check`, `doctor`, `version`, and `help`. +- Repository scanner for docs, tests, CI, Docker, generated agent files, package metadata, and stack signals. +- Sample-aware main stack detection for `.NET`, `.NET CLI / .NET Tool`, and `GitHub Actions` without treating `samples/` stacks as the main product stack. +- Pattern-based secret, PII, brand, risky path, and risky extension scanning. +- JSON output with schema/tool metadata, generated timestamps, repository metadata, summaries, and CI mode fields. +- Task-first development document generation under `docs/tasks`. +- Agent instruction generation for Codex, Claude, Cursor, and GitHub Copilot. +- Offline static HTML report generation with safe repository-relative output handling. +- Offline static Web UI prototype generation for local scan review. +- Local-only dry-run prompt pack generation and explicit-approval context export manifests. +- English and Turkish output/template foundation. +- Config schema documentation and generated-file conventions. +- Focused xUnit test coverage and GitHub Actions CI. +- Local release verification, package metadata, public release audit, release blocker, public gate, and v1.0 readiness scripts. +- v1.0 final local readiness review documentation and gate script. +- Source archive hygiene docs and WinRAR exclude guidance for local ZIP/RAR sharing. +- OSS readiness, governance, privacy, support, security, package, release, and maintainer handoff documentation. + +### Changed +- Public package and docs metadata use the `Cynrath` persona. +- Package URLs point to `https://github.com/Cynrath/agent-context-kit`. +- Public release blockers track the completed GitHub Release and NuGet publication state, with Codex for OSS submission as the remaining follow-up. + +### Fixed +- Added NuGet package README metadata for local pack readiness. +- Refined self-scan stack accuracy so sample ASP.NET Core, Minimal API, TypeScript, and Tailwind CSS signals are not reported as the main repository stack. diff --git a/tests/AgentContextKit.Tests/AgentContextKitBehaviorTests.cs b/tests/AgentContextKit.Tests/AgentContextKitBehaviorTests.cs index 0af4fe7..8230b09 100644 --- a/tests/AgentContextKit.Tests/AgentContextKitBehaviorTests.cs +++ b/tests/AgentContextKit.Tests/AgentContextKitBehaviorTests.cs @@ -2180,7 +2180,7 @@ public void PackageMetadataAndLicenseUsePseudonym() Assert.Contains("<PackageProjectUrl>https://github.com/Cynrath/agent-context-kit</PackageProjectUrl>", projectFile); Assert.Contains("<RepositoryUrl>https://github.com/Cynrath/agent-context-kit</RepositoryUrl>", projectFile); Assert.Contains("<RepositoryType>git</RepositoryType>", projectFile); - Assert.Contains("<PackageReadmeFile>README.md</PackageReadmeFile>", projectFile); + Assert.Contains("<PackageReadmeFile>README.nuget.md</PackageReadmeFile>", projectFile); Assert.Contains("<ToolCommandName>ackit</ToolCommandName>", projectFile); Assert.Contains("Copyright (c) 2026 Cynrath", license); }