diff --git a/harness/adt_client.py b/harness/adt_client.py
index 8be4c13..aba87ca 100644
--- a/harness/adt_client.py
+++ b/harness/adt_client.py
@@ -70,3 +70,31 @@ class AdtClient:
out[uri.group(1) if uri else "?"] = (ok is not None and ok.group(1) == "true",
msg.group(1) if msg else "")
return out
+
+ def write_activate(self, uri, name, source):
+ """Lock, write source/main, unlock, activate. Fallback for EPOD: a second sap_push_source on a
+ PROG or FUNC writes an inactive version that is not in the inactive list, so no activation runs
+ (probe Z0ZZZ999_PROBE_PROG, 2026-10-03). Returns (activated, [(severity, text)])."""
+ if not self.csrf:
+ self.fetch_csrf()
+ st = {"X-sap-adt-sessiontype": "stateful"}
+ _, _, t = self._req("POST", uri + "?_action=LOCK&accessMode=MODIFY", None,
+ dict(st, Accept="application/vnd.sap.as+xml;charset=UTF-8;"
+ "dataname=com.sap.adt.lock.result"))
+ lh = re.search(r"(.*?)", t).group(1)
+ try:
+ self._req("PUT", f"{uri}/source/main?lockHandle={lh}", source,
+ dict(st, **{"Content-Type": "text/plain; charset=utf-8"}))
+ finally:
+ self._req("POST", f"{uri}?_action=UNLOCK&lockHandle={lh}", None, st)
+ body = (''
+ f''
+ '')
+ _, _, t = self._req("POST", "/sap/bc/adt/activation?method=activate&preauditRequested=true", body,
+ {"Content-Type": "application/xml", "Accept": "application/xml"})
+ msgs = [(m.group(1), re.sub(r"<[^>]+>", "", m.group(2)).strip())
+ for m in re.finditer(r']*\btype="(\w)"[^>]*>(.*?)', t, re.S)]
+ done = 'activationExecuted="true"' in t and not any(sev in ("E", "A") for sev, _ in msgs)
+ return done, msgs
+
diff --git a/harness/generator.py b/harness/generator.py
index 6e8b4e8..c3f30e1 100644
--- a/harness/generator.py
+++ b/harness/generator.py
@@ -495,7 +495,7 @@ def make_k_variant(src_id, new_id, style, model, base_url, run_base, tool_schema
src_dir, task_dir = os.path.join(pool_root, src_id), os.path.join(pool_root, new_id)
b = bundle_of(src_dir)
b["files"] = {k: v for k, v in b["files"].items()
- if not (k.startswith("faulty/") or k in ("mutation.json",))}
+ if not (k.startswith("faulty/") or k in ("mutation.json", "review.json", "empirical.json"))}
spec = b["files"]["spec.md"]
terms = contract_terms(b["task"])
messages = [{"role": "user", "content":
diff --git a/harness/proxy.py b/harness/proxy.py
index 6ce50e5..156323e 100644
--- a/harness/proxy.py
+++ b/harness/proxy.py
@@ -50,6 +50,8 @@ class ToolProxy:
self.fail_streak = 0
self.max_fail_streak = 0
self.log = open(log_path, "a")
+ self.adt = None
+ self.fallbacks = [] # writes that needed the ADT activation fallback
def schemas(self):
tools = [t for t in self.mcp.list_tools() if t["name"] in MODEL_TOOLS]
@@ -123,6 +125,8 @@ class ToolProxy:
if tool in WRITE_TOOLS and (tool == "sap_activate" or args.get("activate", True)):
self.activations += 1
err, text = self.mcp.call(tool, args)
+ if tool == "sap_push_source":
+ err, text = self._activation_fallback(args, err, text)
result = (err, self._filter(tool, text))
if tool in WRITE_TOOLS:
failed = err or '"success":false' in text.replace(" ", "")
@@ -133,6 +137,32 @@ class ToolProxy:
self.log.flush()
return result
+ def _activation_fallback(self, args, err, text):
+ """EPOD does not activate a second write of a PROG or FUNC (outcome notExecuted, and sap_activate
+ cannot help). Write and activate the same source through ADT REST; the agent sees one normal result."""
+ otype = str(args.get("objectType", "")).upper()
+ if otype not in ("PROG", "FUNC") or args.get("includeType") or args.get("activate") is False \
+ or '"outcome":"notExecuted"' not in text.replace(" ", ""):
+ return err, text
+ name = str(args.get("objectName", "")).lower()
+ uri = (f"/sap/bc/adt/programs/programs/{name}" if otype == "PROG" else
+ f"/sap/bc/adt/functions/groups/{str(args.get('functionGroup', '')).lower()}/fmodules/{name}")
+ try:
+ if self.adt is None:
+ from .adt_client import AdtClient
+ self.adt = AdtClient()
+ done, msgs = self.adt.write_activate(uri, name.upper(), args.get("source", ""))
+ except Exception as e: # noqa: BLE001 keep the EPOD result when the fallback fails
+ self.fallbacks.append({"object": name, "error": str(e)[:200]})
+ return err, text
+ self.fallbacks.append({"object": name, "activated": done})
+ out = {"success": done,
+ "message": "Source written and activation requested." if done else
+ "Source written. Activation failed. See the activation messages.",
+ "activation": {"messages": [{"severity": sv, "message": tx} for sv, tx in msgs],
+ "activationExecuted": done, "success": done}}
+ return False, json.dumps(out)
+
def note(self, kind, content):
self.log.write(json.dumps({"t": time.time(), "kind": kind, "content": content}) + "\n")
self.log.flush()
diff --git a/harness/runner.py b/harness/runner.py
index 40341f1..f5ce9b6 100644
--- a/harness/runner.py
+++ b/harness/runner.py
@@ -207,6 +207,8 @@ class Runner:
rep["agent_seconds"] = round(time.time() - t1, 1)
rep["tool_calls"], rep["activations"] = proxy.calls, proxy.activations
rep["max_fail_streak"] = proxy.max_fail_streak
+ if proxy.fallbacks:
+ rep["adt_fallbacks"] = proxy.fallbacks
# 3 collect
hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")}