diff --git a/plugin/skills/azure-cloud-migrate/SKILL.md b/plugin/skills/azure-cloud-migrate/SKILL.md index 868d9ce75..d750aff84 100644 --- a/plugin/skills/azure-cloud-migrate/SKILL.md +++ b/plugin/skills/azure-cloud-migrate/SKILL.md @@ -1,10 +1,10 @@ --- name: azure-cloud-migrate -description: "Assess and migrate cross-cloud workloads to Azure. Generates assessment reports and converts code from AWS, GCP, or other providers to Azure services. WHEN: migrate Lambda to Azure Functions, migrate AWS to Azure, Lambda migration assessment, convert AWS serverless to Azure, migration readiness report, migrate from AWS, migrate from GCP, cross-cloud migration." +description: "Assess and migrate cross-cloud workloads to Azure with migration reports and code conversion guidance. Supports AWS, GCP, and other providers. WHEN: migrate Lambda to Azure Functions, migrate AWS to Azure, Lambda migration assessment, convert AWS serverless to Azure, migration readiness report, migrate from AWS, migrate from GCP, cross-cloud migration." license: MIT metadata: author: Microsoft - version: "1.0.0" + version: "1.0.1" --- # Azure Cloud Migrate diff --git a/plugin/skills/azure-compliance/SKILL.md b/plugin/skills/azure-compliance/SKILL.md index 0fe3696a5..bd97ec72a 100644 --- a/plugin/skills/azure-compliance/SKILL.md +++ b/plugin/skills/azure-compliance/SKILL.md @@ -1,10 +1,10 @@ --- name: azure-compliance -description: "Comprehensive Azure compliance and security auditing capabilities including best practices assessment, Key Vault expiration monitoring, and resource configuration validation. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, compliance assessment, resource review, configuration validation, expired certificates, expiring secrets, orphaned resources, policy compliance, security posture evaluation." +description: "Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, run azqr, Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment." license: MIT metadata: author: Microsoft - version: "1.0.2" + version: "1.0.3" --- # Azure Compliance & Security Auditing diff --git a/plugin/skills/azure-cost-optimization/SKILL.md b/plugin/skills/azure-cost-optimization/SKILL.md index 52c59e5d2..4e29bd940 100644 --- a/plugin/skills/azure-cost-optimization/SKILL.md +++ b/plugin/skills/azure-cost-optimization/SKILL.md @@ -1,10 +1,10 @@ --- name: azure-cost-optimization -description: "Identify and quantify cost savings across Azure subscriptions by analyzing actual costs, utilization metrics, and generating actionable optimization recommendations. USE FOR: optimize Azure costs, reduce Azure spending, reduce Azure expenses, analyze Azure costs, find cost savings, generate cost optimization report, find orphaned resources, rightsize VMs, cost analysis, reduce waste, Azure spending analysis, find unused resources, optimize Redis costs. DO NOT USE FOR: deploying resources (use azure-deploy), general Azure diagnostics (use azure-diagnostics), security issues (use azure-security)" +description: "Identify Azure cost savings from usage and spending data. USE FOR: optimize Azure costs, reduce Azure spending/expenses, analyze Azure costs, find cost savings, generate cost optimization report, find orphaned or unused resources, rightsize VMs, reduce waste, optimize Redis costs, optimize storage costs. DO NOT USE FOR: creating new resources, general diagnostics, or security issues." license: MIT metadata: author: Microsoft - version: "1.0.0" + version: "1.0.1" --- # Azure Cost Optimization Skill diff --git a/plugin/skills/azure-kubernetes/SKILL.md b/plugin/skills/azure-kubernetes/SKILL.md new file mode 100644 index 000000000..cb8218bd8 --- /dev/null +++ b/plugin/skills/azure-kubernetes/SKILL.md @@ -0,0 +1,140 @@ +--- +name: azure-kubernetes +license: MIT +metadata: + author: Microsoft + version: "1.0.0" +description: "Plan, create, and configure production-ready Azure Kubernetes Service (AKS) clusters. Covers Day-0 checklist, SKU selection (Automatic vs Standard), networking options (private API server, Azure CNI Overlay, egress configuration), security (workload identity, Azure Policy, Key Vault CSI driver, Deployment Safeguards), and operations (monitoring, upgrade strategy, autoscaling, cost analysis, node pools). WHEN: provision AKS cluster, design AKS networking, choose AKS SKU, secure AKS, set up AKS." +--- + +# Azure Kubernetes Service + +> **AUTHORITATIVE GUIDANCE — MANDATORY COMPLIANCE** +> +> This skill produces a **recommended AKS cluster configuration** based on user requirements, distinguishing **Day-0 decisions** (networking, API server — hard to change later) from **Day-1 features** (can enable post-creation). See [CLI reference](./references/cli-reference.md) for commands. + +## Quick Reference +| Property | Value | +|----------|-------| +| Best for | AKS cluster planning and Day-0 decisions | +| MCP Tools | `mcp_azure_mcp_aks` | +| CLI | `az aks create`, `az aks show`, `kubectl get`, `kubectl describe` | +| Related skills | azure-diagnostics (troubleshooting AKS), azure-deploy (app deployment) | + +## When to Use This Skill +Activate this skill when user wants to: +- Create a new AKS cluster +- Plan AKS cluster configuration for production workloads +- Design AKS networking (API server access, pod IP model, egress) +- Set up AKS identity and secrets management +- Configure AKS governance (Azure Policy, Deployment Safeguards) +- Enable AKS observability (monitoring, Prometheus, Grafana) +- Define AKS upgrade and patching strategy +- Enable AKS cost visibility and analysis +- Understand AKS Automatic vs Standard SKU differences +- Get a Day-0 checklist for AKS cluster setup and configuration + +## Rules +1. Start with the user's requirements for provisioning compute, networking, security, and other settings. +2. Use the `azure` MCP server and its AKS-related MCP tools (`mcp_azure_mcp_aks`, `mcp_aks_mcp_az_aks_operations`) to invoke Azure APIs and perform AKS and kubectl operations; fall back to Azure CLI (`az aks`) only when required functionality is not available via MCP tools. +3. Determine if AKS Automatic or Standard SKU is more appropriate based on the user's need for control vs convenience. Default to AKS Automatic unless specific customizations are required. +4. Document decisions and rationale for cluster configuration choices, especially for Day-0 decisions that are hard to change later (networking, API server access). + + +## Required Inputs (Ask only what’s needed) +If the user is unsure, use safe defaults. +- Cluster environment: dev/test or production +- Region(s), availability zones, preferred node VM sizes +- Expected scale (node/cluster count, workload size) +- Networking requirements (API server access, pod IP model, ingress/egress control) +- Security and identity requirements, including image registry +- Upgrade and observability preferences +- Cost constraints + +## Workflow + +### 1. Cluster Type +- **AKS Automatic** (default): Best for most production workloads, provides a curated experience with pre-configured best practices for security, reliability, and performance. Use unless you have specific custom requirements for networking, autoscaling, or node pool configurations not supported by Node Auto-Provisioning (NAP). +- **AKS Standard**: Use if you need full control over cluster configuration, will require additional overhead to set up and manage. + +### 2. Networking (Pod IP, Egress, Ingress, Dataplane) + +**Pod IP Model** (Key Day-0 decision): +- **Azure CNI Overlay** (recommended): pod IPs from private overlay range, not VNet-routable, scales to large clusters and good for most workloads +- **Azure CNI (VNet-routable)**: pod IPs directly from VNet (pod subnet or node subnet), use when pods must be directly addressable from VNet or on-prem + - Docs: https://learn.microsoft.com/azure/aks/azure-cni-overlay + +**Dataplane & Network Policy**: +- **Azure CNI powered by Cilium** (recommended): eBPF-based for high-performance packet processing, network policies, and observability + +**Egress**: +- **Static Egress Gateway** for stable, predictable outbound IPs +- For restricted egress: UDR + Azure Firewall or NVA + +**Ingress**: +- **App Routing addon with Gateway API** — recommended default for HTTP/HTTPS workloads +- **Istio service mesh with Gateway API** — for advanced traffic management, mTLS, canary deployments +- **Application Gateway for Containers** — for L7 load balancing with WAF integration + +**DNS**: +- Enable **LocalDNS** on all node pools for reliable, performant DNS resolution + +### 3. Security +- Use **Microsoft Entra ID** everywhere (control plane, Workload Identity for pods, node access). Avoid static credentials. +- Azure Key Vault via **Secrets Store CSI Driver** for secrets +- Enable **Azure Policy** + **Deployment Safeguards** +- Enable **Encryption at rest** for etcd/API server; **in-transit** for node-to-node +- Allow only signed, policy-approved images (Azure Policy + Ratify), prefer **Azure Container Registry** +- **Isolation**: Use namespaces, network policies, scoped logging + +### 4. Observability +- Use Azure Monitor and Container Insights for AKS monitoring enablement (logs + Prometheus + Grafana). + +### 5. Upgrades & Patching +- Configure **Maintenance Windows** for controlled upgrade timing +- Enable **auto-upgrades** for cluster and node OS to stay up-to-date with security patches and Kubernetes versions +- Consider **LTS versions** for enterprise stability (2-year support) by upgrading your cluster to the AKS Premium tier +- **Multi-cluster upgrades**: Use **AKS Fleet Manager** for staged rollout across test → production clusters + +### 6. Performance +- Use **Ephemeral OS disks** (`--node-osdisk-type Ephemeral`) for faster node startup +- Select **Azure Linux** as node OS (smaller footprint, faster boot) +- Enable **KEDA** for event-driven autoscaling beyond HPA + +### 7. Node Pools & Compute +- **Dedicated system node pool**: At least 2 nodes, tainted for system workloads only (`CriticalAddonsOnly`) +- Enable **Node Auto Provisioning (NAP)** on all pools for cost savings and responsive scaling +- Use **latest generation SKUs (v5/v6)** for host-level optimizations +- **Avoid B-series VMs** — burstable SKUs cause performance/reliability issues +- Use SKUs with **at least 4 vCPUs** for production workloads +- Set **topology spread constraints** to distribute pods across hosts/zones per SLO + +### 8. Reliability +- Deploy across **3 Availability Zones** (`--zones 1 2 3`) +- Use **Standard tier** for zone-redundant control plane + 99.95% SLA for API server availability +- Enable **Microsoft Defender for Containers** for runtime protection +- Configure **PodDisruptionBudgets** for all production workloads +- Use **topology spread constraints** to ensure pod distribution across failure domains + +### 9. Cost Controls +- Use **Spot node pools** for batch/interruptible workloads (up to 90% savings) +- **Stop/Start** dev/test clusters: `az aks stop/start` +- Consider **Reserved Instances** or **Savings Plans** for steady-state workloads + +## Guardrails / Safety +- Do not request or output secrets (tokens, keys, subscription IDs). +- If requirements are ambiguous for day-0 critical decisions, ask the user clarifying questions. For day-1 enabled features, propose 2–3 safe options with tradeoffs and choose a conservative default. +- Do not promise zero downtime; advise workload safeguards (PDBs, probes, replicas) and staged upgrades along with best practices for reliability and performance. + +## MCP Tools +| Tool | Purpose | Key Parameters | +|------|---------|----------------| +| `mcp_azure_mcp_aks` | Create and query AKS clusters at subscription scope | `subscription_id`, `resource_group` | + +## Error Handling +| Error / Symptom | Likely Cause | Remediation | +|-----------------|--------------|-------------| +| MCP tool call fails or times out | Invalid credentials, subscription, or cluster context | Verify `az login`, check subscription ID and resource group | +| Quota exceeded | Regional vCPU or resource limits | Request quota increase or select different region/VM SKU | +| Networking conflict (IP exhaustion) | Pod subnet too small for overlay/CNI | Re-plan IP ranges; may require cluster recreation (Day-0) | +| Workload Identity not working | Missing OIDC issuer or federated credential | Enable `--enable-oidc-issuer --enable-workload-identity`, configure federated identity | diff --git a/plugin/skills/azure-kubernetes/references/cli-reference.md b/plugin/skills/azure-kubernetes/references/cli-reference.md new file mode 100644 index 000000000..5969dcd6c --- /dev/null +++ b/plugin/skills/azure-kubernetes/references/cli-reference.md @@ -0,0 +1,33 @@ +# CLI Reference for AKS + +```bash +# List AKS clusters +az aks list --output table + +# Show cluster details +az aks show --name --resource-group + +# Get available Kubernetes versions +az aks get-versions --location --output table + +# Create AKS Automatic cluster +az aks create --name --resource-group --sku automatic \ + --network-plugin azure --network-plugin-mode overlay \ + --enable-oidc-issuer --enable-workload-identity + +# Create AKS Standard cluster +az aks create --name --resource-group \ + --node-count 3 --zones 1 2 3 \ + --network-plugin azure --network-plugin-mode overlay \ + --enable-cluster-autoscaler --min-count 1 --max-count 10 + +# Get credentials +az aks get-credentials --name --resource-group + +# List node pools +az aks nodepool list --cluster-name --resource-group --output table + +# Enable monitoring +az aks enable-addons --name --resource-group \ + --addons monitoring --workspace-resource-id +``` \ No newline at end of file diff --git a/plugin/skills/azure-prepare/SKILL.md b/plugin/skills/azure-prepare/SKILL.md index 454d56807..4261b1286 100644 --- a/plugin/skills/azure-prepare/SKILL.md +++ b/plugin/skills/azure-prepare/SKILL.md @@ -4,7 +4,7 @@ description: "Prepare Azure apps for deployment (infra Bicep/Terraform, azure.ya license: MIT metadata: author: Microsoft - version: "1.0.6" + version: "1.0.7" --- # Azure Prepare diff --git a/plugin/skills/azure-prepare/references/architecture.md b/plugin/skills/azure-prepare/references/architecture.md index dba46e7fb..44b57c0f1 100644 --- a/plugin/skills/azure-prepare/references/architecture.md +++ b/plugin/skills/azure-prepare/references/architecture.md @@ -22,6 +22,32 @@ Select hosting stack and map components to Azure services. | Workflow / orchestration | | ✓✓ (Durable Functions + DTS) | | | Minimal ops overhead | | ✓✓ | ✓ | +### Container Hosting: Container Apps vs AKS + +| Factor | Container Apps | AKS | +|--------|:--------------:|:---:| +| **Scale to zero** | ✓✓ | | +| **Kubernetes API access** | | ✓✓ | +| **Custom operators/CRDs** | | ✓✓ | +| **Service mesh** | Dapr (built-in) | Istio, Cilium | +| **GPU workloads** | | ✓✓ | +| **Best for** | Microservices, event-driven | Full K8s control, complex workloads | + +#### When to Use Container Apps +- Microservices without Kubernetes complexity +- Event-driven workloads (KEDA built-in) +- Need scale-to-zero for cost optimization +- Teams without Kubernetes expertise + +#### When to Use AKS +- Need Kubernetes API/kubectl access +- Require custom operators or CRDs +- Service mesh requirements (Istio, Linkerd) +- GPU/ML workloads +- Complex networking or multi-tenant architectures + +> **AKS Planning:** For AKS SKU selection (Automatic vs Standard), networking, identity, scaling, and security configuration, invoke the **azure-kubernetes** skill. + ## Service Mapping ### Hosting @@ -29,11 +55,13 @@ Select hosting stack and map components to Azure services. | Component Type | Primary Service | Alternatives | |----------------|-----------------|--------------| | SPA Frontend | Static Web Apps | Blob + CDN | -| SSR Web App | Container Apps | App Service | -| REST/GraphQL API | Container Apps | App Service, Functions | -| Background Worker | Container Apps | Functions | -| Scheduled Task | Functions (Timer) | Container Apps Jobs | -| Event Processor | Functions | Container Apps | +| SSR Web App | Container Apps | App Service, AKS | +| REST/GraphQL API | Container Apps | App Service, Functions, AKS | +| Background Worker | Container Apps | Functions, AKS | +| Scheduled Task | Functions (Timer) | Container Apps Jobs, Kubernetes CronJob (on AKS) | +| Event Processor | Functions | Container Apps, AKS + KEDA | +| Microservices (full K8s) | AKS | Container Apps | +| GPU/ML Workloads | AKS | Azure ML | ### Data diff --git a/plugin/skills/azure-resource-lookup/SKILL.md b/plugin/skills/azure-resource-lookup/SKILL.md index 093ec884d..0c796591a 100644 --- a/plugin/skills/azure-resource-lookup/SKILL.md +++ b/plugin/skills/azure-resource-lookup/SKILL.md @@ -1,10 +1,10 @@ --- name: azure-resource-lookup -description: "List, find, and show Azure resources. Answers \"list my VMs\", \"show my storage accounts\", \"list websites\", \"find container apps\", \"what resources do I have\", and similar queries for any Azure resource type. USE FOR: list resources, list virtual machines, list VMs, list storage accounts, list websites, list web apps, list container apps, show resources, find resources, what resources do I have, list resources in resource group, list resources in subscription, find resources by tag, find orphaned resources, resource inventory, count resources by type, cross-subscription resource query, Azure Resource Graph, resource discovery, list container registries, list SQL servers, list Key Vaults, show resource groups, list app services, find resources across subscriptions, find unattached disks, tag analysis. DO NOT USE FOR: deploying resources (use azure-deploy), creating or modifying resources, cost optimization (use azure-cost-optimization), writing application code, non-Azure clouds." -license: MIT -metadata: - author: Microsoft - version: "1.0.0" +description: "List, find, and show Azure resources across subscriptions or resource groups. Handles prompts like \"list websites\", \"list virtual machines\", \"list my VMs\", \"show storage accounts\", \"find container apps\", and \"what resources do I have\". USE FOR: resource inventory, find resources by tag, tag analysis, orphaned resource discovery, unattached disks, count resources by type, cross-subscription lookup, and Azure Resource Graph queries. DO NOT USE FOR: deploying/changing resources, cost optimization, or non-Azure clouds." +license: MIT +metadata: + author: Microsoft + version: "1.0.1" --- # Azure Resource Lookup diff --git a/tests/azure-cloud-migrate/__snapshots__/triggers.test.ts.snap b/tests/azure-cloud-migrate/__snapshots__/triggers.test.ts.snap index 3d87b755a..263aa1c05 100644 --- a/tests/azure-cloud-migrate/__snapshots__/triggers.test.ts.snap +++ b/tests/azure-cloud-migrate/__snapshots__/triggers.test.ts.snap @@ -2,21 +2,21 @@ exports[`azure-cloud-migrate - Trigger Tests Trigger Keywords Snapshot skill description triggers match snapshot 1`] = ` { - "description": "Assess and migrate cross-cloud workloads to Azure. Generates assessment reports and converts code from AWS, GCP, or other providers to Azure services. WHEN: migrate Lambda to Azure Functions, migrate AWS to Azure, Lambda migration assessment, convert AWS serverless to Azure, migration readiness report, migrate from AWS, migrate from GCP, cross-cloud migration.", + "description": "Assess and migrate cross-cloud workloads to Azure with migration reports and code conversion guidance. Supports AWS, GCP, and other providers. WHEN: migrate Lambda to Azure Functions, migrate AWS to Azure, Lambda migration assessment, convert AWS serverless to Azure, migration readiness report, migrate from AWS, migrate from GCP, cross-cloud migration.", "extractedKeywords": [ "assess", "assessment", "azure", "cloud", "code", + "conversion", "convert", - "converts", "cross-cloud", "deploy", "from", "function", "functions", - "generates", + "guidance", "lambda", "mcp", "migrate", @@ -27,8 +27,9 @@ exports[`azure-cloud-migrate - Trigger Tests Trigger Keywords Snapshot skill des "report", "reports", "serverless", - "services", + "supports", "when", + "with", "workloads", ], "name": "azure-cloud-migrate", @@ -42,14 +43,14 @@ exports[`azure-cloud-migrate - Trigger Tests Trigger Keywords Snapshot skill key "azure", "cloud", "code", + "conversion", "convert", - "converts", "cross-cloud", "deploy", "from", "function", "functions", - "generates", + "guidance", "lambda", "mcp", "migrate", @@ -60,8 +61,9 @@ exports[`azure-cloud-migrate - Trigger Tests Trigger Keywords Snapshot skill key "report", "reports", "serverless", - "services", + "supports", "when", + "with", "workloads", ] `; diff --git a/tests/azure-compliance/__snapshots__/triggers.test.ts.snap b/tests/azure-compliance/__snapshots__/triggers.test.ts.snap index bcba6bb56..62c3d2e04 100644 --- a/tests/azure-compliance/__snapshots__/triggers.test.ts.snap +++ b/tests/azure-compliance/__snapshots__/triggers.test.ts.snap @@ -2,47 +2,43 @@ exports[`azure-compliance - Trigger Tests Trigger Keywords Snapshot skill description triggers match snapshot 1`] = ` { - "description": "Comprehensive Azure compliance and security auditing capabilities including best practices assessment, Key Vault expiration monitoring, and resource configuration validation. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, compliance assessment, resource review, configuration validation, expired certificates, expiring secrets, orphaned resources, policy compliance, security posture evaluation.", + "description": "Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, run azqr, Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.", "extractedKeywords": [ "assessment", "audit", - "auditing", + "audits", "authentication", "azqr", "azure", - "before", "best", - "capabilities", + "best-practice", "certificates", "check", + "checks", "compliance", - "comprehensive", - "configuration", - "evaluation", + "covers", "expiration", "expired", "expiring", - "including", "key vault", "keyvault", "mcp", "monitor", - "monitoring", "orphaned", + "plus", "policy", "posture", "practices", "resource", "resources", "review", - "running", "scan", "secrets", "security", - "tool", "validation", "vault", "when", + "with", ], "name": "azure-compliance", } @@ -52,42 +48,38 @@ exports[`azure-compliance - Trigger Tests Trigger Keywords Snapshot skill keywor [ "assessment", "audit", - "auditing", + "audits", "authentication", "azqr", "azure", - "before", "best", - "capabilities", + "best-practice", "certificates", "check", + "checks", "compliance", - "comprehensive", - "configuration", - "evaluation", + "covers", "expiration", "expired", "expiring", - "including", "key vault", "keyvault", "mcp", "monitor", - "monitoring", "orphaned", + "plus", "policy", "posture", "practices", "resource", "resources", "review", - "running", "scan", "secrets", "security", - "tool", "validation", "vault", "when", + "with", ] `; diff --git a/tests/azure-cost-optimization/__snapshots__/triggers.test.ts.snap b/tests/azure-cost-optimization/__snapshots__/triggers.test.ts.snap index 428a8c2cc..65acdaab0 100644 --- a/tests/azure-cost-optimization/__snapshots__/triggers.test.ts.snap +++ b/tests/azure-cost-optimization/__snapshots__/triggers.test.ts.snap @@ -2,43 +2,33 @@ exports[`azure-cost-optimization - Trigger Tests Trigger Keywords Snapshot skill description triggers match snapshot 1`] = ` { - "description": "Identify and quantify cost savings across Azure subscriptions by analyzing actual costs, utilization metrics, and generating actionable optimization recommendations. USE FOR: optimize Azure costs, reduce Azure spending, reduce Azure expenses, analyze Azure costs, find cost savings, generate cost optimization report, find orphaned resources, rightsize VMs, cost analysis, reduce waste, Azure spending analysis, find unused resources, optimize Redis costs. DO NOT USE FOR: deploying resources (use azure-deploy), general Azure diagnostics (use azure-diagnostics), security issues (use azure-security)", + "description": "Identify Azure cost savings from usage and spending data. USE FOR: optimize Azure costs, reduce Azure spending/expenses, analyze Azure costs, find cost savings, generate cost optimization report, find orphaned or unused resources, rightsize VMs, reduce waste, optimize Redis costs, optimize storage costs. DO NOT USE FOR: creating new resources, general diagnostics, or security issues.", "extractedKeywords": [ - "across", - "actionable", - "actual", - "analysis", "analyze", - "analyzing", "app service", "azure", - "azure-deploy", - "azure-diagnostics", - "azure-security", "cli", "container", "cosmos", "cost", "costs", - "deploying", + "creating", + "data", "diagnostics", "expenses", "find", + "from", "function", "general", "generate", - "generating", "identify", "issues", "key vault", "mcp", - "metrics", "monitor", "optimization", "optimize", "orphaned", - "quantify", - "recommendations", "redis", "reduce", "report", @@ -49,9 +39,8 @@ exports[`azure-cost-optimization - Trigger Tests Trigger Keywords Snapshot skill "spending", "sql", "storage", - "subscriptions", "unused", - "utilization", + "usage", "validation", "waste", ], @@ -61,41 +50,31 @@ exports[`azure-cost-optimization - Trigger Tests Trigger Keywords Snapshot skill exports[`azure-cost-optimization - Trigger Tests Trigger Keywords Snapshot skill keywords match snapshot 1`] = ` [ - "across", - "actionable", - "actual", - "analysis", "analyze", - "analyzing", "app service", "azure", - "azure-deploy", - "azure-diagnostics", - "azure-security", "cli", "container", "cosmos", "cost", "costs", - "deploying", + "creating", + "data", "diagnostics", "expenses", "find", + "from", "function", "general", "generate", - "generating", "identify", "issues", "key vault", "mcp", - "metrics", "monitor", "optimization", "optimize", "orphaned", - "quantify", - "recommendations", "redis", "reduce", "report", @@ -106,9 +85,8 @@ exports[`azure-cost-optimization - Trigger Tests Trigger Keywords Snapshot skill "spending", "sql", "storage", - "subscriptions", "unused", - "utilization", + "usage", "validation", "waste", ] diff --git a/tests/azure-kubernetes/__snapshots__/triggers.test.ts.snap b/tests/azure-kubernetes/__snapshots__/triggers.test.ts.snap new file mode 100644 index 000000000..ca0cc9a29 --- /dev/null +++ b/tests/azure-kubernetes/__snapshots__/triggers.test.ts.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`azure-kubernetes - Trigger Tests Trigger Keywords Snapshot skill description triggers match snapshot 1`] = ` +{ + "description": "Plan, create, and configure production-ready Azure Kubernetes Service (AKS) clusters. Covers Day-0 checklist, SKU selection (Automatic vs Standard), networking options (private API server, Azure CNI Overlay, egress configuration), security (workload identity, Azure Policy, Key Vault CSI driver, Deployment Safeguards), and operations (monitoring, upgrade strategy, autoscaling, cost analysis, node pools). WHEN: provision AKS cluster, design AKS networking, choose AKS SKU, secure AKS, set up AKS.", + "extractedKeywords": [ + "aks", + "analysis", + "automatic", + "autoscaling", + "azure", + "checklist", + "choose", + "cli", + "cluster", + "clusters", + "configuration", + "configure", + "container", + "cost", + "covers", + "create", + "day-0", + "deploy", + "deployment", + "design", + "diagnostic", + "driver", + "egress", + "entra", + "function", + "identity", + "key vault", + "kubernetes", + "mcp", + "monitor", + "monitoring", + "networking", + "node", + "observability", + "operations", + "options", + "overlay", + "plan", + "policy", + "pools", + "private", + "production-ready", + "provision", + "safeguards", + "secure", + "security", + "selection", + "server", + "service", + "standard", + "strategy", + "upgrade", + "vault", + "when", + "workload", + ], + "name": "azure-kubernetes", +} +`; + +exports[`azure-kubernetes - Trigger Tests Trigger Keywords Snapshot skill keywords match snapshot 1`] = ` +[ + "aks", + "analysis", + "automatic", + "autoscaling", + "azure", + "checklist", + "choose", + "cli", + "cluster", + "clusters", + "configuration", + "configure", + "container", + "cost", + "covers", + "create", + "day-0", + "deploy", + "deployment", + "design", + "diagnostic", + "driver", + "egress", + "entra", + "function", + "identity", + "key vault", + "kubernetes", + "mcp", + "monitor", + "monitoring", + "networking", + "node", + "observability", + "operations", + "options", + "overlay", + "plan", + "policy", + "pools", + "private", + "production-ready", + "provision", + "safeguards", + "secure", + "security", + "selection", + "server", + "service", + "standard", + "strategy", + "upgrade", + "vault", + "when", + "workload", +] +`; diff --git a/tests/azure-kubernetes/integration.test.ts b/tests/azure-kubernetes/integration.test.ts new file mode 100644 index 000000000..c55e08cd8 --- /dev/null +++ b/tests/azure-kubernetes/integration.test.ts @@ -0,0 +1,136 @@ +/** + * Integration Tests for azure-kubernetes + * + * Tests skill behavior with a real Copilot agent session. + * Runs prompts multiple times to measure skill invocation rate. + * + * Prerequisites: + * 1. npm install -g @github/copilot-cli + * 2. Run `copilot` and authenticate + */ + +import { + useAgentRunner, + doesAssistantMessageIncludeKeyword, + shouldSkipIntegrationTests, + getIntegrationSkipReason +} from "../utils/agent-runner"; +import { softCheckSkill } from "../utils/evaluate"; + +const SKILL_NAME = "azure-kubernetes"; +const RUNS_PER_PROMPT = 2; + +const skipTests = shouldSkipIntegrationTests(); +const skipReason = getIntegrationSkipReason(); + +if (skipTests && skipReason) { + console.log(`⏭️ Skipping integration tests: ${skipReason}`); +} + +const describeIntegration = skipTests ? describe.skip : describe; + +describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { + const agent = useAgentRunner(); + + test("invokes azure-kubernetes skill for AKS cluster creation prompt", async () => { + for (let i = 0; i < RUNS_PER_PROMPT; i++) { + try { + const agentMetadata = await agent.run({ + prompt: "Help me create a production-ready AKS cluster with best practices" + }); + + softCheckSkill(agentMetadata, SKILL_NAME); + } catch (e: unknown) { + if (e instanceof Error && e.message?.includes("Failed to load @github/copilot-sdk")) { + console.log("⏭️ SDK not loadable, skipping test"); + return; + } + throw e; + } + } + }); + + test("responds with Day-0 vs Day-1 guidance", async () => { + for (let i = 0; i < RUNS_PER_PROMPT; i++) { + try { + const agentMetadata = await agent.run({ + prompt: "What Day-0 decisions do I need to make for AKS?" + }); + + const hasDay0Content = doesAssistantMessageIncludeKeyword(agentMetadata, "tier") || + doesAssistantMessageIncludeKeyword(agentMetadata, "networking") || + doesAssistantMessageIncludeKeyword(agentMetadata, "API server"); + expect(hasDay0Content).toBe(true); + } catch (e: unknown) { + if (e instanceof Error && e.message?.includes("Failed to load @github/copilot-sdk")) { + console.log("⏭️ SDK not loadable, skipping test"); + return; + } + throw e; + } + } + }); + + test("recommends AKS Automatic vs Standard appropriately", async () => { + for (let i = 0; i < RUNS_PER_PROMPT; i++) { + try { + const agentMetadata = await agent.run({ + prompt: "Should I use AKS Automatic or Standard for my production workload?" + }); + + const hasSkuGuidance = doesAssistantMessageIncludeKeyword(agentMetadata, "Automatic") || + doesAssistantMessageIncludeKeyword(agentMetadata, "Standard"); + expect(hasSkuGuidance).toBe(true); + } catch (e: unknown) { + if (e instanceof Error && e.message?.includes("Failed to load @github/copilot-sdk")) { + console.log("⏭️ SDK not loadable, skipping test"); + return; + } + throw e; + } + } + }); + + test("provides networking recommendations", async () => { + for (let i = 0; i < RUNS_PER_PROMPT; i++) { + try { + const agentMetadata = await agent.run({ + prompt: "How should I configure AKS networking for pods that need VNet-routable IPs?" + }); + + const hasNetworkingContent = doesAssistantMessageIncludeKeyword(agentMetadata, "CNI") || + doesAssistantMessageIncludeKeyword(agentMetadata, "overlay") || + doesAssistantMessageIncludeKeyword(agentMetadata, "VNet"); + expect(hasNetworkingContent).toBe(true); + } catch (e: unknown) { + if (e instanceof Error && e.message?.includes("Failed to load @github/copilot-sdk")) { + console.log("⏭️ SDK not loadable, skipping test"); + return; + } + throw e; + } + } + }); + + test("covers security best practices", async () => { + for (let i = 0; i < RUNS_PER_PROMPT; i++) { + try { + const agentMetadata = await agent.run({ + prompt: "What security best practices should I follow for AKS?" + }); + + const hasSecurityContent = doesAssistantMessageIncludeKeyword(agentMetadata, "identity") || + doesAssistantMessageIncludeKeyword(agentMetadata, "Entra") || + doesAssistantMessageIncludeKeyword(agentMetadata, "workload") || + doesAssistantMessageIncludeKeyword(agentMetadata, "Key Vault"); + expect(hasSecurityContent).toBe(true); + } catch (e: unknown) { + if (e instanceof Error && e.message?.includes("Failed to load @github/copilot-sdk")) { + console.log("⏭️ SDK not loadable, skipping test"); + return; + } + throw e; + } + } + }); +}); diff --git a/tests/azure-kubernetes/triggers.test.ts b/tests/azure-kubernetes/triggers.test.ts new file mode 100644 index 000000000..b2e8145c2 --- /dev/null +++ b/tests/azure-kubernetes/triggers.test.ts @@ -0,0 +1,145 @@ +/** + * Trigger Tests for azure-kubernetes + * + * Tests that verify the skill triggers on appropriate prompts + * and does NOT trigger on unrelated prompts. + */ + +import { TriggerMatcher } from "../utils/trigger-matcher"; +import { loadSkill, LoadedSkill } from "../utils/skill-loader"; + +const SKILL_NAME = "azure-kubernetes"; + +describe(`${SKILL_NAME} - Trigger Tests`, () => { + let triggerMatcher: TriggerMatcher; + let skill: LoadedSkill; + + beforeAll(async () => { + skill = await loadSkill(SKILL_NAME); + triggerMatcher = new TriggerMatcher(skill); + }); + + describe("Should Trigger", () => { + // Common customer prompts for AKS cluster planning and creation + const shouldTriggerPrompts: string[] = [ + // Cluster creation + "Help me create an AKS cluster", + "I need to set up a new Kubernetes cluster on Azure", + "Create a production-ready AKS cluster with best practices", + "How do I provision an AKS cluster for my team?", + + // Day-0 decisions + "What networking options should I choose for AKS?", + "AKS Day-0 checklist", + "Plan AKS configuration for production", + "Design AKS networking with private API server", + + // SKU selection + "What's the difference between AKS Automatic and Standard?", + "Should I use AKS Automatic or Standard SKU?", + "Help me choose the right AKS cluster SKU", + + // Networking + "Configure AKS with Azure CNI Overlay", + "How do I set up private AKS cluster?", + "AKS egress configuration options", + + // Security + "Configure AKS with workload identity", + "Set up Azure Policy for AKS", + "Set up Key Vault CSI driver for AKS", + "Enable Deployment Safeguards for AKS", + "How do I secure my AKS cluster?", + + // Operations + "Enable monitoring for my AKS cluster", + "Configure AKS upgrade strategy", + "How do I set up AKS autoscaling?", + "AKS cost analysis", + "Configure AKS cluster autoscaling and node pools", + ]; + + test.each(shouldTriggerPrompts)( + 'triggers on: "%s"', + (prompt) => { + const result = triggerMatcher.shouldTrigger(prompt); + expect(result.triggered).toBe(true); + } + ); + }); + + describe("Should NOT Trigger", () => { + // Generic prompts unrelated to Azure/Kubernetes + const genericPrompts: string[] = [ + "What is the weather today?", + "Help me write a poem", + "Explain quantum computing", + "Write a Python script to parse JSON", + "How do I bake a cake?", + ]; + + // Competing cloud providers (without "AKS" or "Azure" keywords) + const otherCloudPrompts: string[] = [ + "How do I use AWS EKS?", + "Help me with GCP GKE", + "Help me with AWS Lambda", + "How do I use Google Cloud Platform?", + "Set up EC2 instances", + "Configure S3 bucket policies", + ]; + + // Generic infrastructure prompts (without Azure keywords) + const genericInfraPrompts: string[] = [ + "Set up a PostgreSQL database", + "Configure nginx load balancer", + "How do I use Docker Compose?", + "Set up Redis caching", + "Configure SSL certificates", + ]; + + const shouldNotTriggerPrompts = [ + ...genericPrompts, + ...otherCloudPrompts, + ...genericInfraPrompts, + ]; + + test.each(shouldNotTriggerPrompts)( + 'does not trigger on: "%s"', + (prompt) => { + const result = triggerMatcher.shouldTrigger(prompt); + expect(result.triggered).toBe(false); + } + ); + }); + + describe("Trigger Keywords Snapshot", () => { + test("skill keywords match snapshot", () => { + expect(triggerMatcher.getKeywords()).toMatchSnapshot(); + }); + + test("skill description triggers match snapshot", () => { + expect({ + name: skill.metadata.name, + description: skill.metadata.description, + extractedKeywords: triggerMatcher.getKeywords() + }).toMatchSnapshot(); + }); + }); + + describe("Edge Cases", () => { + test("handles mixed case input", () => { + const result = triggerMatcher.shouldTrigger("CREATE AN AKS CLUSTER"); + expect(result.triggered).toBe(true); + }); + + test("handles partial matches", () => { + const result = triggerMatcher.shouldTrigger("kubernetes on azure"); + expect(result.triggered).toBe(true); + }); + + test("handles empty prompt", () => { + const result = triggerMatcher.shouldTrigger(""); + expect(result.triggered).toBe(false); + }); + }); +}); diff --git a/tests/azure-kubernetes/unit.test.ts b/tests/azure-kubernetes/unit.test.ts new file mode 100644 index 000000000..a02d23b88 --- /dev/null +++ b/tests/azure-kubernetes/unit.test.ts @@ -0,0 +1,182 @@ +/** + * Unit Tests for azure-kubernetes + * + * Tests skill content and structure without requiring external services. + * Focuses on domain invariants rather than exact formatting. + */ + +import { loadSkill, LoadedSkill } from "../utils/skill-loader"; + +const SKILL_NAME = "azure-kubernetes"; + +describe(`${SKILL_NAME} - Unit Tests`, () => { + let skill: LoadedSkill; + + beforeAll(async () => { + skill = await loadSkill(SKILL_NAME); + }); + + describe("Skill Metadata", () => { + test("has required frontmatter fields", () => { + expect(skill.metadata.name).toBe("azure-kubernetes"); + expect(skill.metadata.description).toBeDefined(); + expect(skill.metadata.description.length).toBeGreaterThan(50); + }); + + test("description contains WHEN triggers", () => { + expect(skill.metadata.description).toMatch(/WHEN:/i); + }); + + test("description mentions key AKS concepts", () => { + const desc = skill.metadata.description.toLowerCase(); + expect(desc).toMatch(/aks|kubernetes/); + // Description should mention core topics covered by the skill + expect(desc).toMatch(/cluster|networking|security|deploy/); + }); + }); + + describe("Day-0 vs Day-1 Guidance", () => { + test("distinguishes between Day-0 and Day-1 decisions", () => { + expect(skill.content).toMatch(/Day-0/i); + expect(skill.content).toMatch(/Day-1/i); + }); + + test("identifies networking as Day-0 decision", () => { + expect(skill.content).toMatch(/networking.*day-0|day-0.*networking/i); + }); + + test("identifies API server access as Day-0 consideration", () => { + expect(skill.content).toMatch(/api server/i); + }); + }); + + describe("Cluster SKU Guidance", () => { + test("covers AKS Automatic SKU", () => { + expect(skill.content).toMatch(/AKS Automatic/i); + }); + + test("covers AKS Standard SKU", () => { + expect(skill.content).toMatch(/AKS Standard/i); + }); + + test("recommends Automatic as default", () => { + expect(skill.content).toMatch(/automatic.*default|default.*automatic/i); + }); + }); + + describe("Networking Guidance", () => { + test("covers Azure CNI options", () => { + expect(skill.content).toMatch(/Azure CNI/i); + }); + + test("covers overlay networking", () => { + expect(skill.content).toMatch(/overlay/i); + }); + + test("covers egress patterns", () => { + expect(skill.content).toMatch(/egress/i); + }); + + test("covers ingress options", () => { + expect(skill.content).toMatch(/ingress/i); + }); + }); + + describe("Security Best Practices", () => { + test("recommends Entra ID / Azure AD", () => { + expect(skill.content).toMatch(/entra|azure ad/i); + }); + + test("recommends Workload Identity", () => { + expect(skill.content).toMatch(/workload identity/i); + }); + + test("recommends Key Vault integration", () => { + expect(skill.content).toMatch(/key vault/i); + }); + + test("warns against static credentials", () => { + expect(skill.content).toMatch(/avoid.*static|static.*credential/i); + }); + + test("mentions Azure Policy", () => { + expect(skill.content).toMatch(/azure policy/i); + }); + }); + + describe("Observability Guidance", () => { + test("mentions monitoring options", () => { + expect(skill.content).toMatch(/monitor|observability/i); + }); + + test("mentions Prometheus", () => { + expect(skill.content).toMatch(/prometheus/i); + }); + + test("mentions Grafana", () => { + expect(skill.content).toMatch(/grafana/i); + }); + }); + + describe("Reliability Patterns", () => { + test("recommends availability zones", () => { + expect(skill.content).toMatch(/availability zone|--zones/i); + }); + + test("mentions PodDisruptionBudgets", () => { + expect(skill.content).toMatch(/poddisruptionbudget|pdb/i); + }); + + test("covers upgrade strategy", () => { + expect(skill.content).toMatch(/upgrade/i); + }); + + test("mentions maintenance windows", () => { + expect(skill.content).toMatch(/maintenance window/i); + }); + }); + + describe("Performance Recommendations", () => { + test("recommends ephemeral OS disks", () => { + expect(skill.content).toMatch(/ephemeral.*disk|--node-osdisk-type ephemeral/i); + }); + + test("warns against B-series VMs", () => { + expect(skill.content).toMatch(/avoid.*b-series|b-series.*avoid/i); + }); + + test("mentions autoscaling", () => { + expect(skill.content).toMatch(/autoscal|cluster.?autoscaler/i); + }); + }); + + describe("MCP Tools Section", () => { + test("lists MCP tools", () => { + expect(skill.content).toMatch(/mcp_azure_mcp_aks|mcp_aks_mcp/i); + }); + + test("has MCP Tools section", () => { + expect(skill.content).toMatch(/## MCP Tools/i); + }); + }); + + describe("Error Handling Section", () => { + test("has Error Handling section", () => { + expect(skill.content).toMatch(/## Error Handling/i); + }); + + test("includes remediation guidance", () => { + expect(skill.content).toMatch(/remediation|quota|policy/i); + }); + }); + + describe("Guardrails", () => { + test("warns about secrets handling", () => { + expect(skill.content).toMatch(/secret|token|key/i); + }); + + test("does not promise zero downtime", () => { + expect(skill.content).toMatch(/do not promise zero downtime/i); + }); + }); +}); diff --git a/tests/azure-resource-lookup/__snapshots__/triggers.test.ts.snap b/tests/azure-resource-lookup/__snapshots__/triggers.test.ts.snap index b8ef067eb..5ccddf8cf 100644 --- a/tests/azure-resource-lookup/__snapshots__/triggers.test.ts.snap +++ b/tests/azure-resource-lookup/__snapshots__/triggers.test.ts.snap @@ -2,70 +2,58 @@ exports[`azure-resource-lookup - Trigger Tests Trigger Keywords Snapshot skill description triggers match snapshot 1`] = ` { - "description": "List, find, and show Azure resources. Answers "list my VMs", "show my storage accounts", "list websites", "find container apps", "what resources do I have", and similar queries for any Azure resource type. USE FOR: list resources, list virtual machines, list VMs, list storage accounts, list websites, list web apps, list container apps, show resources, find resources, what resources do I have, list resources in resource group, list resources in subscription, find resources by tag, find orphaned resources, resource inventory, count resources by type, cross-subscription resource query, Azure Resource Graph, resource discovery, list container registries, list SQL servers, list Key Vaults, show resource groups, list app services, find resources across subscriptions, find unattached disks, tag analysis. DO NOT USE FOR: deploying resources (use azure-deploy), creating or modifying resources, cost optimization (use azure-cost-optimization), writing application code, non-Azure clouds.", + "description": "List, find, and show Azure resources across subscriptions or resource groups. Handles prompts like "list websites", "list virtual machines", "list my VMs", "show storage accounts", "find container apps", and "what resources do I have". USE FOR: resource inventory, find resources by tag, tag analysis, orphaned resource discovery, unattached disks, count resources by type, cross-subscription lookup, and Azure Resource Graph queries. DO NOT USE FOR: deploying/changing resources, cost optimization, or non-Azure clouds.", "extractedKeywords": [ "accounts", "across", "aks", "analysis", - "answers", "app service", - "application", "apps", "azure", - "azure-cost-optimization", - "azure-deploy", + "changing", "cli", "clouds", - "code", "container", "cosmos", "cost", "count", - "creating", "cross-subscription", "deploying", "discovery", "disks", "find", "graph", - "group", "groups", + "handles", "have", "inventory", "key vault", "keyvault", "kubernetes", + "like", "list", "lookup", "machines", "mcp", - "modifying", "monitor", "non-azure", "optimization", "orphaned", + "prompts", "queries", - "query", "rbac", - "registries", "resource", "resources", - "servers", - "services", "show", - "similar", "sql", "storage", - "subscription", "subscriptions", "type", "unattached", - "vaults", "virtual", "websites", "what", - "writing", ], "name": "azure-resource-lookup", } @@ -77,63 +65,51 @@ exports[`azure-resource-lookup - Trigger Tests Trigger Keywords Snapshot skill k "across", "aks", "analysis", - "answers", "app service", - "application", "apps", "azure", - "azure-cost-optimization", - "azure-deploy", + "changing", "cli", "clouds", - "code", "container", "cosmos", "cost", "count", - "creating", "cross-subscription", "deploying", "discovery", "disks", "find", "graph", - "group", "groups", + "handles", "have", "inventory", "key vault", "keyvault", "kubernetes", + "like", "list", "lookup", "machines", "mcp", - "modifying", "monitor", "non-azure", "optimization", "orphaned", + "prompts", "queries", - "query", "rbac", - "registries", "resource", "resources", - "servers", - "services", "show", - "similar", "sql", "storage", - "subscription", "subscriptions", "type", "unattached", - "vaults", "virtual", "websites", "what", - "writing", ] `; diff --git a/tests/skills.json b/tests/skills.json index a6dd4ea0e..41e007466 100644 --- a/tests/skills.json +++ b/tests/skills.json @@ -10,6 +10,7 @@ "azure-deploy", "azure-diagnostics", "azure-hosted-copilot-sdk", + "azure-kubernetes", "azure-kusto", "azure-messaging", "azure-prepare", @@ -26,6 +27,6 @@ "integrationTestSchedule": { "0 5 * * 2-6": "microsoft-foundry", "0 8 * * 2-6": "azure-deploy", - "0 12 * * 2-6": "appinsights-instrumentation,azure-ai,azure-aigateway,azure-cloud-migrate,azure-compliance,azure-compute,azure-cost-optimization,azure-diagnostics,azure-hosted-copilot-sdk,azure-kusto,azure-messaging,azure-prepare,azure-quotas,azure-rbac,azure-resource-lookup,azure-resource-visualizer,azure-storage,azure-upgrade,azure-validate,entra-app-registration" + "0 12 * * 2-6": "appinsights-instrumentation,azure-ai,azure-aigateway,azure-cloud-migrate,azure-compliance,azure-compute,azure-cost-optimization,azure-diagnostics,azure-hosted-copilot-sdk,azure-kubernetes,azure-kusto,azure-messaging,azure-prepare,azure-quotas,azure-rbac,azure-resource-lookup,azure-resource-visualizer,azure-storage,azure-upgrade,azure-validate,entra-app-registration" } } \ No newline at end of file diff --git a/tests/utils/agent-runner.ts b/tests/utils/agent-runner.ts index d3456b52c..878b11040 100644 --- a/tests/utils/agent-runner.ts +++ b/tests/utils/agent-runner.ts @@ -344,23 +344,7 @@ function writeMarkdownReport(config: AgentRunConfig, agentMetadata: AgentMetadat } const markdown = redactSecrets(generateMarkdownReport(config, agentMetadata)); - // Use "wx" flag for atomic create-if-not-exists to prevent race conditions - let reportTargetPath = filePath; - let suffix = 0; - while (true) { - try { - fs.writeFileSync(reportTargetPath, markdown, { encoding: "utf-8", flag: "wx" }); - break; - } catch (err: unknown) { - console.log("File exists", reportTargetPath); - if ((err as { code: string }).code === "EEXIST") { - suffix++; - reportTargetPath = filePath.replace(".md", `-${suffix}.md`); - continue; - } - throw err; - } - } + fs.writeFileSync(filePath, markdown, "utf-8"); // Write structured agent-metadata.json for machine consumption const jsonPath = path.join(dir, "agent-metadata.json"); @@ -373,7 +357,7 @@ function writeMarkdownReport(config: AgentRunConfig, agentMetadata: AgentMetadat fs.writeFileSync(jsonPath, redactSecrets(JSON.stringify(jsonData, null, 2)), "utf-8"); if (process.env.DEBUG) { - console.log(`Markdown report written to: ${reportTargetPath}`); + console.log(`Markdown report written to: ${filePath}`); } // Write token usage JSON alongside the markdown report