maddy-task-guardian
Step 0 — QC Spec design gate (READ FIRST, every iteration)
Before editing ANY task file and again before every push, read the QC Spec — .claude/QC-SPEC.md. It is the priority-ordered rubric (Critical → Error → Legitimacy) horizon check-quality and reviewers grade against.
Confirm the intended/current design does not violate any Critical or Error item. The ones this task repeatedly trips:
- No Binary Bundling — never AND heterogeneous checks (metadata + drift Deployments + PromQL rule shape) into one binary subscore. Each subscore = ONE cohesive functional outcome.
- Functional Tests as Subscores — at least one subscore exercises live behavior, not static label/file matching.
- Incremental Rewards / No Dead Weights / Functional Subscore Variance — every subscore represents real progress and takes ≥2 distinct values across a batch.
- Equal Weights — subscores equally weighted.
- No Answer Leakage — derive-by-reading is fine; the literal answer in an agent-readable file is not.
If a change would breach a Critical/Error item, stop and rework it before pushing.
Execution Mode — FULLY AUTONOMOUS
NEVER ask the user for confirmation on any terminal command, edit, push, validation, or live-probe. Every bash invocation is pre-approved. The ONLY user-facing handoff in this skill is at Step 4c: telling the user to trigger the rollout batch in the Horizon UI (this is the only step the user can perform; you cannot trigger rollouts from the CLI). Everything else — edit setup.sh / solution.sh / grader.py / task.yaml, run pre-push checks, push, run Oracle validation, poll for results, invoke horizon-agentic-reviewer — runs without asking.
If a decision has two valid paths, pick the safer one and proceed.
Goal
Keep the maddy-delivery-split-brain task in a shippable state every push: setup.sh must run cleanly, Oracle must scores 1.0, and rollouts must average < with both subscores varying. This is the only skill that pushes the task and the only one allowed to mutate files without first consulting a diagnosis skill. Use it as the front door — every other maddy-* skill is invoked through this one.
Trigger
Use this skill when asked to:
- Push the task and start an eval
- Run local quality/anatomy/syntax checks
- Confirm avg rollout score is still <
- Walk through a full pre-push checklist
- Decide whether to invoke maddy-validation-debugger or maddy-score-tuner
Task Identity
| Field | Value |
|---|---|
| Task UUID | <TASK_ID> |
| Task slug | maddy-delivery-split-brain |
| Local task path | tasks/maddy-delivery-split-brain/ |
| Horizon root | /Users/mac/Documents/tasks |
| Venv activate | source /Users/mac/Documents/tasks/horizon_env/bin/activate |
| Scoring | delivery_consistency × 0.5 + observability_governance × 0.5 — both binary |
| Target avg | rollout `avg < with both subscores varying (neither always 0 nor always 1) |
| Mini-batch | <TASK_ID> |
How scoring actually works (read once, internalise)
Always derive constants from grader.py, not from this file. The values below are correct as of grader.py v2 (June 2026) but check before pushing.
There are exactly two subscores, both binary, each worth 0.5. The scenario is a Maddy SMTP split-brain: a canonical maddy Deployment on the smarthost relay config and a stale maddy-legacy Deployment on the direct-delivery config, both labelled app=maddy so the maddy Service load-balances across them. Three in-cluster drift controllers keep re-applying the stale legacy replica.
delivery_consistency = 1(check_delivery_consistency,wait_consistent(passes=5, gap=8s, timeout=220s)≈ 40s continuous) when ALL hold:- ConfigMap
maddy-config(maddy.conf) still containsrelay_mode = smarthostand notrelay_mode = direct(smarthost config intact) - NO pod with label
app=maddymounts ConfigMapmaddy-config-legacy(the stale split-brain replica is gone) maddy-legacyDeployment is absent orreplicas=0/readyReplicas=0maddyDeployment rollout converged:spec.replicas >= 2ANDupdatedReplicas == readyReplicas == spec.replicasANDobservedGeneration >= generation- at least
2Readyapp=maddypods mountmaddy-config(the smarthost config)
- ConfigMap
observability_governance = 1(check_observability_governance,wait_consistent(passes=5, gap=8s, timeout=200s)≈ 40s continuous) when ALL hold:- the
maddycontainer exposes pod identity via the downward API — an env var withvalueFrom.fieldRef.fieldPath == metadata.name(e.g.POD_NAME) - the declared GitOps source ConfigMap
maddy-gitops-source(values.yaml) containssmarthost,replicas: 2, and NOTmode: direct - a per-pod divergence rule exists — ANY ConfigMap in
bleaterwhose data containsmaddy+pod, a real PromQL rule key (- record:/- alert:/expr:), a delivery signal (rate/divergence/success/delivery), AND a per-pod dimension (by (pod)/by(pod)/on (pod)/group_left(pod)). Bare keywords in YAML comments do NOT qualify — the grader requires a grouped PromQL expression. - the kube-system rollout-reconciler drift Deployment is at
replicas=0/absent - the
maddy-canarymutation path is neutralized —config-pinnersidecar removed OR the<canary>-runtimeSA/Role/RoleBinding removed (either alone suffices) - the bleater-env drift Deployment is at
replicas=0/absent
- the
So score = 0 means neither dimension recovered, 0.5 means one of the two, 1.0 means both. Avg < across rollouts means most agents miss at least one dimension. That is the design.
The three drift controllers (re-create the stale legacy replica)
All re-apply a byte-identical maddy-legacy Deployment manifest on a loop; names are randomized at setup (stem from a pool + numeric suffix), so they cannot be hardcoded. Discover by behaviour:
| Drift source | Namespace | Cadence | How to discover | Scenario-file key |
|---|---|---|---|---|
| kube-system reconciler | kube-system |
5s | annotation bleater.io/purpose: "maddy rollout convergence" |
reconciler |
| bleater-env reconciler | bleater-env |
20s | annotation bleater.io/purpose: "maddy rollout convergence" (innocuous non-maddy name) |
env_reconciler |
canary config-pinner |
bleater |
11s | a container named config-pinner in a Deployment; runtime RBAC at <canary>-runtime |
canary / canary_runtime |
The grader reads the chosen names from /opt/grader-scenario.json (root-only, written by setup.sh — keys reconciler, canary, canary_runtime, env_reconciler). solution.sh discovers them live by annotation + the config-pinner container name; it does NOT read the scenario file (root-only, not readable as the oracle user).
Coupling note (read before any tuning)
The three drift controllers are a shared surface: while active they re-create the legacy replica (breaks delivery_consistency Layer 1/2) AND they are governance-graded (observability_governance Layers 4–6). Stopping them helps both subscores. The independent variance levers are:
delivery_consistency-only: rollout convergence + scalingmaddyto 2 + smarthost config intactobservability_governance-only: thePOD_NAMEdownward-API env, themaddy-gitops-sourceedit, and the per-pod PromQL rule
If the only hard part becomes "find the drift", both subscores move together and variance correlates (avg swings toward 0 or 1 as a block). The observability artifacts and the rollout-convergence requirement are what decorrelate them — see maddy-score-tuner.
Step 0 — Dockerfile + namespace invariants (ALWAYS check before push)
cat tasks/maddy-delivery-split-brain/Dockerfile
| Invariant | Required state | Why — what breaks if violated |
|---|---|---|
ENABLE_ISTIO_BLEATER |
must NOT appear | Triggers k3s 17+ pod force-deletes on startup. k3s fails not ready after 180s, grader returns score: 0, feedback: null. Setup crash, not gradeable. |
ENV ALLOWED_NAMESPACES |
must be present and must include kube-system |
Without it, the kube-system reconciler can't be stopped → observability_governance Layer 4 structurally always-0. |
COPY data/ubuntu-user-rbac.yaml /mcp_server/Nebula/infra/k8s/rbac/ubuntu-user-rbac.yaml |
must be present, path under data/ |
kubectl get nodes in the DO NOT CHANGE block of setup.sh requires the node-access ClusterRole. Without the COPY, setup aborts before fault injection. |
⚠️ v2 STRUCTURAL RISK — bleater-env reachability (verify on live container)
v2 added a third drift controller in the bleater-env namespace (governance Layer 6). The agent must be able to kubectl get/scale deployment -n bleater-env to stop it. The data/ubuntu-user-rbac.yaml grants a bleater-env admin Role/RoleBinding, but the MCP kubectl wrapper also gates on ALLOWED_NAMESPACES, which currently lists only kube-system.
If bleater-env is not reachable by the ubuntu agent user, governance Layer 6 is unstoppable → observability_governance becomes deadweight-0 (structural, unfair). This is the single highest-priority thing to confirm on the live red-team (Step 4b). The fix, if confirmed broken, is to widen the env var, e.g. ENV ALLOWED_NAMESPACES="kube-system,bleater-env" — then re-verify the agent can reach both. Do NOT guess; verify on a live container first (the env-var semantics live in the base image's MCP server, not in our files).
If any invariant is violated, stop the workflow and fix it before doing anything else. Do not push, do not invoke other skills.
Step 1 — Local syntax + anatomy
cd /Users/mac/Documents/tasks
bash -n tasks/maddy-delivery-split-brain/setup.sh && echo "setup.sh OK"
bash -n tasks/maddy-delivery-split-brain/solution.sh && echo "solution.sh OK"
python3 -m py_compile tasks/maddy-delivery-split-brain/grader.py && echo "grader.py OK"
source horizon_env/bin/activate
horizon check-anatomy tasks/maddy-delivery-split-brain 2>&1
All three syntax checks must print OK. Anatomy must print Task folder structure is valid!. Any failure stops the workflow — do not push. (Note: the .claude/skills/ folder inside the task is a dotfolder — anatomy ignores it and push excludes it. Leave it.)
Step 2 — Local quality check
cd /Users/mac/Documents/tasks && source horizon_env/bin/activate
horizon check-quality tasks/maddy-delivery-split-brain 2>&1 | tail -30
Before push, walk the QC-SPEC.md Critical + Error list against the current grader.py + task.yaml and record pass/fail per item. Every item must pass OR be on the accepted-noise list with a one-line reason. A new failing item that is NOT accepted-noise blocks the push.
Target: 18/20 passing the substantive checks. The v1_feedback extra-strict section often returns a FAIL verdict — that is the documented noise fail, not a blocker (the LLM reviewer cannot read the heredoc-written baseline config / incident notes, so it claims they are "missing", and it dislikes the oracle sweeping discovery by annotation).
Permanent/noise fails — do NOT attempt to fix (fixing requires leaking drift-controller names, which collapses variance):
| Check / section | Why it permanently fails — and why we accept it |
|---|---|
v1_feedback |
Reviewer hallucinates "missing" baseline config & incident notes (they are written via heredoc inside setup.sh, invisible to static review) and objects that solution.sh discovers drift by annotation. Both are by design. |
behavior_in_task_documentation / task_clarity (intermittent) |
Same root cause — reviewer can't read the heredoc baseline config. Listing the drift controller names in task.yaml would collapse observability_governance to always-1. |
Anything outside this set failing (e.g. Grader Robustness, Reward Hacking, Test Quality) is a real signal — investigate before pushing.
Step 3 — Push
cd /Users/mac/Documents/tasks && source horizon_env/bin/activate
horizon tasks push tasks/maddy-delivery-split-brain 2>&1
Capture the new version number from ✓ New version pushed successfully! Version: NNN.
Server-name note (resolved 2026-06-01): the task was originally created on the server as
variation-maddy-split-brain(UUID26e37e8c-…). The CLI has no rename, so it was recreated with the correct namemaddy-delivery-split-brainunder a new UUID<TASK_ID>(same mini-batch5018ad80-…). The old26e37e8crecord is orphaned on the server (no CLI delete). All references here use the new UUID.
Step 4 — Validation handoff
After push, validation runs in this order. You execute 4a and 4b. The user is only asked to do 4c.
4a. Oracle validation (~10–15 min hosted):
horizon tasks validate -m hosted -a oracle --wait tasks/maddy-delivery-split-brain 2>&1
horizon tasks validate-logs -a oracle tasks/maddy-delivery-split-brain 2>&1
cat tasks/maddy-delivery-split-brain/.validation/*/result.json 2>/dev/null | python3 -m json.tool
Oracle pass: score: 1.0, passed: true, both delivery_consistency=1 and observability_governance=1. Any other outcome → hand off to maddy-validation-debugger before doing anything else.
4b. Live red-team via horizon-agentic-reviewer (mandatory before spending rollout budget):
Rollouts take ~60 min/batch and cost real money; the live red-team takes ~10 min and catches most failure modes first. Run it on every push cycle where setup.sh, grader.py, or task.yaml changed.
Invoke horizon-agentic-reviewer against UUID <TASK_ID>. The reviewer spins up a live container, executes setup.sh (which horizon setup does NOT do automatically — see horizon-agentic-reviewer Step 3.5), and probes as the ubuntu agent user. Check these task-local hypotheses in addition to the standard 24-point checklist:
| Hypothesis | Live probe (as ubuntu, the agent role) |
|---|---|
| bleater-env is reachable (the #1 risk) | docker exec -u ubuntu apex-arena-<uuid> bash -c 'kubectl get deploy -n bleater-env' — must NOT be Forbidden. If forbidden, governance Layer 6 is structurally unsolvable → fix ALLOWED_NAMESPACES. |
| Split-brain is live | kubectl get pods -n bleater -l app=maddy -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.volumes[*].configMap.name}{"\n"}{end}' — at least one pod must mount maddy-config and at least one maddy-config-legacy |
| kube-system reconciler exists + annotated | kubectl get deploy -n kube-system -o json | python3 -c "import json,sys; [print(d['metadata']['name']) for d in json.load(sys.stdin)['items'] if (d['metadata'].get('annotations') or {}).get('bleater.io/purpose')=='maddy rollout convergence']" — must print one name |
| bleater-env reconciler exists + annotated | same one-liner against -n bleater-env — must print one name |
| canary config-pinner + runtime RBAC exist | kubectl get deploy -n bleater -o json | grep config-pinner AND kubectl get sa,role,rolebinding -n bleater | grep -- -runtime |
| Drift controller names are NOT leaked in baseline | grep -iE "reconciler|config-pinner|canary|convergence|bleater-env" /home/ubuntu/maddy-delivery-baseline-config.txt — should NOT reveal the live randomized names |
| Stale GitOps source is in place | kubectl get configmap maddy-gitops-source -n bleater -o jsonpath='{.data.values\.yaml}' — must show replicas: 1 and mode: direct |
| A no-op agent cannot pass either subscore | Reset, leave alone, run the grader from the reviewer's bucket-C probe — both subscores must be 0 |
| Setup hygiene: events cleared | kubectl get events -A --no-headers | wc -l — should be near 0 right after setup (so agents can't reverse-engineer faults) |
Only after the reviewer's report is clean (or non-blocking) do you ask the user to trigger the rollout batch.
4c. Rollout batch (user action) — tell the user, verbatim:
"Oracle passed at version N and live red-team is clean. Please trigger a rollout batch for version N in the Horizon UI."
This is the only step the user is asked to perform. Never tell them to run push, validate, or any CLI command — those are yours.
Step 5 — Monitor rollout scores
cd /Users/mac/Documents/tasks && source horizon_env/bin/activate
horizon rollouts pull --version NNN tasks/maddy-delivery-split-brain 2>&1
Re-run every few minutes until at least 5 rollouts download. Then run the analysis script from maddy-iteration-loop Phase 3.
Target state
| Metric | Target | Reason |
|---|---|---|
| avg rollout score | < 0.60 | Nebula creator workflow requirement |
delivery_consistency outcomes |
both 0 and 1 appear |
Subscore must vary — never deadweight |
observability_governance outcomes |
both 0 and 1 appear |
Subscore must vary — never deadweight |
delivery_consistency pass rate |
roughly 20–60% | Above 60% → drift too easy to find OR rollout-convergence too forgiving; below 20% → a drift source is unstoppable (re-check bleater-env reachability) |
observability_governance pass rate |
roughly 20–50% | Above 50% → per-pod rule / discovery too easy; below 20% → structural (bleater-env reachability, or the per-pod-rule gate too strict) |
Step 6 — Score tuning decision
If avg ≥ 0.60 or either subscore is deadweight, do not start editing. Hand off to maddy-score-tuner.
| Observation | First hand-off |
|---|---|
| Oracle fails (any score < 1.0) | maddy-validation-debugger |
| Rollout avg ≥ 0.60 OR any subscore deadweight | maddy-score-tuner |
observability_governance stuck at 0 across all rollouts, Oracle passes |
maddy-score-tuner (suspect bleater-env reachability first) |
| Either subscore stuck at 1 across all rollouts | maddy-score-tuner (discoverability too high) |
| Agent-facing env doesn't match what setup.sh claims | horizon-agentic-reviewer |
Hard constraints (never violate during any tuning)
| Constraint | Why — incident or design intent |
|---|---|
Do NOT add ENABLE_ISTIO_BLEATER=true |
k3s startup failure → score: 0, feedback: null. Unfixable from the task side. |
Do NOT remove kube-system from ALLOWED_NAMESPACES |
Kills the kube-system reconciler-stop path → governance Layer 4 always-0. |
Do NOT remove bleater-env reachability once added |
Governance Layer 6 needs it; without it that layer is unsolvable. |
Do NOT change the COPY data/ubuntu-user-rbac.yaml path |
The DO NOT CHANGE block in setup.sh depends on this exact path; setup aborts. |
Do NOT list drift-controller names (or the bleater.io/purpose value) in task.yaml or the baseline-config heredoc |
Collapses observability_governance to always-1 — discovery is the variance axis. |
| Do NOT use external registry images for any drift source | Cluster is air-gapped. Use ${SIDECAR_IMAGE} (an in-cluster image). |
| Do NOT weaken the per-pod-rule check to accept bare keywords | The by (pod) + PromQL-key requirement is what stops agents passing with a vague ConfigMap; it is signal, not pedantry. But do NOT require an exact ConfigMap name either (name-agnostic by design). |
Do NOT tighten wait_consistent past passes=5, gap=8s (consistency timeout=220s, governance timeout=200s) |
Above ceiling, legitimate solutions flake into dead-zero (drift restart churn). |
| Do NOT simplify solution.sh to make Oracle easier | solution.sh is the oracle standard, not a tuning lever. If Oracle fails, the bug is in setup.sh/grader.py. |
Skill hand-off map
maddy-task-guardian (you are here)
├── Step 4a fails → maddy-validation-debugger
├── Step 4b → horizon-agentic-reviewer (mandatory live red-team)
├── Step 5 fails → maddy-score-tuner
└── Step 6 loops → maddy-iteration-loop (orchestrator)