From aa6d9aaa6e6d70ea814ce41a6913a94b91eb6711 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 10 Jun 2026 10:33:43 +0200 Subject: [PATCH 01/18] plural Gitops skills --- .../dockerfiles/agent-harness/base.Dockerfile | 1 + .../agent-harness/skills/README.md | 56 ++++ .../skills/infrastructure-stack.md | 255 ++++++++++++++ .../skills/official-cd-extensions.md | 130 ++++++++ .../agent-harness/skills/repo-structure.md | 169 ++++++++++ .../agent-harness/skills/services.md | 311 ++++++++++++++++++ .../agent-harness/system/analyze.md.tmpl | 22 ++ .../agent-harness/system/babysit.md.tmpl | 22 ++ .../agent-harness/system/write.md.tmpl | 22 ++ 9 files changed, 988 insertions(+) create mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/README.md create mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md create mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/official-cd-extensions.md create mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md create mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/services.md diff --git a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile index c0bca9c2e7..558752ba5e 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile +++ b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile @@ -105,6 +105,7 @@ RUN groupadd -g 65532 nonroot && \ WORKDIR /plural COPY deployment-operator/dockerfiles/agent-harness/system /plural/system +COPY deployment-operator/dockerfiles/agent-harness/skills /plural/skills RUN mkdir -p /plural/.opencode && \ mkdir -p /plural/.claude && \ diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md new file mode 100644 index 0000000000..f838f82c92 --- /dev/null +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md @@ -0,0 +1,56 @@ +# Plural GitOps Skills + +Domain-knowledge reference files for AI coding agents working on Plural-managed GitOps +repositories. Read the relevant files **before** making changes. + +## Architecture (read this once) + +Plural CD uses **two cooperating layers**: + +1. **Management controller** (`go/controller`, runs on the mgmt cluster) — watches GitOps CRDs, + syncs them to the Plural Console API. It is a **frontend**: it does not apply workloads to + fleet clusters directly. +2. **Deployment operator agent** (runs on each target cluster) — pulls service definitions from + Console, renders Helm/Kustomize/raw YAML, and applies manifests locally. + +`InfrastructureStack` runs execute as **Batch Jobs** on a cluster (usually mgmt) via the +deployment-operator harness — not on every workload cluster unless configured that way. + +Reference repos: run `plural up` to scaffold a full layout, or study +[pluralsh/demo](https://github.com/pluralsh/demo). Layouts are **not** enforced — repos vary. + +**CRD field reference**: for authoritative `spec` / `status` definitions of every +`deployments.plural.sh/v1alpha1` kind (`Cluster`, `ServiceDeployment`, `GlobalService`, +`InfrastructureStack`, `Pipeline`, `Observer`, …), use the +[Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference). +Agent-side CRDs are documented separately in the +[Agent API Reference](https://docs.plural.sh/api-reference/kubernetes/agent-api-reference). + +## Files + +| File | When to read | +|---|---| +| [`repo-structure.md`](repo-structure.md) | Always — start here. Explains the standard directory layout, CR naming conventions, and how the operator reconciles changes. | +| [`infrastructure-stack.md`](infrastructure-stack.md) | When creating or modifying `InfrastructureStack` CRs, or any Terraform / Terragrunt / Ansible IaC runs. | +| [`services.md`](services.md) | When creating or modifying `ServiceDeployment` or `GlobalService` CRs, or deciding which one to use. | +| [`official-cd-extensions.md`](official-cd-extensions.md) | When you need advanced patterns from official docs: service-of-services, multi-source services, Lua Helm generation, sync controls, observers, and pipelines. | + +## Quick-start checklist + +Before editing any YAML in a Plural GitOps repo: + +1. **Read `repo-structure.md`** to orient yourself in the directory layout. +2. **Identify the CR kind** you are modifying (`ServiceDeployment`, `GlobalService`, + `InfrastructureStack`, etc.). For field-level specs, check the + [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference). +3. **Read the matching skill file** for that kind. +4. **If using advanced CD patterns**, read `official-cd-extensions.md` and follow the + linked official docs for the specific feature. +5. **Check the existing CRs** in the repo for the namespace and naming convention already + in use — follow the same pattern. +6. **Verify cluster handles** with `kubectl get clusters -n ` or by reading the + `clusters/` directory. +7. **Check read-only resources** — if `.status.readonly: true` on a Cluster (or similar), the + CR is observed from Console/Terraform and **must not** be edited as the write path; update + the upstream source instead. + diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md b/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md new file mode 100644 index 0000000000..46707059fb --- /dev/null +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md @@ -0,0 +1,255 @@ +# InfrastructureStack — Terraform / IaC Mapping + +`InfrastructureStack` is the Plural GitOps CRD for IaC. Each CR maps to **stack runs** in +Console that the deployment-operator executes as **Batch Jobs** on `spec.clusterRef` (commonly +the management cluster). + +The management controller syncs the CR to Console; the operator runs plan/apply on git changes, +cron, or manual trigger — it does not continuously reconcile cloud resources like a k8s +controller. + +This document explains the field-level mapping so you can create, read, and modify stacks +correctly. + +--- + +## Minimal Terraform stack + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: InfrastructureStack +metadata: + name: vpc-prod-eu + namespace: infra +spec: + type: TERRAFORM # IaC tool: TERRAFORM | TERRAGRUNT | ANSIBLE | CUSTOM + repositoryRef: + name: infra-repo # GitRepository CR name + namespace: infra + git: + ref: main # Branch, tag, or commit SHA + folder: terraform/vpc # Path inside the repo containing .tf files + clusterRef: + name: mgmt # Cluster where the job pod runs (often mgmt, not workload cluster) + namespace: infra + manageState: true # Plural provisions remote state (optional) +``` + +--- + +## Type → tool mapping + +| `spec.type` | Binary executed | Notes | +|---|---|---| +| `TERRAFORM` | `terraform` (or `tofu` if `configuration.terraform.tofu: true`) | Standard HCL, state managed by Plural or remote backend | +| `TERRAGRUNT` | `terragrunt` | Wraps Terraform; supports HCL-based DRY configs | +| `ANSIBLE` | `ansible-playbook` | Requires `configuration.ansible.playbook` | +| `CUSTOM` | User-supplied image | Full control; no automatic `plan`/`apply` lifecycle | + +--- + +## `spec.git` — locating the IaC source + +```yaml +git: + ref: main # Git ref to check out + folder: terraform/vpc # Subdirectory of the repo to use as working directory +``` + +- The operator checks out `ref` and `cd`s into `folder` before running the tool. +- Use `spec.workdir` as an **override** if the entry-point differs from `git.folder` (e.g. + for Terragrunt repos with external modules that must be run from a parent dir). + +--- + +## `spec.stackDefinitionRef` — reusable stack scaffold + +Point at a `StackDefinition` CR for shared configuration (tool version, hooks, defaults) +instead of duplicating `spec.configuration` on every stack. + +--- + +## `spec.configuration` — tool version & hooks + +```yaml +configuration: + version: "1.9.0" # Pin Terraform/Terragrunt version (image tag) + image: "hashicorp/terraform" # Override default tool image + + terraform: + parallelism: 10 # -parallelism flag + refresh: true # -refresh flag + approveEmpty: true # Auto-approve when plan has zero changes + tofu: true # Use OpenTofu instead of Terraform + tofuRegistry: true # Use OpenTofu registry for providers/modules + + terragrunt: + parallelism: 10 + refresh: true + approveEmpty: true + + ansible: + playbook: site.yml # Playbook to run + inventory: hosts.ini # Inventory file (relative to git.folder) + additionalArgs: ["-v"] + privateKeyFile: /secrets/id_rsa + configFile: ansible.cfg + supportsCheck: true # Whether --check mode is supported + deletePlaybook: delete.yml # Playbook run on stack deletion + + hooks: + - cmd: "pre-plan.sh" + afterStage: INIT # INIT | PLAN | APPLY | DESTROY | ... +``` + +--- + +## `spec.manageState` — Plural-managed Terraform state + +When `true`, Plural provisions and manages an S3/GCS-backed remote state bucket automatically. +When `false` (default), your Terraform code must configure a `backend {}` block to point at +an external state store. + +--- + +## `spec.variables` — variable file injection + +Arbitrary YAML/JSON injected as a variables file before the IaC run. For Terraform this +becomes a `.tfvars.json` file; for Ansible it becomes extra vars. + +```yaml +variables: + region: eu-west-1 + instance_count: 3 +``` + +--- + +## `spec.environment` — environment variable injection + +```yaml +environment: + - name: TF_LOG + value: DEBUG + - name: AWS_ROLE_ARN + secretRef: # Pull from a Kubernetes Secret + name: aws-creds + key: role_arn +``` + +--- + +## `spec.files` — mounting credential files + +```yaml +files: + - mountPath: /root/.aws/credentials + secretRef: + name: aws-credentials # Secret key must be the file content +``` + +This is the escape hatch for tools that read credentials from files rather than env vars. +IRSA / Workload Identity is preferred where available. + +--- + +## `spec.approval` — human gate before apply + +When `approval: true`, a plan is generated and the run pauses until a Plural Console user +approves it. Set this for production stacks where a human review of the diff is required. + +--- + +## `spec.detach` — deletion behavior + +- `detach: false` (default): deleting the CR triggers a `terraform destroy` run. +- `detach: true`: deleting the CR removes it from Plural Console but leaves cloud resources + in place. + +--- + +## `spec.cron` — scheduled runs + +```yaml +cron: + cron: "0 6 * * *" # Standard cron expression + autoApprove: true # Skip approval gate for scheduled runs +``` + +--- + +## Stack run lifecycle + +``` +InfrastructureStack CR created/updated (GitOps repo) + │ + ▼ + Management controller → Console InfrastructureStack + │ + ▼ + Git commit / cron / manual trigger → StackRun (PENDING → RUNNING → …) + │ + ▼ + deployment-operator creates StackRunJob → Batch Job on spec.clusterRef cluster + ┌─────────────────────────────────┐ + │ git clone + cd git.folder │ + │ terraform init │ + │ terraform plan ──► (approval) │ + │ terraform apply │ + └─────────────────────────────────┘ + │ + ▼ + Outputs in Console → available via ServiceDeployment.spec.imports +``` + +--- + +## Importing stack outputs into ServiceDeployments + +```yaml +spec: + imports: + - stackRef: + name: vpc-prod-eu + namespace: infra +``` + +In Liquid templates, outputs appear under `imports..` (see +`repo-structure.md`). + +--- + +## Service-of-services (IaC + CD composition) + +A `ServiceDeployment` can sync a git folder containing **other** Plural CRs (including more +`InfrastructureStack` / `ServiceDeployment` YAML). This batches complex provisioning into one +git-driven tree. See +[git-sourced services](https://docs.plural.sh/plural-features/continuous-deployment/git-service). + +--- + +## Common mistakes + +| Mistake | Fix | +|---|---| +| `git.folder` points at the repo root when `.tf` files are in a subdirectory | Set `git.folder: terraform/` | +| Wrong `clusterRef` — pointing at a workload cluster with no operator | Use the management cluster handle (`mgmt`) unless the workload cluster has the operator installed | +| Missing backend block with `manageState: false` | Either set `manageState: true` or add a `terraform { backend "s3" {} }` block | +| Secrets in `spec.variables` | Use `spec.environment[*].secretRef` or `spec.files` instead | + +--- + +## Official docs references + +- [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference) — `InfrastructureStack`, `StackDefinition`, and related field specs +- Continuous deployment overview: + https://docs.plural.sh/plural-features/continuous-deployment +- Deployment operator behavior: + https://docs.plural.sh/plural-features/continuous-deployment/deployment-operator +- Reconciliation modes (creation/read-only): + https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic + +When stack-adjacent resources are primarily managed by Terraform/Terragrunt, keep ownership +clear: use `InfrastructureStack` as the write path for IaC, and avoid conflicting direct +edits from unrelated CD resources. + diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/official-cd-extensions.md b/go/deployment-operator/dockerfiles/agent-harness/skills/official-cd-extensions.md new file mode 100644 index 0000000000..c15d69f91e --- /dev/null +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/official-cd-extensions.md @@ -0,0 +1,130 @@ +# Official Continuous Deployment Extensions + +This file maps common "advanced" Plural CD workflows to official docs so agents can quickly +jump from CRD basics to the right extension pattern. + +Primary docs entrypoint: +- https://docs.plural.sh/plural-features/continuous-deployment + +CRD field reference (all management kinds): +- https://docs.plural.sh/api-reference/kubernetes/management-api-reference + +--- + +## Service-of-services (recursive GitOps) + +Use this when one `ServiceDeployment` should apply a folder containing other +`deployments.plural.sh` objects (`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, etc.). + +Official docs: +- https://docs.plural.sh/plural-features/continuous-deployment/git-service + +Agent guidance: +- Treat this as a composition/orchestration layer, not an app workload. +- Keep child objects in deterministic folder paths; avoid mixing unrelated environments. +- Expect reconciliation to overwrite manual edits to generated/child resources. + +--- + +## GlobalService fleet templating + +Use `GlobalService` when you need one service template replicated across many clusters, +with small per-cluster overrides. + +Official docs: +- https://docs.plural.sh/plural-features/continuous-deployment/global-service +- https://docs.plural.sh/plural-features/continuous-deployment/service-templating + +Agent guidance: +- Prefer `spec.template` + `spec.context.raw` for cluster-specific values. +- Use cluster metadata/tags for routing and Liquid variables for substitutions. +- If per-cluster behavior diverges heavily, split into multiple `ServiceDeployment`s. + +--- + +## Multi-source services + +Use this when one deployment needs manifests from multiple sources (for example, +operator chart from Helm + CRs from Git). + +Official docs: +- https://docs.plural.sh/plural-features/continuous-deployment/multi-source-services + +Agent guidance: +- Model each source explicitly via `spec.sources`. +- Control render behavior with `spec.renderers` by path. +- Keep source path boundaries clear to avoid accidental file shadowing. + +--- + +## Dynamic Helm values via Lua + +Use this for runtime generation of Helm values or value files from context. + +Official docs: +- https://docs.plural.sh/plural-features/continuous-deployment/helm-service +- https://docs.plural.sh/plural-features/continuous-deployment/lua + +Agent guidance: +- Keep Lua logic deterministic and side-effect free. +- Reserve Lua for cases where static values files or Liquid cannot express required logic. +- Document expected input context near the script. + +--- + +## Resource application and sync behavior + +Official docs: +https://docs.plural.sh/plural-features/continuous-deployment/resource-application-logic + +Supported annotations on **any managed manifest**: + +| Annotation | Effect | +|------------|--------| +| `deployment.plural.sh/sync-options: Replace=True` | Replace semantics (PUT) instead of server-side apply — drops fields absent from desired manifest | +| `deployment.plural.sh/sync-options: Force=True` | On apply failure (e.g. immutable field), delete and recreate | +| `deployment.plural.sh/sync-wave: ""` | Apply ordering — lower numbers first | +| `argocd.argoproj.io/sync-options` | Argo CD-compatible alias (same semantics) | +| `argocd.argoproj.io/sync-wave` | Argo CD-compatible wave alias | + +Hook delete policies and lifecycle hooks are also supported — see the full doc for +pre/post-sync hook resources. + +Agent guidance: +- Use sync waves for strict ordering (CRDs → namespace → operator → app). +- Prefer minimal overrides; document non-obvious ordering in commit messages. + +--- + +## Event-driven updates and promotion workflows + +Use observers/pipelines when deployments should respond to upstream changes or environment +promotion rules. + +Official docs: +- https://docs.plural.sh/plural-features/continuous-deployment/observer +- https://docs.plural.sh/plural-features/continuous-deployment/pipelines + +Agent guidance: +- Use `Observer` for trigger detection (registry/git/other sources) and action execution. +- Use `Pipeline` for deterministic or AI-assisted promotion across environments. +- Keep promotion stages explicit and test-gated. + +--- + +## Operator behavior and ownership boundaries + +The deployment operator is an **API-driven frontend** — it automates Console provisioning and +applies manifests on clusters; it does not replace Terraform for cluster creation. + +Official docs: +- https://docs.plural.sh/plural-features/continuous-deployment/deployment-operator +- https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic + +Agent guidance: +- **Creation mode** CRs: edit the GitOps YAML as the source of truth. +- **Read-only mode** (`.status.readonly: true`): resource owned elsewhere — update Terraform / + Console directly, not the observing CR. +- Avoid conflicting writes: Terraform owns infra, Plural CD owns k8s app manifests unless + explicitly composed via service-of-services. + diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md b/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md new file mode 100644 index 0000000000..1776e6476f --- /dev/null +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md @@ -0,0 +1,169 @@ +# Plural GitOps Repository Structure + +A Plural-managed GitOps repository holds Kubernetes Custom Resource manifests (`apiVersion: +deployments.plural.sh/v1alpha1`) that the **management controller** reconciles into the Plural +Console API. The **deployment-operator agent** on each cluster then applies workloads from +Console. + +Understanding the layout helps you locate, read, and modify the right files without breaking +unrelated services. + +--- + +## Top-level layout + +``` +/ + clusters/ # Cluster CRs (one per managed cluster) + services/ # ServiceDeployment CRs (single-cluster deployments) + global-services/ # GlobalService CRs (fleet-wide deployments) + stacks/ # InfrastructureStack CRs (IaC runs) + projects/ # Project CRs (optional multi-tenancy grouping) + git-repositories/ # GitRepository CRs (SCM source references) + pipelines/ # Pipeline CRs (promotion workflows) + namespaces/ # ManagedNamespace CRs + observers/ # Observer CRs (event-driven triggers) +``` + +> **Note**: Directory names vary by repo. There is no required layout — `plural up` generates +> one reference architecture, but teams often reorganize. Find CRs with: +> +> ```bash +> grep -rl 'kind: ServiceDeployment\|kind: InfrastructureStack\|kind: GlobalService' . +> grep -rl 'deployments.plural.sh/v1alpha1' . +> ``` + +Application manifests and Terraform usually live in **separate git repos** referenced by +`GitRepository` + `spec.git.folder`, not necessarily next to the CR YAML. + +--- + +## Cluster CRs (`clusters/`) + +Each file describes one registered cluster (management or workload). + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: Cluster +metadata: + name: prod-eu-1 + namespace: infra +spec: + handle: prod-eu-1 # Short handle used in ServiceDeployment.spec.cluster + projectRef: + name: my-project + tags: + env: prod + region: eu-west-1 + metadata: # Arbitrary JSON — key for fleet-wide templating + externaldnsRoleArn: arn:aws:iam::123456789012:role/external-dns + domain: prod.example.com +``` + +Key fields: +- **`spec.handle`** — referenced by `ServiceDeployment.spec.cluster` and Plural MCP + `downloadServiceManifests`. Prefer the handle over `metadata.name`. +- **`spec.tags`** — used by `GlobalService` cluster selection. +- **`spec.metadata`** — per-cluster values exposed as `cluster.metadata.*` in Liquid templates + (often populated from Terraform via `plural_cluster`). + +Read-only clusters: if `.status.readonly: true`, the cluster was created elsewhere (e.g. +Terraform). The controller observes it — do not treat the CR as the write path. + +--- + +## GitRepository CRs (`git-repositories/`) + +SCM sources referenced by `ServiceDeployment` and `InfrastructureStack` via `spec.repositoryRef`. +You can also inline `spec.git.url` on a ServiceDeployment without a separate CR. + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: GitRepository +metadata: + name: my-app-repo + namespace: infra +spec: + url: https://github.com/my-org/my-app.git +``` + +Plural is **Flux-interoperable** for repository types — many repos reuse Flux source patterns. + +--- + +## Namespace conventions + +Most CRs share one namespace per repo (`infra`, `plural`, or a project namespace). Match +existing files before creating new ones. + +--- + +## How reconciliation works + +### Management controller (CRD → Console) + +1. Watches CRDs in the GitOps repo (via Flux, app-of-apps, or direct apply). +2. **Creation mode** — creates/updates/deletes the matching Console resource; sets `.status.id`. +3. **Read-only mode** — resource already exists externally; controller sets `.status.id` but + does not push spec changes or delete Console on CR removal. + +See [management-controllers-reconciliation-logic](https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic). + +### Deployment operator (Console → cluster) + +On each target cluster, the agent: +- **ServiceDeployment / GlobalService children** → fetch git/helm, render, apply manifests. +- **InfrastructureStack** → not applied here; stack runs are Batch Jobs (see + `infrastructure-stack.md`). + +--- + +## Common patterns + +### Referencing another CR + +```yaml +clusterRef: + name: prod-eu-1 + namespace: infra +repositoryRef: + name: my-app-repo + namespace: infra +``` + +Shorthand: `spec.cluster: prod-eu-1` instead of `clusterRef` — follow the repo's convention. + +### Liquid templating + +Plural uses [Liquid](https://shopify.github.io/liquid/) (plus a Sprig subset) at **apply time** +on the deployment-operator agent — not in the GitOps CR YAML itself. + +Templatable files (per [official docs](https://docs.plural.sh/plural-features/continuous-deployment/service-templating)): +- Raw YAML: files with a `.liquid` extension (e.g. `deployment.yaml.liquid`) +- Helm: values files ending in `.liquid` + +`spec.templated: true` (default) gates whether templating runs. Available Liquid roots +(from the deployment-operator renderer): + +| Variable | Contents | +|----------|----------| +| `cluster` | `handle`, `name`, `distro`, `tags`, **`metadata`**, `kasUrl`, … | +| `configuration` | `spec.configuration` key-value pairs on the service | +| `contexts` | Named `ServiceContext` configurations | +| `imports` | Stack outputs keyed by stack name → output name | +| `service` | Service name, namespace, helm block | + +Use `cluster.metadata.` for fleet-specific IRSA ARNs, domains, etc. + +--- + +## Official docs + +- [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference) — all `deployments.plural.sh/v1alpha1` CRD types and fields +- [Continuous deployment overview](https://docs.plural.sh/plural-features/continuous-deployment) +- [Git-sourced services / app-of-apps](https://docs.plural.sh/plural-features/continuous-deployment/git-service) +- [Deployment operator](https://docs.plural.sh/plural-features/continuous-deployment/deployment-operator) +- [Controller reconciliation modes](https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic) + +**Rule of thumb**: if Terraform or another tool owns a resource, do not duplicate writes from +GitOps YAML — observe via read-only CRs or stack outputs instead. diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/services.md b/go/deployment-operator/dockerfiles/agent-harness/skills/services.md new file mode 100644 index 0000000000..46305568e6 --- /dev/null +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/services.md @@ -0,0 +1,311 @@ +# GlobalService vs ServiceDeployment + +Plural provides two CRDs for deploying workloads to Kubernetes clusters. Choosing the right +one — and knowing how they relate — is essential for working with GitOps repos correctly. + +--- + +## TL;DR + +| | `ServiceDeployment` | `GlobalService` | +|---|---|---| +| **Target** | Exactly **one** cluster | **All clusters** matching a selector | +| **Manifest** | Defined inline in the CR | Defined in `spec.template` (a `ServiceTemplate`) | +| **Child resources** | None (leaf) | Creates one `ServiceDeployment` per matched cluster | +| **Use when** | Single-env deployment or env-specific config | Fleet-wide services (monitoring, security agents, etc.) | + +--- + +## ServiceDeployment + +A `ServiceDeployment` deploys a workload to **one** cluster. It is the fundamental unit of +deployment in Plural. + +### Minimal example + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: ServiceDeployment +metadata: + name: my-app-prod-eu + namespace: infra +spec: + cluster: prod-eu-1 + repositoryRef: + name: my-app-repo + namespace: infra + git: + ref: main + folder: deploy/k8s + namespace: my-app # Kubernetes namespace on the target cluster +``` + +Inline git URL (no `GitRepository` CR) is also supported: + +```yaml +spec: + cluster: prod-eu-1 + git: + url: git@github.com:my-org/my-app.git + ref: main + folder: kubernetes/manifests + namespace: my-app +``` + +### Key spec fields + +| Field | Purpose | +|---|---| +| `spec.cluster` | Cluster handle — short string matching `Cluster.spec.handle` | +| `spec.clusterRef` | ObjectReference alternative to `spec.cluster` | +| `spec.git` | Git ref + folder for raw/Kustomize manifests | +| `spec.helm` | Helm chart source, values, and release name | +| `spec.kustomize` | Kustomize base path | +| `spec.namespace` | Target namespace on the cluster | +| `spec.configuration` | Non-secret key-value pairs for Liquid templating | +| `spec.configurationRef` | Secret reference for sensitive template values | +| `spec.contexts` | Names of `ServiceContext` CRs to inject | +| `spec.imports` | Consume outputs from `InfrastructureStack` runs | +| `spec.dependencies` | Services that must be healthy first | +| `spec.protect` | Prevent accidental deletion | +| `spec.detach` | Remove from Console without deleting cluster resources | +| `spec.templated` | Enable Liquid templating on raw YAML files (default: true) | +| `spec.syncConfig` | Namespace creation, drift detection, ownership rules | + +### Helm example + +```yaml +spec: + cluster: prod-eu-1 + helm: + chart: nginx + version: "1.2.3" + repository: + name: bitnami + namespace: infra + values: + replicaCount: 2 + service: + type: ClusterIP + valuesFiles: + - values/prod.yaml +``` + +### Kustomize example + +```yaml +spec: + cluster: prod-eu-1 + repositoryRef: + name: my-app-repo + namespace: infra + git: + ref: main + folder: deploy + kustomize: + path: overlays/prod +``` + +--- + +## GlobalService + +A `GlobalService` is a **fleet multiplexer**: it watches all clusters that match its +selector and automatically creates (and manages) one `ServiceDeployment` child per matching +cluster. When clusters are added or removed the controller reconciles accordingly. + +### Minimal example + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: GlobalService +metadata: + name: datadog-agent + namespace: infra +spec: + tags: + env: prod + template: # ServiceTemplate — fields are flat, NOT nested under .spec + namespace: monitoring + repositoryRef: + name: monitoring-repo + namespace: infra + git: + ref: main + folder: agents/datadog + helm: + chart: datadog + version: "3.x" + repository: + name: bitnami + namespace: infra +``` + +### Targeting clusters + +GlobalService offers multiple, composable selectors: + +```yaml +spec: + tags: + env: prod # All clusters tagged env=prod + distro: EKS # Further restrict to EKS clusters only + mgmt: false # Exclude the management cluster (default) + projectRef: + name: platform-project # Only clusters in this project + ignoreClusters: + - staging-eu-1 # Explicit exclusion by handle +``` + +All specified selectors are ANDed together. + +### `spec.template` — the ServiceTemplate + +`spec.template` is a `ServiceTemplate` with the **same top-level fields** as `ServiceSpec` +(`namespace`, `git`, `helm`, `kustomize`, `repositoryRef`, …). Do **not** nest them under +`template.spec`: + +```yaml +spec: + template: + namespace: monitoring + helm: + chart: kube-state-metrics + version: "5.3.0" + repository: + name: prometheus-community + namespace: infra +``` + +Official example: +[Global services](https://docs.plural.sh/plural-features/continuous-deployment/global-service). + +### Per-cluster values — prefer `Cluster.spec.metadata` + +For fleet-wide services (external-dns, IRSA roles, per-cluster domains), put cluster-specific +data on each `Cluster` CR (or via Terraform `plural_cluster.metadata`) and reference it in +Liquid as `cluster.metadata.` inside `.liquid` manifest or values files. + +Example Cluster metadata (from official docs): + +```yaml +spec: + handle: eks-prod + tags: + fabric: eks + metadata: + externaldnsRoleArn: arn:aws:iam::123456789012:role/external-dns + domain: prod.example.com +``` + +### `spec.context` — GlobalService-level template data + +`spec.context.raw` adds YAML available to the template engine across all matched clusters. +Use for defaults; use `Cluster.spec.metadata` when values differ per cluster. + +### `spec.serviceRef` — reuse an existing ServiceDeployment as template + +Instead of writing a full `spec.template`, you can point at an existing `ServiceDeployment` +and the `GlobalService` will clone its spec to all targeted clusters: + +```yaml +spec: + serviceRef: + name: my-app-canary + namespace: infra + tags: + env: staging +``` + +### Deletion cascade + +Control what happens when the `GlobalService` is deleted: + +```yaml +spec: + cascade: + delete: true # Delete child ServiceDeployments AND cluster resources + detach: false # (alternatively) Orphan cluster resources, remove from Console +``` + +--- + +## Finding child ServiceDeployments + +The Console links children via `GlobalService.services`. In a GitOps repo, child +`ServiceDeployment` CRs may also exist — they are **reconciled from** the parent +`GlobalService` and manual edits to children are overwritten. + +To download rendered manifests via Plural MCP `downloadServiceManifests`, use the **child** +`ServiceDeployment` name and cluster handle — not the `GlobalService` name. + +Naming is deterministic (derived from GlobalService name + cluster handle) but varies — list +services in Console or search the repo for CRs targeting the same template source. + +--- + +## Decision guide + +Use **`ServiceDeployment`** when: +- Deploying to a single, known cluster. +- Each environment (dev/staging/prod) needs meaningfully different configuration that + cannot be expressed through a template + context. +- You need fine-grained ordering via `spec.dependencies`. + +Use **`GlobalService`** when: +- The same workload should run on all (or a tagged subset of) clusters — e.g. logging + agents, security scanners, cert-manager, monitoring exporters. +- You want the fleet to stay in sync automatically as new clusters are added. +- Per-cluster overrides are small enough to fit in `spec.context`. + +--- + +## Relationship diagram + +``` +GlobalService (fleet selector) + │ spec.tags: {env: prod} + │ spec.distro: EKS + │ + ├──► creates ──► ServiceDeployment (cluster: prod-eu-1) + ├──► creates ──► ServiceDeployment (cluster: prod-us-1) + └──► creates ──► ServiceDeployment (cluster: prod-ap-1) + │ + └──► deploys ──► Kubernetes workload +``` + +--- + +## Common mistakes + +| Mistake | Fix | +|---|---| +| Editing a child `ServiceDeployment` that belongs to a `GlobalService` | Edit the parent `GlobalService.spec.template` instead — child changes are overwritten on the next reconcile | +| Using `GlobalService` for a service that needs totally different config per cluster | Use separate `ServiceDeployment` CRs per cluster | +| Passing `GlobalService` name to `downloadServiceManifests` | Find and use the child `ServiceDeployment` name | +| Forgetting `spec.mgmt: true` when you want the management cluster included | Set `mgmt: true` explicitly — it defaults to false | + +--- + +## Official docs extensions + +For field-level CRD specs, start with the +[Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference) +(`ServiceDeployment`, `GlobalService`, `ServiceContext`, …). + +For advanced service patterns, cross-check these pages: + +- Global service targeting and cluster-specific templating: + https://docs.plural.sh/plural-features/continuous-deployment/global-service +- Service templating model (Liquid + available data): + https://docs.plural.sh/plural-features/continuous-deployment/service-templating +- Multi-source services (`spec.sources` + `spec.renderers`): + https://docs.plural.sh/plural-features/continuous-deployment/multi-source-services +- Helm service variants (including Lua-driven dynamic values): + https://docs.plural.sh/plural-features/continuous-deployment/helm-service +- Resource application logic (`sync-options`, `sync-wave`): + https://docs.plural.sh/plural-features/continuous-deployment/resource-application-logic + +Use these extensions when one standard `git`/`helm` service definition is not enough, but keep +the base distinction intact: `ServiceDeployment` is single-cluster, `GlobalService` is fleet-wide. + diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl index 703fef0244..d9c9e7abd3 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl @@ -32,6 +32,28 @@ When analysis is finished, you **must** persist the report by calling the Plural --- +## Plural GitOps skills + +If the repository you are analysing is a **Plural-managed GitOps repo** (i.e. it contains +`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` +CRs), read the skill files below **before** starting the environment scan: + +- `/plural/skills/repo-structure.md` — standard directory layout, naming conventions, and + how the Plural operator reconciles changes. +- `/plural/skills/infrastructure-stack.md` — field-by-field mapping between + `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, + variable injection, and lifecycle. +- `/plural/skills/services.md` — when to use `ServiceDeployment` vs `GlobalService`, how + child ServiceDeployments are created by a GlobalService, and common mistakes. +- `/plural/skills/official-cd-extensions.md` — official docs-backed advanced patterns + (service-of-services, multi-source services, Lua Helm config, sync controls, observers, + pipelines). + +Read the `/plural/skills/README.md` index first for architecture context and a quick-start +checklist. + +--- + ## 1. Hard rules You MUST always obey: diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl index 0787c41b74..eb045e4ed7 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl @@ -48,6 +48,28 @@ Docker-in-Docker is enabled. You can use `docker` and `docker-compose` for testi --- {{ end }} +## Plural GitOps skills + +If the repository you are working on is a **Plural-managed GitOps repo** (i.e. it contains +`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` +CRs), read the skill files below **before** analysing or editing any manifests: + +- `/plural/skills/repo-structure.md` — standard directory layout, naming conventions, and + how the Plural operator reconciles changes. +- `/plural/skills/infrastructure-stack.md` — field-by-field mapping between + `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, + variable injection, and lifecycle. +- `/plural/skills/services.md` — when to use `ServiceDeployment` vs `GlobalService`, how + child ServiceDeployments are created by a GlobalService, and common mistakes. +- `/plural/skills/official-cd-extensions.md` — official docs-backed advanced patterns + (service-of-services, multi-source services, Lua Helm config, sync controls, observers, + pipelines). + +Read the `/plural/skills/README.md` index first for architecture context and a quick-start +checklist. + +--- + ## Plural MCP tool hints ### `downloadServiceManifests` diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl index c0c958188e..306dc97259 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl @@ -132,6 +132,28 @@ Each failure → **one** cycle updating only the relevant todo’s `description` --- +## Plural GitOps skills + +If the repository you are working on is a **Plural-managed GitOps repo** (i.e. it contains +`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` +CRs), read the skill files below **before** analysing or editing any manifests: + +- `/plural/skills/repo-structure.md` — standard directory layout, naming conventions, and + how the Plural operator reconciles changes. +- `/plural/skills/infrastructure-stack.md` — field-by-field mapping between + `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, + variable injection, and lifecycle. +- `/plural/skills/services.md` — when to use `ServiceDeployment` vs `GlobalService`, how + child ServiceDeployments are created by a GlobalService, and common mistakes. +- `/plural/skills/official-cd-extensions.md` — official docs-backed advanced patterns + (service-of-services, multi-source services, Lua Helm config, sync controls, observers, + pipelines). + +Read the `/plural/skills/README.md` index first for architecture context and a quick-start +checklist. + +--- + ## Plural MCP tool hints ### `downloadServiceManifests` From f559e94d500362e5c95259b030455646c5a9ee3f Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 10 Jun 2026 16:17:39 +0200 Subject: [PATCH 02/18] update --- .../dockerfiles/agent-harness/skills/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md index f838f82c92..baa3e743f2 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md @@ -13,11 +13,6 @@ Plural CD uses **two cooperating layers**: 2. **Deployment operator agent** (runs on each target cluster) — pulls service definitions from Console, renders Helm/Kustomize/raw YAML, and applies manifests locally. -`InfrastructureStack` runs execute as **Batch Jobs** on a cluster (usually mgmt) via the -deployment-operator harness — not on every workload cluster unless configured that way. - -Reference repos: run `plural up` to scaffold a full layout, or study -[pluralsh/demo](https://github.com/pluralsh/demo). Layouts are **not** enforced — repos vary. **CRD field reference**: for authoritative `spec` / `status` definitions of every `deployments.plural.sh/v1alpha1` kind (`Cluster`, `ServiceDeployment`, `GlobalService`, From 6472419ccd28572dd14f802103acdb9fddd4a59b Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 10 Jun 2026 16:42:11 +0200 Subject: [PATCH 03/18] update --- .../agent-harness/skills/repo-structure.md | 121 ++++++++---------- 1 file changed, 52 insertions(+), 69 deletions(-) diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md b/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md index 1776e6476f..c1e6a2a125 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md @@ -1,46 +1,13 @@ # Plural GitOps Repository Structure A Plural-managed GitOps repository holds Kubernetes Custom Resource manifests (`apiVersion: -deployments.plural.sh/v1alpha1`) that the **management controller** reconciles into the Plural -Console API. The **deployment-operator agent** on each cluster then applies workloads from -Console. +deployments.plural.sh/v1alpha1`). -Understanding the layout helps you locate, read, and modify the right files without breaking -unrelated services. +## Cluster CRs ---- - -## Top-level layout - -``` -/ - clusters/ # Cluster CRs (one per managed cluster) - services/ # ServiceDeployment CRs (single-cluster deployments) - global-services/ # GlobalService CRs (fleet-wide deployments) - stacks/ # InfrastructureStack CRs (IaC runs) - projects/ # Project CRs (optional multi-tenancy grouping) - git-repositories/ # GitRepository CRs (SCM source references) - pipelines/ # Pipeline CRs (promotion workflows) - namespaces/ # ManagedNamespace CRs - observers/ # Observer CRs (event-driven triggers) -``` - -> **Note**: Directory names vary by repo. There is no required layout — `plural up` generates -> one reference architecture, but teams often reorganize. Find CRs with: -> -> ```bash -> grep -rl 'kind: ServiceDeployment\|kind: InfrastructureStack\|kind: GlobalService' . -> grep -rl 'deployments.plural.sh/v1alpha1' . -> ``` - -Application manifests and Terraform usually live in **separate git repos** referenced by -`GitRepository` + `spec.git.folder`, not necessarily next to the CR YAML. - ---- - -## Cluster CRs (`clusters/`) - -Each file describes one registered cluster (management or workload). +Each file **adopts** an existing cluster already registered in Console — it does **not** +provision a new cluster from YAML alone. The controller looks up `spec.handle` in Console, +sets `.status.readonly: true`, and syncs a limited set of fields (tags, metadata, bindings). ```yaml apiVersion: deployments.plural.sh/v1alpha1 @@ -49,30 +16,28 @@ metadata: name: prod-eu-1 namespace: infra spec: - handle: prod-eu-1 # Short handle used in ServiceDeployment.spec.cluster - projectRef: - name: my-project + handle: prod-eu-1 # Required — must match an existing Console cluster handle tags: env: prod region: eu-west-1 - metadata: # Arbitrary JSON — key for fleet-wide templating + metadata: # Arbitrary JSON — exposed in Liquid as cluster.metadata.* externaldnsRoleArn: arn:aws:iam::123456789012:role/external-dns domain: prod.example.com ``` Key fields: -- **`spec.handle`** — referenced by `ServiceDeployment.spec.cluster` and Plural MCP - `downloadServiceManifests`. Prefer the handle over `metadata.name`. -- **`spec.tags`** — used by `GlobalService` cluster selection. -- **`spec.metadata`** — per-cluster values exposed as `cluster.metadata.*` in Liquid templates - (often populated from Terraform via `plural_cluster`). +- **`spec.handle`** — must match a cluster already in Console. Used by + `ServiceDeployment.spec.cluster` and Plural MCP `downloadServiceManifests`. +- **`spec.tags`** — synced to Console; used by `GlobalService` cluster selection. +- **`spec.metadata`** — synced to Console; available as `cluster.metadata.*` in Liquid (often + populated from Terraform via `plural_cluster`). -Read-only clusters: if `.status.readonly: true`, the cluster was created elsewhere (e.g. -Terraform). The controller observes it — do not treat the CR as the write path. +Clusters are provisioned via Terraform or Console — not by applying a bare Cluster CR without +a matching handle. --- -## GitRepository CRs (`git-repositories/`) +## GitRepository CRs SCM sources referenced by `ServiceDeployment` and `InfrastructureStack` via `spec.repositoryRef`. You can also inline `spec.git.url` on a ServiceDeployment without a separate CR. @@ -87,7 +52,8 @@ spec: url: https://github.com/my-org/my-app.git ``` -Plural is **Flux-interoperable** for repository types — many repos reuse Flux source patterns. +Plural CD is **Flux-interoperable** for source types ([deployment operator +docs](https://docs.plural.sh/plural-features/continuous-deployment/deployment-operator)). --- @@ -102,19 +68,32 @@ existing files before creating new ones. ### Management controller (CRD → Console) -1. Watches CRDs in the GitOps repo (via Flux, app-of-apps, or direct apply). -2. **Creation mode** — creates/updates/deletes the matching Console resource; sets `.status.id`. -3. **Read-only mode** — resource already exists externally; controller sets `.status.id` but - does not push spec changes or delete Console on CR removal. +Runs on the **management cluster**. Watches CRDs applied there (Flux, app-of-apps, or +`kubectl apply`). + +| Mode | When | Behavior | +|------|------|----------| +| **Creation** | New CR, no matching Console resource | Creates Console resource; sets `.status.id`; manages updates/deletes via finalizers | +| **Read-only** | Resource already in Console and `spec.reconciliation.driftDetection: false`, or resource owned elsewhere (e.g. Terraform-provisioned Cluster) | Sets `.status.id`; does **not** push spec changes or delete Console on CR removal | + +Most writable GitOps resources (`ServiceDeployment`, `GlobalService`, `InfrastructureStack`) use +**creation mode** when defined only in git. `Cluster` is special: it always adopts by handle and +runs in read-only mode after linking. See [management-controllers-reconciliation-logic](https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic). ### Deployment operator (Console → cluster) -On each target cluster, the agent: -- **ServiceDeployment / GlobalService children** → fetch git/helm, render, apply manifests. -- **InfrastructureStack** → not applied here; stack runs are Batch Jobs (see - `infrastructure-stack.md`). +Runs on **each registered cluster** (agent pod): + +- **ServiceDeployment** (and GlobalService children) → clone git / pull helm, render, apply + manifests on **that** cluster. +- **InfrastructureStack** → stack runs execute as **Batch Jobs** on the cluster referenced by + `InfrastructureStack.spec.clusterRef` (often mgmt), not as ongoing service sync. See + `infrastructure-stack.md`. + +The management controller does not talk to workload clusters directly for CD — Console is the +hub between git CRDs and per-cluster agents. --- @@ -136,24 +115,28 @@ Shorthand: `spec.cluster: prod-eu-1` instead of `clusterRef` — follow the repo ### Liquid templating Plural uses [Liquid](https://shopify.github.io/liquid/) (plus a Sprig subset) at **apply time** -on the deployment-operator agent — not in the GitOps CR YAML itself. +on the deployment-operator agent — not inside GitOps CR YAML. -Templatable files (per [official docs](https://docs.plural.sh/plural-features/continuous-deployment/service-templating)): -- Raw YAML: files with a `.liquid` extension (e.g. `deployment.yaml.liquid`) -- Helm: values files ending in `.liquid` +Templatable files ([official docs](https://docs.plural.sh/plural-features/continuous-deployment/service-templating)): +- Raw services: files whose path ends in **`.liquid`** (e.g. `deployment.yaml.liquid`) +- Helm: values files ending in **`.liquid`** -`spec.templated: true` (default) gates whether templating runs. Available Liquid roots -(from the deployment-operator renderer): +Plain `.yaml` / `.yml` files are **not** Liquid-processed. `spec.templated: true` (default) +enables Liquid only for `.liquid` files; when `false`, even `.liquid` files are skipped. + +Available Liquid roots (from `pkg/manifests/template/`): | Variable | Contents | |----------|----------| -| `cluster` | `handle`, `name`, `distro`, `tags`, **`metadata`**, `kasUrl`, … | +| `cluster` | `handle`, `name`, `distro`, `tags`, `metadata`, `kasUrl`, … | | `configuration` | `spec.configuration` key-value pairs on the service | | `contexts` | Named `ServiceContext` configurations | -| `imports` | Stack outputs keyed by stack name → output name | +| `imports` | Stack outputs: `imports..` | | `service` | Service name, namespace, helm block | -Use `cluster.metadata.` for fleet-specific IRSA ARNs, domains, etc. +Use `cluster.metadata.` for per-cluster IRSA ARNs, domains, etc. + +Helm also supports **Lua** scripts for dynamic values — see `official-cd-extensions.md`. --- @@ -166,4 +149,4 @@ Use `cluster.metadata.` for fleet-specific IRSA ARNs, domains, etc. - [Controller reconciliation modes](https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic) **Rule of thumb**: if Terraform or another tool owns a resource, do not duplicate writes from -GitOps YAML — observe via read-only CRs or stack outputs instead. +GitOps YAML — observe via read-only CRs or consume stack outputs via `spec.imports`. From 3e20b6179040fa687e0cd834b403b48764595a58 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Wed, 10 Jun 2026 17:03:41 +0200 Subject: [PATCH 04/18] add repo --- .../agent-harness/skills/README.md | 8 +- .../agent-harness/skills/git-repository.md | 97 +++++++++++ .../skills/infrastructure-stack.md | 2 +- .../agent-harness/skills/repo-structure.md | 152 ------------------ .../agent-harness/system/analyze.md.tmpl | 4 +- .../agent-harness/system/babysit.md.tmpl | 4 +- .../agent-harness/system/write.md.tmpl | 4 +- 7 files changed, 107 insertions(+), 164 deletions(-) create mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/git-repository.md delete mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md index baa3e743f2..98a7973e91 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md @@ -25,7 +25,7 @@ Agent-side CRDs are documented separately in the | File | When to read | |---|---| -| [`repo-structure.md`](repo-structure.md) | Always — start here. Explains the standard directory layout, CR naming conventions, and how the operator reconciles changes. | +| [`git-repository.md`](git-repository.md) | Always — start here. How GitOps repos relate to source repos, `GitRepository` CRs, and `repositoryRef` / `spec.git` wiring. | | [`infrastructure-stack.md`](infrastructure-stack.md) | When creating or modifying `InfrastructureStack` CRs, or any Terraform / Terragrunt / Ansible IaC runs. | | [`services.md`](services.md) | When creating or modifying `ServiceDeployment` or `GlobalService` CRs, or deciding which one to use. | | [`official-cd-extensions.md`](official-cd-extensions.md) | When you need advanced patterns from official docs: service-of-services, multi-source services, Lua Helm generation, sync controls, observers, and pipelines. | @@ -34,7 +34,7 @@ Agent-side CRDs are documented separately in the Before editing any YAML in a Plural GitOps repo: -1. **Read `repo-structure.md`** to orient yourself in the directory layout. +1. **Read `git-repository.md`** to understand how this repo relates to GitOps CRs and source repos. 2. **Identify the CR kind** you are modifying (`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, etc.). For field-level specs, check the [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference). @@ -43,9 +43,7 @@ Before editing any YAML in a Plural GitOps repo: linked official docs for the specific feature. 5. **Check the existing CRs** in the repo for the namespace and naming convention already in use — follow the same pattern. -6. **Verify cluster handles** with `kubectl get clusters -n ` or by reading the - `clusters/` directory. -7. **Check read-only resources** — if `.status.readonly: true` on a Cluster (or similar), the +6. **Check read-only resources** — if `.status.readonly: true` on a Cluster (or similar), the CR is observed from Console/Terraform and **must not** be edited as the write path; update the upstream source instead. diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/git-repository.md b/go/deployment-operator/dockerfiles/agent-harness/skills/git-repository.md new file mode 100644 index 0000000000..71787c5c87 --- /dev/null +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/git-repository.md @@ -0,0 +1,97 @@ +# GitRepository + +Plural CD separates **where CRs are defined** from **where application or IaC code lives**: + +| Repo role | Contents | Typical path | +|---|---|---| +| **GitOps repo** | Plural management CRs (`ServiceDeployment`, `GitRepository`, `Cluster`, …) | `manifests/*.yaml` | +| **Source repo** | Helm charts, Kustomize, raw YAML, Terraform, etc. | paths referenced by `spec.git.folder` | + +A `GitRepository` CR registers a **source repo** in Console so `ServiceDeployment` and +`InfrastructureStack` can clone it without repeating the URL. + +--- + +## GitRepository CR + +Cluster-scoped (`deployments.plural.sh/v1alpha1`, no namespace): + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: GitRepository +metadata: + name: my-app-repo +spec: + url: https://github.com/my-org/my-app.git + connectionRef: # optional — reuse an ScmConnection for auth + name: github + credentialsRef: # optional — Secret with privateKey, username, password, … + name: my-app-repo-credentials + namespace: infra +``` + +| Field | Purpose | +|---|---| +| `spec.url` | HTTPS or SSH git URL — **immutable** after creation | +| `spec.connectionRef` | Existing SCM connection for credentials | +| `spec.credentialsRef` | Direct Secret reference for repo auth | +| `status.health` | `PULLABLE` or `FAILED` — Console can reach the repo | + +Find existing registrations in the GitOps repo: + +```bash +grep -rl 'kind: GitRepository' manifests/ +``` + +--- + +## How consumers use it + +`ServiceDeployment` and `InfrastructureStack` point at a `GitRepository` via `repositoryRef`, +then specify **where inside that repo** to read with `spec.git`: + +```yaml +apiVersion: deployments.plural.sh/v1alpha1 +kind: ServiceDeployment +metadata: + name: my-app-prod + namespace: infra +spec: + cluster: prod-eu-1 + repositoryRef: + name: my-app-repo + git: + ref: main + folder: deploy/k8s + namespace: my-app +``` + +| Field | Purpose | +|---|---| +| `repositoryRef.name` | Name of the `GitRepository` CR | +| `spec.git.ref` | Branch, tag, or commit | +| `spec.git.folder` | Subdirectory with manifests, chart, or Terraform | + +**Inline URL** (no `GitRepository` CR) is also supported — set `spec.git.url` directly on the +consumer CR instead of `repositoryRef`. + +--- + +## Working in an unfamiliar repo + +**If you are in the GitOps repo** — read `manifests/` for `GitRepository`, `ServiceDeployment`, +and `Cluster` CRs. Use `Cluster` resources with `apiVersion: deployments.plural.sh/v1alpha1` +(not CAPI `cluster.x-k8s.io`) to find valid `spec.handle` values for `spec.cluster`. + +**If you are in a source repo** — find the matching `GitRepository.spec.url` and follow +`repositoryRef` + `spec.git.folder` on the consuming CRs in the GitOps repo's `manifests/`. + +--- + +## Official docs + +- [Management API Reference — GitRepository](https://docs.plural.sh/api-reference/kubernetes/management-api-reference) +- [Git-sourced services](https://docs.plural.sh/plural-features/continuous-deployment/git-service) +- [Deployment operator](https://docs.plural.sh/plural-features/continuous-deployment/deployment-operator) + +Plural CD is **Flux-interoperable** for source types. diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md b/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md index 46707059fb..fded2947ec 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md @@ -215,7 +215,7 @@ spec: ``` In Liquid templates, outputs appear under `imports..` (see -`repo-structure.md`). +`services.md`). --- diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md b/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md deleted file mode 100644 index c1e6a2a125..0000000000 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/repo-structure.md +++ /dev/null @@ -1,152 +0,0 @@ -# Plural GitOps Repository Structure - -A Plural-managed GitOps repository holds Kubernetes Custom Resource manifests (`apiVersion: -deployments.plural.sh/v1alpha1`). - -## Cluster CRs - -Each file **adopts** an existing cluster already registered in Console — it does **not** -provision a new cluster from YAML alone. The controller looks up `spec.handle` in Console, -sets `.status.readonly: true`, and syncs a limited set of fields (tags, metadata, bindings). - -```yaml -apiVersion: deployments.plural.sh/v1alpha1 -kind: Cluster -metadata: - name: prod-eu-1 - namespace: infra -spec: - handle: prod-eu-1 # Required — must match an existing Console cluster handle - tags: - env: prod - region: eu-west-1 - metadata: # Arbitrary JSON — exposed in Liquid as cluster.metadata.* - externaldnsRoleArn: arn:aws:iam::123456789012:role/external-dns - domain: prod.example.com -``` - -Key fields: -- **`spec.handle`** — must match a cluster already in Console. Used by - `ServiceDeployment.spec.cluster` and Plural MCP `downloadServiceManifests`. -- **`spec.tags`** — synced to Console; used by `GlobalService` cluster selection. -- **`spec.metadata`** — synced to Console; available as `cluster.metadata.*` in Liquid (often - populated from Terraform via `plural_cluster`). - -Clusters are provisioned via Terraform or Console — not by applying a bare Cluster CR without -a matching handle. - ---- - -## GitRepository CRs - -SCM sources referenced by `ServiceDeployment` and `InfrastructureStack` via `spec.repositoryRef`. -You can also inline `spec.git.url` on a ServiceDeployment without a separate CR. - -```yaml -apiVersion: deployments.plural.sh/v1alpha1 -kind: GitRepository -metadata: - name: my-app-repo - namespace: infra -spec: - url: https://github.com/my-org/my-app.git -``` - -Plural CD is **Flux-interoperable** for source types ([deployment operator -docs](https://docs.plural.sh/plural-features/continuous-deployment/deployment-operator)). - ---- - -## Namespace conventions - -Most CRs share one namespace per repo (`infra`, `plural`, or a project namespace). Match -existing files before creating new ones. - ---- - -## How reconciliation works - -### Management controller (CRD → Console) - -Runs on the **management cluster**. Watches CRDs applied there (Flux, app-of-apps, or -`kubectl apply`). - -| Mode | When | Behavior | -|------|------|----------| -| **Creation** | New CR, no matching Console resource | Creates Console resource; sets `.status.id`; manages updates/deletes via finalizers | -| **Read-only** | Resource already in Console and `spec.reconciliation.driftDetection: false`, or resource owned elsewhere (e.g. Terraform-provisioned Cluster) | Sets `.status.id`; does **not** push spec changes or delete Console on CR removal | - -Most writable GitOps resources (`ServiceDeployment`, `GlobalService`, `InfrastructureStack`) use -**creation mode** when defined only in git. `Cluster` is special: it always adopts by handle and -runs in read-only mode after linking. - -See [management-controllers-reconciliation-logic](https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic). - -### Deployment operator (Console → cluster) - -Runs on **each registered cluster** (agent pod): - -- **ServiceDeployment** (and GlobalService children) → clone git / pull helm, render, apply - manifests on **that** cluster. -- **InfrastructureStack** → stack runs execute as **Batch Jobs** on the cluster referenced by - `InfrastructureStack.spec.clusterRef` (often mgmt), not as ongoing service sync. See - `infrastructure-stack.md`. - -The management controller does not talk to workload clusters directly for CD — Console is the -hub between git CRDs and per-cluster agents. - ---- - -## Common patterns - -### Referencing another CR - -```yaml -clusterRef: - name: prod-eu-1 - namespace: infra -repositoryRef: - name: my-app-repo - namespace: infra -``` - -Shorthand: `spec.cluster: prod-eu-1` instead of `clusterRef` — follow the repo's convention. - -### Liquid templating - -Plural uses [Liquid](https://shopify.github.io/liquid/) (plus a Sprig subset) at **apply time** -on the deployment-operator agent — not inside GitOps CR YAML. - -Templatable files ([official docs](https://docs.plural.sh/plural-features/continuous-deployment/service-templating)): -- Raw services: files whose path ends in **`.liquid`** (e.g. `deployment.yaml.liquid`) -- Helm: values files ending in **`.liquid`** - -Plain `.yaml` / `.yml` files are **not** Liquid-processed. `spec.templated: true` (default) -enables Liquid only for `.liquid` files; when `false`, even `.liquid` files are skipped. - -Available Liquid roots (from `pkg/manifests/template/`): - -| Variable | Contents | -|----------|----------| -| `cluster` | `handle`, `name`, `distro`, `tags`, `metadata`, `kasUrl`, … | -| `configuration` | `spec.configuration` key-value pairs on the service | -| `contexts` | Named `ServiceContext` configurations | -| `imports` | Stack outputs: `imports..` | -| `service` | Service name, namespace, helm block | - -Use `cluster.metadata.` for per-cluster IRSA ARNs, domains, etc. - -Helm also supports **Lua** scripts for dynamic values — see `official-cd-extensions.md`. - ---- - -## Official docs - -- [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference) — all `deployments.plural.sh/v1alpha1` CRD types and fields -- [Continuous deployment overview](https://docs.plural.sh/plural-features/continuous-deployment) -- [Git-sourced services / app-of-apps](https://docs.plural.sh/plural-features/continuous-deployment/git-service) -- [Deployment operator](https://docs.plural.sh/plural-features/continuous-deployment/deployment-operator) -- [Controller reconciliation modes](https://docs.plural.sh/plural-features/continuous-deployment/management-controllers-reconciliation-logic) - -**Rule of thumb**: if Terraform or another tool owns a resource, do not duplicate writes from -GitOps YAML — observe via read-only CRs or consume stack outputs via `spec.imports`. diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl index d9c9e7abd3..614d2e027d 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl @@ -38,8 +38,8 @@ If the repository you are analysing is a **Plural-managed GitOps repo** (i.e. it `ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` CRs), read the skill files below **before** starting the environment scan: -- `/plural/skills/repo-structure.md` — standard directory layout, naming conventions, and - how the Plural operator reconciles changes. +- `/plural/skills/git-repository.md` — `GitRepository` CRs, source vs GitOps repos, and + `repositoryRef` / `spec.git` wiring. - `/plural/skills/infrastructure-stack.md` — field-by-field mapping between `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, variable injection, and lifecycle. diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl index eb045e4ed7..d8d98d4836 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl @@ -54,8 +54,8 @@ If the repository you are working on is a **Plural-managed GitOps repo** (i.e. i `ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` CRs), read the skill files below **before** analysing or editing any manifests: -- `/plural/skills/repo-structure.md` — standard directory layout, naming conventions, and - how the Plural operator reconciles changes. +- `/plural/skills/git-repository.md` — `GitRepository` CRs, source vs GitOps repos, and + `repositoryRef` / `spec.git` wiring. - `/plural/skills/infrastructure-stack.md` — field-by-field mapping between `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, variable injection, and lifecycle. diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl index 306dc97259..94591bdabe 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl @@ -138,8 +138,8 @@ If the repository you are working on is a **Plural-managed GitOps repo** (i.e. i `ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` CRs), read the skill files below **before** analysing or editing any manifests: -- `/plural/skills/repo-structure.md` — standard directory layout, naming conventions, and - how the Plural operator reconciles changes. +- `/plural/skills/git-repository.md` — `GitRepository` CRs, source vs GitOps repos, and + `repositoryRef` / `spec.git` wiring. - `/plural/skills/infrastructure-stack.md` — field-by-field mapping between `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, variable injection, and lifecycle. From 42ed629246041b9b1ca980bdec3b07fb1a77e0c9 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 09:04:54 +0200 Subject: [PATCH 05/18] Wire Plural GitOps skills for all agent runtimes using standard SKILL.md format. Restructure bundled skills into discoverable directories and symlink them into each provider's skills path at harness startup so Claude, Codex, Gemini, and OpenCode can auto-load GitOps domain knowledge. Co-authored-by: Cursor --- .../agent-harness/skills/README.md | 49 ++++---- .../SKILL.md} | 4 + .../SKILL.md} | 4 + .../SKILL.md} | 8 +- .../{services.md => plural-services/SKILL.md} | 4 + .../agent-harness/system/analyze.md.tmpl | 26 ++-- .../agent-harness/system/babysit.md.tmpl | 26 ++-- .../agent-harness/system/write.md.tmpl | 27 ++--- .../pkg/agentrun-harness/tool/codex/codex.go | 1 + .../tool/codex/codex_types.go | 5 + .../pkg/agentrun-harness/tool/v1/skills.go | 112 ++++++++++++++++++ .../agentrun-harness/tool/v1/skills_test.go | 74 ++++++++++++ .../pkg/agentrun-harness/tool/v1/tool.go | 8 ++ 13 files changed, 275 insertions(+), 73 deletions(-) rename go/deployment-operator/dockerfiles/agent-harness/skills/{official-cd-extensions.md => plural-cd-extensions/SKILL.md} (95%) rename go/deployment-operator/dockerfiles/agent-harness/skills/{git-repository.md => plural-git-repository/SKILL.md} (92%) rename go/deployment-operator/dockerfiles/agent-harness/skills/{infrastructure-stack.md => plural-infrastructure-stack/SKILL.md} (96%) rename go/deployment-operator/dockerfiles/agent-harness/skills/{services.md => plural-services/SKILL.md} (97%) create mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go create mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md index 98a7973e91..c32113e23e 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md @@ -1,7 +1,9 @@ # Plural GitOps Skills -Domain-knowledge reference files for AI coding agents working on Plural-managed GitOps -repositories. Read the relevant files **before** making changes. +Agent skills for Plural-managed GitOps repositories. Each skill is a directory with a +`SKILL.md` file (YAML frontmatter + markdown body) following the +[Claude Code skills format](https://code.claude.com/docs/en/skills). The agent harness +symlinks them into each runtime's skills discovery path at pod startup. ## Architecture (read this once) @@ -13,37 +15,44 @@ Plural CD uses **two cooperating layers**: 2. **Deployment operator agent** (runs on each target cluster) — pulls service definitions from Console, renders Helm/Kustomize/raw YAML, and applies manifests locally. - **CRD field reference**: for authoritative `spec` / `status` definitions of every -`deployments.plural.sh/v1alpha1` kind (`Cluster`, `ServiceDeployment`, `GlobalService`, -`InfrastructureStack`, `Pipeline`, `Observer`, …), use the +`deployments.plural.sh/v1alpha1` kind, use the [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference). Agent-side CRDs are documented separately in the [Agent API Reference](https://docs.plural.sh/api-reference/kubernetes/agent-api-reference). -## Files +## Skills -| File | When to read | +| Skill | When to use | |---|---| -| [`git-repository.md`](git-repository.md) | Always — start here. How GitOps repos relate to source repos, `GitRepository` CRs, and `repositoryRef` / `spec.git` wiring. | -| [`infrastructure-stack.md`](infrastructure-stack.md) | When creating or modifying `InfrastructureStack` CRs, or any Terraform / Terragrunt / Ansible IaC runs. | -| [`services.md`](services.md) | When creating or modifying `ServiceDeployment` or `GlobalService` CRs, or deciding which one to use. | -| [`official-cd-extensions.md`](official-cd-extensions.md) | When you need advanced patterns from official docs: service-of-services, multi-source services, Lua Helm generation, sync controls, observers, and pipelines. | +| `plural-git-repository` | GitRepository CRs, source vs GitOps repos, `repositoryRef` / `spec.git` | +| `plural-infrastructure-stack` | InfrastructureStack CRs, Terraform / Terragrunt / Ansible runs | +| `plural-services` | ServiceDeployment vs GlobalService | +| `plural-cd-extensions` | Advanced CD patterns from official docs | + +## Runtime discovery paths + +The harness links bundled skills from `/plural/skills//` into: + +| Runtime | Work dir | Cloned repo (when separate) | +|---|---|---| +| Claude | `.claude/skills/` | `.claude/skills/` | +| Codex | `.codex/skills/` | `.agents/skills/` | +| Gemini | `.gemini/skills/` | `.agents/skills/` | +| OpenCode | `.opencode/skills/` | `.claude/skills/`, `.agents/skills/` | + +Codex requires `[features] skills = true` in its config (set by the harness). ## Quick-start checklist Before editing any YAML in a Plural GitOps repo: -1. **Read `git-repository.md`** to understand how this repo relates to GitOps CRs and source repos. -2. **Identify the CR kind** you are modifying (`ServiceDeployment`, `GlobalService`, - `InfrastructureStack`, etc.). For field-level specs, check the +1. **Load `plural-git-repository`** to understand how this repo relates to GitOps CRs and source repos. +2. **Identify the CR kind** you are modifying. For field-level specs, check the [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference). -3. **Read the matching skill file** for that kind. -4. **If using advanced CD patterns**, read `official-cd-extensions.md` and follow the - linked official docs for the specific feature. -5. **Check the existing CRs** in the repo for the namespace and naming convention already - in use — follow the same pattern. +3. **Load the matching skill** for that kind. +4. **If using advanced CD patterns**, load `plural-cd-extensions` and follow the linked official docs. +5. **Check existing CRs** in `manifests/` for namespace and naming conventions — follow the same pattern. 6. **Check read-only resources** — if `.status.readonly: true` on a Cluster (or similar), the CR is observed from Console/Terraform and **must not** be edited as the write path; update the upstream source instead. - diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/official-cd-extensions.md b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-cd-extensions/SKILL.md similarity index 95% rename from go/deployment-operator/dockerfiles/agent-harness/skills/official-cd-extensions.md rename to go/deployment-operator/dockerfiles/agent-harness/skills/plural-cd-extensions/SKILL.md index c15d69f91e..6e192fbf02 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/official-cd-extensions.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-cd-extensions/SKILL.md @@ -1,3 +1,7 @@ +--- +name: plural-cd-extensions +description: Advanced Plural CD patterns including service-of-services, multi-source services, Liquid/Lua templating, observers, and pipelines. Use when implementing advanced continuous deployment features in Plural GitOps. +--- # Official Continuous Deployment Extensions This file maps common "advanced" Plural CD workflows to official docs so agents can quickly diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/git-repository.md b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-git-repository/SKILL.md similarity index 92% rename from go/deployment-operator/dockerfiles/agent-harness/skills/git-repository.md rename to go/deployment-operator/dockerfiles/agent-harness/skills/plural-git-repository/SKILL.md index 71787c5c87..b20f8b66ef 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/git-repository.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-git-repository/SKILL.md @@ -1,3 +1,7 @@ +--- +name: plural-git-repository +description: Explains GitRepository CRs and how GitOps repos relate to source repos. Use when editing Plural GitOps repos, ServiceDeployment repositoryRef, spec.git.folder, or deployments.plural.sh GitRepository CRs. +--- # GitRepository Plural CD separates **where CRs are defined** from **where application or IaC code lives**: diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-infrastructure-stack/SKILL.md similarity index 96% rename from go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md rename to go/deployment-operator/dockerfiles/agent-harness/skills/plural-infrastructure-stack/SKILL.md index fded2947ec..e1c32397f8 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/infrastructure-stack.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-infrastructure-stack/SKILL.md @@ -1,3 +1,7 @@ +--- +name: plural-infrastructure-stack +description: Maps InfrastructureStack CRs to Terraform, Terragrunt, and Ansible stack runs. Use when creating or modifying InfrastructureStack CRs, stack imports/outputs, or IaC in Plural GitOps. +--- # InfrastructureStack — Terraform / IaC Mapping `InfrastructureStack` is the Plural GitOps CRD for IaC. Each CR maps to **stack runs** in @@ -214,8 +218,8 @@ spec: namespace: infra ``` -In Liquid templates, outputs appear under `imports..` (see -`services.md`). +In Liquid templates, outputs appear under `imports..` (see the +`plural-services` skill). --- diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/services.md b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-services/SKILL.md similarity index 97% rename from go/deployment-operator/dockerfiles/agent-harness/skills/services.md rename to go/deployment-operator/dockerfiles/agent-harness/skills/plural-services/SKILL.md index 46305568e6..ec0097f2bb 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/services.md +++ b/go/deployment-operator/dockerfiles/agent-harness/skills/plural-services/SKILL.md @@ -1,3 +1,7 @@ +--- +name: plural-services +description: Compares ServiceDeployment vs GlobalService and fleet deployment patterns. Use when creating or modifying ServiceDeployment, GlobalService, or child service CRs in Plural GitOps repos. +--- # GlobalService vs ServiceDeployment Plural provides two CRDs for deploying workloads to Kubernetes clusters. Choosing the right diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl index 614d2e027d..b80b9fcc0d 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl @@ -34,23 +34,15 @@ When analysis is finished, you **must** persist the report by calling the Plural ## Plural GitOps skills -If the repository you are analysing is a **Plural-managed GitOps repo** (i.e. it contains -`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` -CRs), read the skill files below **before** starting the environment scan: - -- `/plural/skills/git-repository.md` — `GitRepository` CRs, source vs GitOps repos, and - `repositoryRef` / `spec.git` wiring. -- `/plural/skills/infrastructure-stack.md` — field-by-field mapping between - `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, - variable injection, and lifecycle. -- `/plural/skills/services.md` — when to use `ServiceDeployment` vs `GlobalService`, how - child ServiceDeployments are created by a GlobalService, and common mistakes. -- `/plural/skills/official-cd-extensions.md` — official docs-backed advanced patterns - (service-of-services, multi-source services, Lua Helm config, sync controls, observers, - pipelines). - -Read the `/plural/skills/README.md` index first for architecture context and a quick-start -checklist. +If this repository contains `deployments.plural.sh` CRs, load the bundled Plural GitOps agent +skills before starting the environment scan. They are auto-discovered from `/plural/skills/`: + +- `plural-git-repository` — GitRepository CRs and source vs GitOps repo wiring +- `plural-infrastructure-stack` — InfrastructureStack → Terraform / Terragrunt / Ansible +- `plural-services` — ServiceDeployment vs GlobalService +- `plural-cd-extensions` — advanced CD patterns + +See `/plural/skills/README.md` for architecture context and a quick-start checklist. --- diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl index d8d98d4836..3a525087d7 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl @@ -50,23 +50,15 @@ Docker-in-Docker is enabled. You can use `docker` and `docker-compose` for testi ## Plural GitOps skills -If the repository you are working on is a **Plural-managed GitOps repo** (i.e. it contains -`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` -CRs), read the skill files below **before** analysing or editing any manifests: - -- `/plural/skills/git-repository.md` — `GitRepository` CRs, source vs GitOps repos, and - `repositoryRef` / `spec.git` wiring. -- `/plural/skills/infrastructure-stack.md` — field-by-field mapping between - `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, - variable injection, and lifecycle. -- `/plural/skills/services.md` — when to use `ServiceDeployment` vs `GlobalService`, how - child ServiceDeployments are created by a GlobalService, and common mistakes. -- `/plural/skills/official-cd-extensions.md` — official docs-backed advanced patterns - (service-of-services, multi-source services, Lua Helm config, sync controls, observers, - pipelines). - -Read the `/plural/skills/README.md` index first for architecture context and a quick-start -checklist. +If this repository contains `deployments.plural.sh` CRs, load the bundled Plural GitOps agent +skills before analysing or editing manifests. They are auto-discovered from `/plural/skills/`: + +- `plural-git-repository` — GitRepository CRs and source vs GitOps repo wiring +- `plural-infrastructure-stack` — InfrastructureStack → Terraform / Terragrunt / Ansible +- `plural-services` — ServiceDeployment vs GlobalService +- `plural-cd-extensions` — advanced CD patterns + +See `/plural/skills/README.md` for architecture context and a quick-start checklist. --- diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl index 94591bdabe..c873da93e7 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl @@ -134,23 +134,16 @@ Each failure → **one** cycle updating only the relevant todo’s `description` ## Plural GitOps skills -If the repository you are working on is a **Plural-managed GitOps repo** (i.e. it contains -`ServiceDeployment`, `GlobalService`, `InfrastructureStack`, or other `deployments.plural.sh` -CRs), read the skill files below **before** analysing or editing any manifests: - -- `/plural/skills/git-repository.md` — `GitRepository` CRs, source vs GitOps repos, and - `repositoryRef` / `spec.git` wiring. -- `/plural/skills/infrastructure-stack.md` — field-by-field mapping between - `InfrastructureStack` CRs and Terraform / Terragrunt / Ansible runs, state management, - variable injection, and lifecycle. -- `/plural/skills/services.md` — when to use `ServiceDeployment` vs `GlobalService`, how - child ServiceDeployments are created by a GlobalService, and common mistakes. -- `/plural/skills/official-cd-extensions.md` — official docs-backed advanced patterns - (service-of-services, multi-source services, Lua Helm config, sync controls, observers, - pipelines). - -Read the `/plural/skills/README.md` index first for architecture context and a quick-start -checklist. +If this repository contains `deployments.plural.sh` CRs (`ServiceDeployment`, `GlobalService`, +`InfrastructureStack`, …), load the bundled Plural GitOps agent skills before editing manifests. +They are auto-discovered from `/plural/skills/`: + +- `plural-git-repository` — GitRepository CRs and source vs GitOps repo wiring +- `plural-infrastructure-stack` — InfrastructureStack → Terraform / Terragrunt / Ansible +- `plural-services` — ServiceDeployment vs GlobalService +- `plural-cd-extensions` — advanced CD patterns (multi-source, Lua, observers, pipelines) + +See `/plural/skills/README.md` for architecture context and a quick-start checklist. --- diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go index f2bcefec11..d43e1f38b4 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go @@ -186,6 +186,7 @@ func (in *Codex) writeCodexConfig() error { if err != nil { return err } + cfg.Features = &CodexGlobalFeatures{Skills: true} config, err := WriteCodexConfig(path.Join(in.Config.WorkDir, ".codex"), cfg) if err != nil { diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go index 1b4de18a0b..a88e4c0d77 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go @@ -262,4 +262,9 @@ type CodexConfig struct { ModelProviders map[string]*ModelProviderConfig `toml:"model_providers,omitempty"` Profiles map[string]*Profile `toml:"profiles"` MCPServers map[string]*MCPServer `toml:"mcp_servers"` + Features *CodexGlobalFeatures `toml:"features,omitempty"` +} + +type CodexGlobalFeatures struct { + Skills bool `toml:"skills,omitempty"` } diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go new file mode 100644 index 0000000000..e0ea247890 --- /dev/null +++ b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go @@ -0,0 +1,112 @@ +package v1 + +import ( + "fmt" + "os" + "path" + "path/filepath" + + console "github.com/pluralsh/console/go/client" + "k8s.io/klog/v2" + + "github.com/pluralsh/console/go/deployment-operator/pkg/log" +) + +const ( + bundledSkillsDir = "skills" + skillFileName = "SKILL.md" +) + +var providerSkillsDirs = map[console.AgentRuntimeType][]string{ + console.AgentRuntimeTypeClaude: {".claude/skills"}, + console.AgentRuntimeTypeCodex: {".codex/skills"}, + console.AgentRuntimeTypeGemini: {".gemini/skills"}, + console.AgentRuntimeTypeOpencode: {".opencode/skills"}, +} + +// repositorySkillsDirs are paths scanned from the cloned repository working directory. +var repositorySkillsDirs = []string{ + ".claude/skills", + ".agents/skills", + ".opencode/skills", +} + +// ConfigureSkills symlinks bundled skill directories into each provider's skills discovery path. +func (in DefaultTool) ConfigureSkills(runtime console.AgentRuntimeType) error { + dirs, ok := providerSkillsDirs[runtime] + if !ok { + return fmt.Errorf("unsupported agent runtime %q for skills configuration", runtime) + } + + skillsRoot := path.Join(in.Config.WorkDir, bundledSkillsDir) + entries, err := os.ReadDir(skillsRoot) + if err != nil { + return fmt.Errorf("read bundled skills dir %q: %w", skillsRoot, err) + } + + linked := 0 + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + src := path.Join(skillsRoot, entry.Name()) + if _, err := os.Stat(path.Join(src, skillFileName)); err != nil { + continue + } + + for _, rel := range dirs { + dst := path.Join(in.Config.WorkDir, rel, entry.Name()) + if err := linkSkillDir(dst, src); err != nil { + return fmt.Errorf("link skill %q for %s: %w", entry.Name(), runtime, err) + } + } + + if err := in.linkRepositorySkills(entry.Name(), src); err != nil { + return err + } + + linked++ + } + + klog.V(log.LogLevelExtended).InfoS( + "skills configured", + "runtime", runtime, + "count", linked, + "workDir", in.Config.WorkDir, + "repositoryDir", in.Config.RepositoryDir, + ) + return nil +} + +func (in DefaultTool) linkRepositorySkills(name, src string) error { + if in.Config.RepositoryDir == "" || in.Config.RepositoryDir == in.Config.WorkDir { + return nil + } + + for _, rel := range repositorySkillsDirs { + dst := path.Join(in.Config.RepositoryDir, rel, name) + if err := linkSkillDir(dst, src); err != nil { + return fmt.Errorf("link repository skill %q at %q: %w", name, dst, err) + } + } + return nil +} + +func linkSkillDir(dst, src string) error { + if err := os.MkdirAll(path.Dir(dst), 0755); err != nil { + return fmt.Errorf("create skills parent dir: %w", err) + } + + _ = os.Remove(dst) + + srcAbs, err := filepath.Abs(src) + if err != nil { + return fmt.Errorf("resolve skill source %q: %w", src, err) + } + + if err := os.Symlink(srcAbs, dst); err != nil { + return fmt.Errorf("symlink %q -> %q: %w", dst, srcAbs, err) + } + return nil +} diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go new file mode 100644 index 0000000000..c5d7c0933a --- /dev/null +++ b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go @@ -0,0 +1,74 @@ +package v1 + +import ( + "os" + "path/filepath" + "testing" + + console "github.com/pluralsh/console/go/client" +) + +func TestConfigureSkills_LinksBundledSkills(t *testing.T) { + workDir := t.TempDir() + repoDir := filepath.Join(workDir, "repo") + if err := os.MkdirAll(repoDir, 0755); err != nil { + t.Fatalf("mkdir repo: %v", err) + } + + skillsSrc := filepath.Join(workDir, bundledSkillsDir, "plural-git-repository") + if err := os.MkdirAll(skillsSrc, 0755); err != nil { + t.Fatalf("mkdir skills source: %v", err) + } + if err := os.WriteFile(filepath.Join(skillsSrc, skillFileName), []byte("---\nname: plural-git-repository\ndescription: test\n---\n# test\n"), 0644); err != nil { + t.Fatalf("write SKILL.md: %v", err) + } + + tool := DefaultTool{Config: Config{WorkDir: workDir, RepositoryDir: repoDir}} + for _, runtime := range []console.AgentRuntimeType{ + console.AgentRuntimeTypeClaude, + console.AgentRuntimeTypeCodex, + console.AgentRuntimeTypeGemini, + console.AgentRuntimeTypeOpencode, + } { + t.Run(string(runtime), func(t *testing.T) { + if err := tool.ConfigureSkills(runtime); err != nil { + t.Fatalf("ConfigureSkills() failed: %v", err) + } + + for _, rel := range providerSkillsDirs[runtime] { + link := filepath.Join(workDir, rel, "plural-git-repository", skillFileName) + if _, err := os.Stat(link); err != nil { + t.Fatalf("expected skill link at %q: %v", link, err) + } + } + + for _, rel := range repositorySkillsDirs { + link := filepath.Join(repoDir, rel, "plural-git-repository", skillFileName) + if _, err := os.Stat(link); err != nil { + t.Fatalf("expected repository skill link at %q: %v", link, err) + } + } + }) + } +} + +func TestConfigureSkills_IgnoresNonSkillEntries(t *testing.T) { + workDir := t.TempDir() + skillsRoot := filepath.Join(workDir, bundledSkillsDir) + if err := os.MkdirAll(skillsRoot, 0755); err != nil { + t.Fatalf("mkdir skills root: %v", err) + } + if err := os.WriteFile(filepath.Join(skillsRoot, "README.md"), []byte("# index"), 0644); err != nil { + t.Fatalf("write README: %v", err) + } + + tool := DefaultTool{Config: Config{WorkDir: workDir, RepositoryDir: workDir}} + if err := tool.ConfigureSkills(console.AgentRuntimeTypeClaude); err != nil { + t.Fatalf("ConfigureSkills() failed: %v", err) + } + + claudeSkills := filepath.Join(workDir, ".claude", "skills") + if entries, err := os.ReadDir(claudeSkills); err == nil && len(entries) > 0 { + t.Fatalf("expected no skill links, found %d", len(entries)) + } +} diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go index 7bacd3493c..1a95ae593e 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go @@ -64,6 +64,10 @@ func (in DefaultTool) ConfigureSystemPrompt(runtime console.AgentRuntimeType) er return fmt.Errorf("failed configuring %s system prompt/context file %q: %w", runtime, outputFile, err) } + if err := in.ConfigureSkills(runtime); err != nil { + return fmt.Errorf("failed configuring %s skills: %w", runtime, err) + } + klog.V(log.LogLevelExtended).InfoS("system prompt/context file configured", "output", outputFile) return nil } @@ -93,6 +97,10 @@ func (in DefaultTool) ConfigureSystemPromptForBabysitRun(runtime console.AgentRu return fmt.Errorf("failed to write babysit system prompt %q: %w", outputFile, err) } + if err := in.ConfigureSkills(runtime); err != nil { + return fmt.Errorf("failed configuring %s skills: %w", runtime, err) + } + return nil } From 8cdca5499e2efc441dc7746efd8d2ff332450927 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 09:23:37 +0200 Subject: [PATCH 06/18] update logs --- .../pkg/agentrun-harness/tool/v1/skills.go | 37 ++++++++++++++----- .../pkg/agentrun-harness/tool/v1/tool.go | 4 -- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go index e0ea247890..fbfe0497e7 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go @@ -39,6 +39,14 @@ func (in DefaultTool) ConfigureSkills(runtime console.AgentRuntimeType) error { } skillsRoot := path.Join(in.Config.WorkDir, bundledSkillsDir) + klog.V(log.LogLevelInfo).InfoS( + "configuring agent skills", + "runtime", runtime, + "skillsRoot", skillsRoot, + "workDir", in.Config.WorkDir, + "repositoryDir", in.Config.RepositoryDir, + ) + entries, err := os.ReadDir(skillsRoot) if err != nil { return fmt.Errorf("read bundled skills dir %q: %w", skillsRoot, err) @@ -57,43 +65,43 @@ func (in DefaultTool) ConfigureSkills(runtime console.AgentRuntimeType) error { for _, rel := range dirs { dst := path.Join(in.Config.WorkDir, rel, entry.Name()) - if err := linkSkillDir(dst, src); err != nil { + if err := linkSkillDir(dst, src, "workDir", runtime, entry.Name()); err != nil { return fmt.Errorf("link skill %q for %s: %w", entry.Name(), runtime, err) } } - if err := in.linkRepositorySkills(entry.Name(), src); err != nil { + if err := in.linkRepositorySkills(runtime, entry.Name(), src); err != nil { return err } linked++ } - klog.V(log.LogLevelExtended).InfoS( - "skills configured", + klog.V(log.LogLevelInfo).InfoS( + "agent skills configured", "runtime", runtime, - "count", linked, - "workDir", in.Config.WorkDir, - "repositoryDir", in.Config.RepositoryDir, + "skillCount", linked, + "workDirTargets", dirs, + "repositoryTargets", repositorySkillsDirs, ) return nil } -func (in DefaultTool) linkRepositorySkills(name, src string) error { +func (in DefaultTool) linkRepositorySkills(runtime console.AgentRuntimeType, name, src string) error { if in.Config.RepositoryDir == "" || in.Config.RepositoryDir == in.Config.WorkDir { return nil } for _, rel := range repositorySkillsDirs { dst := path.Join(in.Config.RepositoryDir, rel, name) - if err := linkSkillDir(dst, src); err != nil { + if err := linkSkillDir(dst, src, "repositoryDir", runtime, name); err != nil { return fmt.Errorf("link repository skill %q at %q: %w", name, dst, err) } } return nil } -func linkSkillDir(dst, src string) error { +func linkSkillDir(dst, src, scope string, runtime console.AgentRuntimeType, skill string) error { if err := os.MkdirAll(path.Dir(dst), 0755); err != nil { return fmt.Errorf("create skills parent dir: %w", err) } @@ -108,5 +116,14 @@ func linkSkillDir(dst, src string) error { if err := os.Symlink(srcAbs, dst); err != nil { return fmt.Errorf("symlink %q -> %q: %w", dst, srcAbs, err) } + + klog.V(log.LogLevelInfo).InfoS( + "skill symlink created", + "runtime", runtime, + "skill", skill, + "scope", scope, + "dst", dst, + "src", srcAbs, + ) return nil } diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go index 1a95ae593e..7807676ae4 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go @@ -97,10 +97,6 @@ func (in DefaultTool) ConfigureSystemPromptForBabysitRun(runtime console.AgentRu return fmt.Errorf("failed to write babysit system prompt %q: %w", outputFile, err) } - if err := in.ConfigureSkills(runtime); err != nil { - return fmt.Errorf("failed configuring %s skills: %w", runtime, err) - } - return nil } From 43d2b4c25210a6bca8a63e02157a9f8b74f5f4c3 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 09:56:09 +0200 Subject: [PATCH 07/18] update logs --- .../dockerfiles/agent-harness/system/analyze.md.tmpl | 3 +++ .../dockerfiles/agent-harness/system/babysit.md.tmpl | 3 +++ .../dockerfiles/agent-harness/system/write.md.tmpl | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl index b80b9fcc0d..0d72d5def6 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl @@ -44,6 +44,9 @@ skills before starting the environment scan. They are auto-discovered from `/plu See `/plural/skills/README.md` for architecture context and a quick-start checklist. +After loading a skill, apply it internally. Do **not** paste or summarize skill text in your +report — read `references/guide.md` inside a skill only when you need examples or field detail. + --- ## 1. Hard rules diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl index 3a525087d7..7599f95a13 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl @@ -60,6 +60,9 @@ skills before analysing or editing manifests. They are auto-discovered from `/pl See `/plural/skills/README.md` for architecture context and a quick-start checklist. +After loading a skill, apply it internally. Do **not** paste or summarize skill text in your +replies — read `references/guide.md` inside a skill only when you need examples or field detail. + --- ## Plural MCP tool hints diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl index c873da93e7..6bdec0ff8b 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl @@ -145,6 +145,10 @@ They are auto-discovered from `/plural/skills/`: See `/plural/skills/README.md` for architecture context and a quick-start checklist. +After loading a skill, apply it internally. Do **not** paste or summarize skill text in your +replies — the Skill tool output is for you, not the user. Read `references/guide.md` inside a +skill only when you need examples or field-level detail. + --- ## Plural MCP tool hints From e9dad6333161b4fe4879171853828069c54a8e7d Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 10:17:22 +0200 Subject: [PATCH 08/18] update skills --- .../dockerfiles/agent-harness/base.Dockerfile | 14 +- .../agent-harness/skills/README.md | 58 -------- .../agent-harness/system/analyze.md.tmpl | 2 - .../agent-harness/system/babysit.md.tmpl | 2 - .../agent-harness/system/write.md.tmpl | 2 - .../opencode/templates/opencode.json.gotmpl | 5 + .../pkg/agentrun-harness/tool/v1/skills.go | 129 ------------------ .../agentrun-harness/tool/v1/skills_test.go | 74 ---------- .../pkg/agentrun-harness/tool/v1/tool.go | 4 - 9 files changed, 15 insertions(+), 275 deletions(-) delete mode 100644 go/deployment-operator/dockerfiles/agent-harness/skills/README.md delete mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go delete mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go diff --git a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile index 558752ba5e..921705aff0 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile +++ b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile @@ -107,10 +107,16 @@ WORKDIR /plural COPY deployment-operator/dockerfiles/agent-harness/system /plural/system COPY deployment-operator/dockerfiles/agent-harness/skills /plural/skills -RUN mkdir -p /plural/.opencode && \ - mkdir -p /plural/.claude && \ - mkdir -p /plural/.gemini && \ - mkdir -p /plural/.codex +RUN for provider in .claude .codex .gemini .opencode; do \ + mkdir -p "/plural/$${provider}/skills"; \ + done && \ + for skill in /plural/skills/*/; do \ + name="$$(basename "$$skill")"; \ + [ -f "$$skill/SKILL.md" ] || continue; \ + for provider in .claude .codex .gemini .opencode; do \ + ln -sfn "/plural/skills/$${name}" "/plural/$${provider}/skills/$${name}"; \ + done; \ + done RUN chown -R 65532:65532 /plural && \ mkdir -p /run/user/65532 && \ diff --git a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md b/go/deployment-operator/dockerfiles/agent-harness/skills/README.md deleted file mode 100644 index c32113e23e..0000000000 --- a/go/deployment-operator/dockerfiles/agent-harness/skills/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Plural GitOps Skills - -Agent skills for Plural-managed GitOps repositories. Each skill is a directory with a -`SKILL.md` file (YAML frontmatter + markdown body) following the -[Claude Code skills format](https://code.claude.com/docs/en/skills). The agent harness -symlinks them into each runtime's skills discovery path at pod startup. - -## Architecture (read this once) - -Plural CD uses **two cooperating layers**: - -1. **Management controller** (`go/controller`, runs on the mgmt cluster) — watches GitOps CRDs, - syncs them to the Plural Console API. It is a **frontend**: it does not apply workloads to - fleet clusters directly. -2. **Deployment operator agent** (runs on each target cluster) — pulls service definitions from - Console, renders Helm/Kustomize/raw YAML, and applies manifests locally. - -**CRD field reference**: for authoritative `spec` / `status` definitions of every -`deployments.plural.sh/v1alpha1` kind, use the -[Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference). -Agent-side CRDs are documented separately in the -[Agent API Reference](https://docs.plural.sh/api-reference/kubernetes/agent-api-reference). - -## Skills - -| Skill | When to use | -|---|---| -| `plural-git-repository` | GitRepository CRs, source vs GitOps repos, `repositoryRef` / `spec.git` | -| `plural-infrastructure-stack` | InfrastructureStack CRs, Terraform / Terragrunt / Ansible runs | -| `plural-services` | ServiceDeployment vs GlobalService | -| `plural-cd-extensions` | Advanced CD patterns from official docs | - -## Runtime discovery paths - -The harness links bundled skills from `/plural/skills//` into: - -| Runtime | Work dir | Cloned repo (when separate) | -|---|---|---| -| Claude | `.claude/skills/` | `.claude/skills/` | -| Codex | `.codex/skills/` | `.agents/skills/` | -| Gemini | `.gemini/skills/` | `.agents/skills/` | -| OpenCode | `.opencode/skills/` | `.claude/skills/`, `.agents/skills/` | - -Codex requires `[features] skills = true` in its config (set by the harness). - -## Quick-start checklist - -Before editing any YAML in a Plural GitOps repo: - -1. **Load `plural-git-repository`** to understand how this repo relates to GitOps CRs and source repos. -2. **Identify the CR kind** you are modifying. For field-level specs, check the - [Management API Reference](https://docs.plural.sh/api-reference/kubernetes/management-api-reference). -3. **Load the matching skill** for that kind. -4. **If using advanced CD patterns**, load `plural-cd-extensions` and follow the linked official docs. -5. **Check existing CRs** in `manifests/` for namespace and naming conventions — follow the same pattern. -6. **Check read-only resources** — if `.status.readonly: true` on a Cluster (or similar), the - CR is observed from Console/Terraform and **must not** be edited as the write path; update - the upstream source instead. diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl index 0d72d5def6..c3d962447a 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/analyze.md.tmpl @@ -42,8 +42,6 @@ skills before starting the environment scan. They are auto-discovered from `/plu - `plural-services` — ServiceDeployment vs GlobalService - `plural-cd-extensions` — advanced CD patterns -See `/plural/skills/README.md` for architecture context and a quick-start checklist. - After loading a skill, apply it internally. Do **not** paste or summarize skill text in your report — read `references/guide.md` inside a skill only when you need examples or field detail. diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl index 7599f95a13..ac26f3d2d4 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/babysit.md.tmpl @@ -58,8 +58,6 @@ skills before analysing or editing manifests. They are auto-discovered from `/pl - `plural-services` — ServiceDeployment vs GlobalService - `plural-cd-extensions` — advanced CD patterns -See `/plural/skills/README.md` for architecture context and a quick-start checklist. - After loading a skill, apply it internally. Do **not** paste or summarize skill text in your replies — read `references/guide.md` inside a skill only when you need examples or field detail. diff --git a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl index 6bdec0ff8b..d0405648cd 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl +++ b/go/deployment-operator/dockerfiles/agent-harness/system/write.md.tmpl @@ -143,8 +143,6 @@ They are auto-discovered from `/plural/skills/`: - `plural-services` — ServiceDeployment vs GlobalService - `plural-cd-extensions` — advanced CD patterns (multi-source, Lua, observers, pipelines) -See `/plural/skills/README.md` for architecture context and a quick-start checklist. - After loading a skill, apply it internally. Do **not** paste or summarize skill text in your replies — the Skill tool output is for you, not the user. Read `references/guide.md` inside a skill only when you need examples or field-level detail. diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl index 95feebe493..fadc296573 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl +++ b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl @@ -61,6 +61,11 @@ } }, "username": "plural", + "skills": { + "paths": [ + "/plural/skills" + ] + }, "provider": { "{{ .Provider }}": { {{ if or (eq .Provider "plural") .OpenAICompatible }}"npm": "@ai-sdk/openai", diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go deleted file mode 100644 index fbfe0497e7..0000000000 --- a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills.go +++ /dev/null @@ -1,129 +0,0 @@ -package v1 - -import ( - "fmt" - "os" - "path" - "path/filepath" - - console "github.com/pluralsh/console/go/client" - "k8s.io/klog/v2" - - "github.com/pluralsh/console/go/deployment-operator/pkg/log" -) - -const ( - bundledSkillsDir = "skills" - skillFileName = "SKILL.md" -) - -var providerSkillsDirs = map[console.AgentRuntimeType][]string{ - console.AgentRuntimeTypeClaude: {".claude/skills"}, - console.AgentRuntimeTypeCodex: {".codex/skills"}, - console.AgentRuntimeTypeGemini: {".gemini/skills"}, - console.AgentRuntimeTypeOpencode: {".opencode/skills"}, -} - -// repositorySkillsDirs are paths scanned from the cloned repository working directory. -var repositorySkillsDirs = []string{ - ".claude/skills", - ".agents/skills", - ".opencode/skills", -} - -// ConfigureSkills symlinks bundled skill directories into each provider's skills discovery path. -func (in DefaultTool) ConfigureSkills(runtime console.AgentRuntimeType) error { - dirs, ok := providerSkillsDirs[runtime] - if !ok { - return fmt.Errorf("unsupported agent runtime %q for skills configuration", runtime) - } - - skillsRoot := path.Join(in.Config.WorkDir, bundledSkillsDir) - klog.V(log.LogLevelInfo).InfoS( - "configuring agent skills", - "runtime", runtime, - "skillsRoot", skillsRoot, - "workDir", in.Config.WorkDir, - "repositoryDir", in.Config.RepositoryDir, - ) - - entries, err := os.ReadDir(skillsRoot) - if err != nil { - return fmt.Errorf("read bundled skills dir %q: %w", skillsRoot, err) - } - - linked := 0 - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - src := path.Join(skillsRoot, entry.Name()) - if _, err := os.Stat(path.Join(src, skillFileName)); err != nil { - continue - } - - for _, rel := range dirs { - dst := path.Join(in.Config.WorkDir, rel, entry.Name()) - if err := linkSkillDir(dst, src, "workDir", runtime, entry.Name()); err != nil { - return fmt.Errorf("link skill %q for %s: %w", entry.Name(), runtime, err) - } - } - - if err := in.linkRepositorySkills(runtime, entry.Name(), src); err != nil { - return err - } - - linked++ - } - - klog.V(log.LogLevelInfo).InfoS( - "agent skills configured", - "runtime", runtime, - "skillCount", linked, - "workDirTargets", dirs, - "repositoryTargets", repositorySkillsDirs, - ) - return nil -} - -func (in DefaultTool) linkRepositorySkills(runtime console.AgentRuntimeType, name, src string) error { - if in.Config.RepositoryDir == "" || in.Config.RepositoryDir == in.Config.WorkDir { - return nil - } - - for _, rel := range repositorySkillsDirs { - dst := path.Join(in.Config.RepositoryDir, rel, name) - if err := linkSkillDir(dst, src, "repositoryDir", runtime, name); err != nil { - return fmt.Errorf("link repository skill %q at %q: %w", name, dst, err) - } - } - return nil -} - -func linkSkillDir(dst, src, scope string, runtime console.AgentRuntimeType, skill string) error { - if err := os.MkdirAll(path.Dir(dst), 0755); err != nil { - return fmt.Errorf("create skills parent dir: %w", err) - } - - _ = os.Remove(dst) - - srcAbs, err := filepath.Abs(src) - if err != nil { - return fmt.Errorf("resolve skill source %q: %w", src, err) - } - - if err := os.Symlink(srcAbs, dst); err != nil { - return fmt.Errorf("symlink %q -> %q: %w", dst, srcAbs, err) - } - - klog.V(log.LogLevelInfo).InfoS( - "skill symlink created", - "runtime", runtime, - "skill", skill, - "scope", scope, - "dst", dst, - "src", srcAbs, - ) - return nil -} diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go deleted file mode 100644 index c5d7c0933a..0000000000 --- a/go/deployment-operator/pkg/agentrun-harness/tool/v1/skills_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package v1 - -import ( - "os" - "path/filepath" - "testing" - - console "github.com/pluralsh/console/go/client" -) - -func TestConfigureSkills_LinksBundledSkills(t *testing.T) { - workDir := t.TempDir() - repoDir := filepath.Join(workDir, "repo") - if err := os.MkdirAll(repoDir, 0755); err != nil { - t.Fatalf("mkdir repo: %v", err) - } - - skillsSrc := filepath.Join(workDir, bundledSkillsDir, "plural-git-repository") - if err := os.MkdirAll(skillsSrc, 0755); err != nil { - t.Fatalf("mkdir skills source: %v", err) - } - if err := os.WriteFile(filepath.Join(skillsSrc, skillFileName), []byte("---\nname: plural-git-repository\ndescription: test\n---\n# test\n"), 0644); err != nil { - t.Fatalf("write SKILL.md: %v", err) - } - - tool := DefaultTool{Config: Config{WorkDir: workDir, RepositoryDir: repoDir}} - for _, runtime := range []console.AgentRuntimeType{ - console.AgentRuntimeTypeClaude, - console.AgentRuntimeTypeCodex, - console.AgentRuntimeTypeGemini, - console.AgentRuntimeTypeOpencode, - } { - t.Run(string(runtime), func(t *testing.T) { - if err := tool.ConfigureSkills(runtime); err != nil { - t.Fatalf("ConfigureSkills() failed: %v", err) - } - - for _, rel := range providerSkillsDirs[runtime] { - link := filepath.Join(workDir, rel, "plural-git-repository", skillFileName) - if _, err := os.Stat(link); err != nil { - t.Fatalf("expected skill link at %q: %v", link, err) - } - } - - for _, rel := range repositorySkillsDirs { - link := filepath.Join(repoDir, rel, "plural-git-repository", skillFileName) - if _, err := os.Stat(link); err != nil { - t.Fatalf("expected repository skill link at %q: %v", link, err) - } - } - }) - } -} - -func TestConfigureSkills_IgnoresNonSkillEntries(t *testing.T) { - workDir := t.TempDir() - skillsRoot := filepath.Join(workDir, bundledSkillsDir) - if err := os.MkdirAll(skillsRoot, 0755); err != nil { - t.Fatalf("mkdir skills root: %v", err) - } - if err := os.WriteFile(filepath.Join(skillsRoot, "README.md"), []byte("# index"), 0644); err != nil { - t.Fatalf("write README: %v", err) - } - - tool := DefaultTool{Config: Config{WorkDir: workDir, RepositoryDir: workDir}} - if err := tool.ConfigureSkills(console.AgentRuntimeTypeClaude); err != nil { - t.Fatalf("ConfigureSkills() failed: %v", err) - } - - claudeSkills := filepath.Join(workDir, ".claude", "skills") - if entries, err := os.ReadDir(claudeSkills); err == nil && len(entries) > 0 { - t.Fatalf("expected no skill links, found %d", len(entries)) - } -} diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go b/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go index 7807676ae4..7bacd3493c 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/v1/tool.go @@ -64,10 +64,6 @@ func (in DefaultTool) ConfigureSystemPrompt(runtime console.AgentRuntimeType) er return fmt.Errorf("failed configuring %s system prompt/context file %q: %w", runtime, outputFile, err) } - if err := in.ConfigureSkills(runtime); err != nil { - return fmt.Errorf("failed configuring %s skills: %w", runtime, err) - } - klog.V(log.LogLevelExtended).InfoS("system prompt/context file configured", "output", outputFile) return nil } From b0a4f9e53fd2201fc8cf22d835662eae81b9991b Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 11:11:39 +0200 Subject: [PATCH 09/18] update claude --- .../dockerfiles/agent-harness/base.Dockerfile | 3 +- .../agentrun-harness/tool/claude/agents.go | 6 +- .../agentrun-harness/tool/claude/claude.go | 56 ++++++++++--------- .../agentrun-harness/tool/claude/skills.go | 35 ++++++++++++ 4 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go diff --git a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile index 921705aff0..605cf6c581 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile +++ b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile @@ -114,7 +114,8 @@ RUN for provider in .claude .codex .gemini .opencode; do \ name="$$(basename "$$skill")"; \ [ -f "$$skill/SKILL.md" ] || continue; \ for provider in .claude .codex .gemini .opencode; do \ - ln -sfn "/plural/skills/$${name}" "/plural/$${provider}/skills/$${name}"; \ + rm -rf "/plural/$${provider}/skills/$${name}"; \ + cp -a "$$skill" "/plural/$${provider}/skills/$${name}"; \ done; \ done diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/claude/agents.go b/go/deployment-operator/pkg/agentrun-harness/tool/claude/agents.go index 090bc14655..ca35e1dc93 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/claude/agents.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/claude/agents.go @@ -62,13 +62,13 @@ var ( analysisAgent = agentJSON("analysis", agentDef{ Description: "Analyze code for potential issues, vulnerabilities and improvements. Use PROACTIVELY.", Prompt: "You are a read-only autonomous analysis agent.", - Tools: appendTools([]string{"Read", "Grep", "Glob", "Bash"}, analyzePluralMCPTools), + Tools: appendTools([]string{"Read", "Grep", "Glob", "Bash", "Skill"}, analyzePluralMCPTools), }) autonomousAgent = agentJSON("autonomous", agentDef{ Description: "Autonomous agent for making code changes and creating pull requests. Use PROACTIVELY.", Prompt: "You are an autonomous coding agent, highly skilled in coding and code analysis.", Tools: appendTools( - []string{"Read", "Write", "Edit", "MultiEdit", "Bash", "Grep", "Glob", "WebFetch"}, + []string{"Read", "Write", "Edit", "MultiEdit", "Bash", "Grep", "Glob", "WebFetch", "Skill"}, writePluralMCPTools, ), }) @@ -76,7 +76,7 @@ var ( Description: "Autonomous agent responding to pull request feedback. Commits to the existing PR branch. Does NOT create new PRs. Use PROACTIVELY.", Prompt: "You are an autonomous coding agent. Your pull request is already open. Address reviewer comments and fix CI failures, then commit to the existing branch.", Tools: appendTools( - []string{"Read", "Write", "Edit", "MultiEdit", "Bash", "Grep", "Glob", "WebFetch"}, + []string{"Read", "Write", "Edit", "MultiEdit", "Bash", "Grep", "Glob", "WebFetch", "Skill"}, babysitPluralMCPTools, ), }) diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go b/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go index 61e29db3b2..18e7fecfe8 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go @@ -60,15 +60,7 @@ func (in *Claude) BabysitRun(ctx context.Context, bCtx *v1.BabysitContext) bool promptFile := path.Join(in.Config.WorkDir, ".claude", "prompts", v1.SystemPromptFile) agent := babysitAgent - args := []string{ - "--add-dir", in.Config.RepositoryDir, - "--agents", agent, - "--system-prompt-file", promptFile, - "--model", string(in.model), - "-p", bCtx.Prompt, - "--output-format", "stream-json", - "--verbose", - } + args := in.baseArgs(promptFile, agent, bCtx.Prompt) var envOpt exec.Option if in.Config.Run.IsProxyEnabled() { @@ -134,15 +126,7 @@ func (in *Claude) AnalysisFollowUpRun(ctx context.Context, followUpPrompt string if in.Config.Run.Mode == console.AgentRunModeWrite { agent = autonomousAgent } - args := []string{ - "--add-dir", in.Config.RepositoryDir, - "--agents", agent, - "--system-prompt-file", promptFile, - "--model", string(in.model), - "-p", followUpPrompt, - "--output-format", "stream-json", - "--verbose", - } + args := in.baseArgs(promptFile, agent, followUpPrompt) var opts []exec.Option if in.Config.Run.IsProxyEnabled() { @@ -195,14 +179,7 @@ func (in *Claude) start(ctx context.Context, options ...exec.Option) { if in.Config.Run.Mode == console.AgentRunModeWrite { agent = autonomousAgent } - args := []string{ - "--add-dir", in.Config.RepositoryDir, - "--agents", agent, - "--system-prompt-file", promptFile, - "--model", string(in.model), - "-p", in.Config.Run.Prompt, - "--output-format", "stream-json", - "--verbose"} + args := in.baseArgs(promptFile, agent, in.Config.Run.Prompt) if in.Config.Run.IsProxyEnabled() { options = append(options, @@ -265,6 +242,10 @@ func (in *Claude) ConfigureBabysitRun() error { return err } + if err := in.ensureBundledSkills(); err != nil { + return err + } + settings := NewSettingsBuilder(in.model) settings.AllowTools( "Read", @@ -273,6 +254,7 @@ func (in *Claude) ConfigureBabysitRun() error { "MultiEdit", "Bash", "WebFetch", + "Skill", PluralMCPToolsWildcard, ) defaultTimeout := fmt.Sprintf("%d", in.Config.Run.Runtime.Config.Claude.BashTimeout.Milliseconds()) @@ -289,6 +271,10 @@ func (in *Claude) Configure(consoleURL, consoleToken string) error { return err } + if err := in.ensureBundledSkills(); err != nil { + return err + } + mcp := NewMCPConfigBuilder() mcp. AddURLServer("plural", common.AgentMCPServerURL). @@ -321,6 +307,7 @@ func (in *Claude) Configure(consoleURL, consoleToken string) error { "Bash(rg:*)", "Bash(find:*)", "WebFetch", + "Skill", PluralMCPToolsWildcard, ).DenyTools("Edit", "Write", "Bash(rm:*)", "Bash(sudo:*)") } else { @@ -331,6 +318,7 @@ func (in *Claude) Configure(consoleURL, consoleToken string) error { "MultiEdit", "Bash", "WebFetch", + "Skill", PluralMCPToolsWildcard, ) } @@ -348,6 +336,22 @@ func (in *Claude) configPath() string { return path.Join(in.Config.WorkDir, ".claude") } +func (in *Claude) baseArgs(promptFile, agentsJSON, userPrompt string) []string { + args := []string{ + "--add-dir", in.Config.WorkDir, + "--add-dir", in.Config.RepositoryDir, + "--agents", agentsJSON, + "--system-prompt-file", promptFile, + "--model", string(in.model), + "--output-format", "stream-json", + "--verbose", + } + if userPrompt != "" { + args = append(args, "-p", userPrompt) + } + return args +} + func (in *Claude) withConfigEnv(env []string) []string { return append(env, fmt.Sprintf("CLAUDE_CONFIG_DIR=%s", in.configPath())) } diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go b/go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go new file mode 100644 index 0000000000..5b114e7854 --- /dev/null +++ b/go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go @@ -0,0 +1,35 @@ +package claude + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "k8s.io/klog/v2" + + "github.com/pluralsh/console/go/deployment-operator/pkg/harness/exec" + "github.com/pluralsh/console/go/deployment-operator/pkg/log" +) + +const bundledSkillsDir = "skills" + +func (in *Claude) ensureBundledSkills() error { + src := filepath.Join(in.Config.WorkDir, bundledSkillsDir) + dst := filepath.Join(in.configPath(), "skills") + + if _, err := os.Stat(src); err != nil { + return fmt.Errorf("bundled skills source %q is missing: %w", src, err) + } + + script := fmt.Sprintf( + `mkdir -p %q && for skill in %q/*/; do name="$(basename "$skill")"; [ -f "$skill/SKILL.md" ] || continue; rm -rf %q/"$name"; cp -a "$skill" %q/"$name"; done`, + dst, src, dst, dst, + ) + if err := exec.NewExecutable("sh", exec.WithArgs([]string{"-c", script})).Run(context.Background()); err != nil { + return fmt.Errorf("failed to install bundled skills into %q: %w", dst, err) + } + + klog.V(log.LogLevelExtended).InfoS("claude bundled skills installed", "src", src, "dst", dst) + return nil +} From 052ccc543d12cc08a99714083c1df31bf2ea1e0c Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 11:17:06 +0200 Subject: [PATCH 10/18] update --- .../agentrun-harness/tool/claude/claude.go | 8 ----- .../agentrun-harness/tool/claude/skills.go | 35 ------------------- 2 files changed, 43 deletions(-) delete mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go b/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go index 18e7fecfe8..df1eae082d 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/claude/claude.go @@ -242,10 +242,6 @@ func (in *Claude) ConfigureBabysitRun() error { return err } - if err := in.ensureBundledSkills(); err != nil { - return err - } - settings := NewSettingsBuilder(in.model) settings.AllowTools( "Read", @@ -271,10 +267,6 @@ func (in *Claude) Configure(consoleURL, consoleToken string) error { return err } - if err := in.ensureBundledSkills(); err != nil { - return err - } - mcp := NewMCPConfigBuilder() mcp. AddURLServer("plural", common.AgentMCPServerURL). diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go b/go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go deleted file mode 100644 index 5b114e7854..0000000000 --- a/go/deployment-operator/pkg/agentrun-harness/tool/claude/skills.go +++ /dev/null @@ -1,35 +0,0 @@ -package claude - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "k8s.io/klog/v2" - - "github.com/pluralsh/console/go/deployment-operator/pkg/harness/exec" - "github.com/pluralsh/console/go/deployment-operator/pkg/log" -) - -const bundledSkillsDir = "skills" - -func (in *Claude) ensureBundledSkills() error { - src := filepath.Join(in.Config.WorkDir, bundledSkillsDir) - dst := filepath.Join(in.configPath(), "skills") - - if _, err := os.Stat(src); err != nil { - return fmt.Errorf("bundled skills source %q is missing: %w", src, err) - } - - script := fmt.Sprintf( - `mkdir -p %q && for skill in %q/*/; do name="$(basename "$skill")"; [ -f "$skill/SKILL.md" ] || continue; rm -rf %q/"$name"; cp -a "$skill" %q/"$name"; done`, - dst, src, dst, dst, - ) - if err := exec.NewExecutable("sh", exec.WithArgs([]string{"-c", script})).Run(context.Background()); err != nil { - return fmt.Errorf("failed to install bundled skills into %q: %w", dst, err) - } - - klog.V(log.LogLevelExtended).InfoS("claude bundled skills installed", "src", src, "dst", dst) - return nil -} From 526c12a75dfe4169d3945c482333bd45441dbd2d Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 13:41:57 +0200 Subject: [PATCH 11/18] fix codex skills --- .../dockerfiles/agent-harness/base.Dockerfile | 12 ++-- .../pkg/agentrun-harness/tool/codex/codex.go | 8 +++ .../tool/codex/codex_types.go | 10 +++ .../pkg/agentrun-harness/tool/codex/skills.go | 48 ++++++++++++++ .../tool/codex/skills_test.go | 65 +++++++++++++++++++ 5 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/codex/skills.go create mode 100644 go/deployment-operator/pkg/agentrun-harness/tool/codex/skills_test.go diff --git a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile index 605cf6c581..bcb291e203 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile +++ b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile @@ -107,15 +107,19 @@ WORKDIR /plural COPY deployment-operator/dockerfiles/agent-harness/system /plural/system COPY deployment-operator/dockerfiles/agent-harness/skills /plural/skills -RUN for provider in .claude .codex .gemini .opencode; do \ +# Provider skill paths: +# - Claude/Gemini/OpenCode: ./skills under /plural +# - Codex: [[skills.config]] in /plural/.codex/config.toml (see codex writeCodexConfig) +# https://developers.openai.com/codex/skills +RUN for provider in .claude .gemini .opencode; do \ mkdir -p "/plural/$${provider}/skills"; \ done && \ for skill in /plural/skills/*/; do \ name="$$(basename "$$skill")"; \ [ -f "$$skill/SKILL.md" ] || continue; \ - for provider in .claude .codex .gemini .opencode; do \ - rm -rf "/plural/$${provider}/skills/$${name}"; \ - cp -a "$$skill" "/plural/$${provider}/skills/$${name}"; \ + for dest in /plural/.claude/skills /plural/.gemini/skills /plural/.opencode/skills; do \ + rm -rf "$${dest}/$${name}"; \ + cp -a "$$skill" "$${dest}/$${name}"; \ done; \ done diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go index d43e1f38b4..151413be30 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex.go @@ -188,6 +188,14 @@ func (in *Codex) writeCodexConfig() error { } cfg.Features = &CodexGlobalFeatures{Skills: true} + skillConfigs, err := bundledSkillConfigs(bundledSkillsDir(in.Config.WorkDir)) + if err != nil { + return err + } + if len(skillConfigs) > 0 { + cfg.Skills = &SkillsSettings{Config: skillConfigs} + } + config, err := WriteCodexConfig(path.Join(in.Config.WorkDir, ".codex"), cfg) if err != nil { return err diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go index a88e4c0d77..2923c0005b 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/codex/codex_types.go @@ -263,6 +263,16 @@ type CodexConfig struct { Profiles map[string]*Profile `toml:"profiles"` MCPServers map[string]*MCPServer `toml:"mcp_servers"` Features *CodexGlobalFeatures `toml:"features,omitempty"` + Skills *SkillsSettings `toml:"skills,omitempty"` +} + +type SkillsSettings struct { + Config []SkillConfigEntry `toml:"config,omitempty"` +} + +type SkillConfigEntry struct { + Path string `toml:"path"` + Enabled bool `toml:"enabled,omitempty"` } type CodexGlobalFeatures struct { diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/codex/skills.go b/go/deployment-operator/pkg/agentrun-harness/tool/codex/skills.go new file mode 100644 index 0000000000..75217878d2 --- /dev/null +++ b/go/deployment-operator/pkg/agentrun-harness/tool/codex/skills.go @@ -0,0 +1,48 @@ +package codex + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +const bundledSkillsDirName = "skills" + +func bundledSkillsDir(workDir string) string { + return filepath.Join(workDir, bundledSkillsDirName) +} + +func bundledSkillConfigs(skillsDir string) ([]SkillConfigEntry, error) { + entries, err := os.ReadDir(skillsDir) + if err != nil { + return nil, fmt.Errorf("read bundled skills dir %q: %w", skillsDir, err) + } + + var configs []SkillConfigEntry + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + skillPath := filepath.Join(skillsDir, entry.Name(), "SKILL.md") + if _, err := os.Stat(skillPath); err != nil { + if os.IsNotExist(err) { + continue + } + return nil, fmt.Errorf("stat skill %q: %w", skillPath, err) + } + + configs = append(configs, SkillConfigEntry{ + Path: skillPath, + Enabled: true, + }) + } + + sort.Slice(configs, func(i, j int) bool { + return strings.ToLower(configs[i].Path) < strings.ToLower(configs[j].Path) + }) + + return configs, nil +} diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/codex/skills_test.go b/go/deployment-operator/pkg/agentrun-harness/tool/codex/skills_test.go new file mode 100644 index 0000000000..66b870114e --- /dev/null +++ b/go/deployment-operator/pkg/agentrun-harness/tool/codex/skills_test.go @@ -0,0 +1,65 @@ +package codex + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pelletier/go-toml/v2" +) + +func TestBundledSkillConfigs(t *testing.T) { + skillsDir := t.TempDir() + for _, name := range []string{"plural-services", "plural-git-repository"} { + dir := filepath.Join(skillsDir, name) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("---\nname: "+name+"\n---\n"), 0644); err != nil { + t.Fatal(err) + } + } + + configs, err := bundledSkillConfigs(skillsDir) + if err != nil { + t.Fatalf("bundledSkillConfigs: %v", err) + } + if len(configs) != 2 { + t.Fatalf("expected 2 skills, got %d", len(configs)) + } + if configs[0].Path != filepath.Join(skillsDir, "plural-git-repository", "SKILL.md") { + t.Fatalf("unexpected first skill path: %s", configs[0].Path) + } + if !configs[0].Enabled { + t.Fatal("expected skills to be enabled") + } +} + +func TestCodexConfigSkillsConfigTOML(t *testing.T) { + cfg := &CodexConfig{ + Features: &CodexGlobalFeatures{Skills: true}, + Skills: &SkillsSettings{ + Config: []SkillConfigEntry{{ + Path: "/plural/skills/plural-git-repository/SKILL.md", + Enabled: true, + }}, + }, + } + + data, err := toml.Marshal(cfg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + out := string(data) + if !strings.Contains(out, "[[skills.config]]") { + t.Fatalf("expected [[skills.config]] in config, got:\n%s", out) + } + if !strings.Contains(out, "/plural/skills/plural-git-repository/SKILL.md") { + t.Fatalf("expected skill path in config, got:\n%s", out) + } + if !strings.Contains(out, "enabled = true") { + t.Fatalf("expected enabled = true in config, got:\n%s", out) + } +} From a27013e9247cf98385c6b87f706c6694fa0532d1 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 14:16:01 +0200 Subject: [PATCH 12/18] fix base.Dockerfile --- .../dockerfiles/agent-harness/base.Dockerfile | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile index bcb291e203..a669d5d2e7 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile +++ b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile @@ -107,19 +107,16 @@ WORKDIR /plural COPY deployment-operator/dockerfiles/agent-harness/system /plural/system COPY deployment-operator/dockerfiles/agent-harness/skills /plural/skills -# Provider skill paths: -# - Claude/Gemini/OpenCode: ./skills under /plural -# - Codex: [[skills.config]] in /plural/.codex/config.toml (see codex writeCodexConfig) -# https://developers.openai.com/codex/skills -RUN for provider in .claude .gemini .opencode; do \ +# Copy bundled skills into each runtime's discovery path under /plural. +RUN for provider in .claude .codex .gemini .opencode; do \ mkdir -p "/plural/$${provider}/skills"; \ done && \ for skill in /plural/skills/*/; do \ name="$$(basename "$$skill")"; \ [ -f "$$skill/SKILL.md" ] || continue; \ - for dest in /plural/.claude/skills /plural/.gemini/skills /plural/.opencode/skills; do \ - rm -rf "$${dest}/$${name}"; \ - cp -a "$$skill" "$${dest}/$${name}"; \ + for provider in .claude .codex .gemini .opencode; do \ + rm -rf "/plural/$${provider}/skills/$${name}"; \ + cp -a "$$skill" "/plural/$${provider}/skills/$${name}"; \ done; \ done From 5e7e20c1e52aa70c650f1437599f72a7c03d27a8 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 14:26:38 +0200 Subject: [PATCH 13/18] cleanup --- go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile index a669d5d2e7..1228e30048 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile +++ b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile @@ -115,7 +115,6 @@ RUN for provider in .claude .codex .gemini .opencode; do \ name="$$(basename "$$skill")"; \ [ -f "$$skill/SKILL.md" ] || continue; \ for provider in .claude .codex .gemini .opencode; do \ - rm -rf "/plural/$${provider}/skills/$${name}"; \ cp -a "$$skill" "/plural/$${provider}/skills/$${name}"; \ done; \ done From b538e045925528e0511e0c2c6fe8e1b7a2a1ca25 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Thu, 11 Jun 2026 14:38:11 +0200 Subject: [PATCH 14/18] fix base.Dockerfile --- .../dockerfiles/agent-harness/base.Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile index 1228e30048..7404d27626 100644 --- a/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile +++ b/go/deployment-operator/dockerfiles/agent-harness/base.Dockerfile @@ -108,14 +108,15 @@ COPY deployment-operator/dockerfiles/agent-harness/system /plural/system COPY deployment-operator/dockerfiles/agent-harness/skills /plural/skills # Copy bundled skills into each runtime's discovery path under /plural. +# Shell vars use single `$` here (not `$$`) — `$$` expands to the shell PID in RUN. RUN for provider in .claude .codex .gemini .opencode; do \ - mkdir -p "/plural/$${provider}/skills"; \ + mkdir -p "/plural/${provider}/skills"; \ done && \ for skill in /plural/skills/*/; do \ - name="$$(basename "$$skill")"; \ - [ -f "$$skill/SKILL.md" ] || continue; \ + name="$(basename "$skill")"; \ + [ -f "$skill/SKILL.md" ] || continue; \ for provider in .claude .codex .gemini .opencode; do \ - cp -a "$$skill" "/plural/$${provider}/skills/$${name}"; \ + cp -a "$skill" "/plural/${provider}/skills/${name}"; \ done; \ done From e7b6103455c7be282138145b4693d5a44864ced2 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 12 Jun 2026 10:33:21 +0200 Subject: [PATCH 15/18] update opencode --- .../tool/opencode/opencode_templates_test.go | 15 +++++++++++++++ .../tool/opencode/templates/opencode.json.gotmpl | 10 ++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/opencode_templates_test.go b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/opencode_templates_test.go index c14cb9f152..2faa0d9619 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/opencode_templates_test.go +++ b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/opencode_templates_test.go @@ -201,6 +201,21 @@ func TestConfigTemplate_Provider(t *testing.T) { }) } +func TestConfigTemplate_SkillPermissions(t *testing.T) { + t.Run("analysis and autonomous agents allow all skills", func(t *testing.T) { + out := renderJSON(t, baseInput(console.AgentRunModeWrite)) + + agent := out["agent"].(map[string]any) + for _, name := range []string{"analysis", "autonomous"} { + permission := agent[name].(map[string]any)["permission"].(map[string]any) + skill := permission["skill"].(map[string]any) + if skill["*"] != "allow" { + t.Fatalf("agent.%s permission.skill.* = %v, want allow", name, skill["*"]) + } + } + }) +} + func TestConfigTemplate_AgentTools(t *testing.T) { t.Run("analysis agent has plural* tool only", func(t *testing.T) { out := renderJSON(t, baseInput(console.AgentRunModeWrite)) diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl index fadc296573..3e04e24eea 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl +++ b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl @@ -30,7 +30,10 @@ }, {{- end }} "edit": "deny", - "webfetch": "allow" + "webfetch": "allow", + "skill": { + "*": "allow" + } } }, "autonomous": { @@ -48,7 +51,10 @@ "bash": "allow", "edit": "allow", "webfetch": "allow", - "git": "deny" + "git": "deny", + "skill": { + "*": "allow" + } } } }, From df95931cf0a56d3466783a335d358fd6fa500ac2 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 12 Jun 2026 10:34:28 +0200 Subject: [PATCH 16/18] update opencode --- .../tool/opencode/templates/opencode.json.gotmpl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl index 3e04e24eea..9f0be08ebb 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl +++ b/go/deployment-operator/pkg/agentrun-harness/tool/opencode/templates/opencode.json.gotmpl @@ -67,11 +67,6 @@ } }, "username": "plural", - "skills": { - "paths": [ - "/plural/skills" - ] - }, "provider": { "{{ .Provider }}": { {{ if or (eq .Provider "plural") .OpenAICompatible }}"npm": "@ai-sdk/openai", From e1aa204e6c40ed17259744502d14b132cb1e45e7 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 12 Jun 2026 13:56:43 +0200 Subject: [PATCH 17/18] enable skills for gemini --- .../tool/gemini/templates/settings.json.gotmpl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl b/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl index 8a50958e7d..0c2a1390ad 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl +++ b/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl @@ -60,4 +60,7 @@ "inactivityTimeout": {{ .InactivityTimeout }} } } + "skills": { + "enabled": true + } } From ad2f77a82a29c7d1076e648ba57d096901470092 Mon Sep 17 00:00:00 2001 From: Lukasz Zajaczkowski Date: Fri, 12 Jun 2026 16:26:53 +0200 Subject: [PATCH 18/18] fix gemini config --- .../agentrun-harness/tool/gemini/templates/settings.json.gotmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl b/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl index 0c2a1390ad..a7cfd0e65e 100644 --- a/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl +++ b/go/deployment-operator/pkg/agentrun-harness/tool/gemini/templates/settings.json.gotmpl @@ -59,7 +59,7 @@ "shell": { "inactivityTimeout": {{ .InactivityTimeout }} } - } + }, "skills": { "enabled": true }