From aa9fdf2e80ccbeb80e54b848e9a5b240b40f9188 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 06:22:58 +0000 Subject: [PATCH] fix(#380): tolerate Claude harness output-scan markers in strict JSON gates Claude Code v2.1.210+ may prepend a marker line starting with "[harness: subagent output matched instruction-shaped pattern(s):" to a subagent report when the output matches instruction-shaped patterns. The payload itself is the original valid agent JSON. `detect_truncated_agent_output` previously treated any leading text as a "wrapped in prose" signal and returned `truncated: true`, causing valid Monitor / Predictor / Evaluator / Actor payloads to be rejected and wasting a retry (or stopping the workflow with a misleading error). Changes: - Add `_HARNESS_MARKER_PREFIX` constant documenting the exact marker form - Add `_strip_harness_markers(text)` helper that removes only lines whose stripped form starts with that prefix; arbitrary prose is left intact - Call the helper inside `detect_truncated_agent_output` before JSON parsing, after the empty-response check - Add `harness_markers_stripped: list[str]` to every return dict so the event is observable downstream - 6 new regression tests covering: monitor + actor payloads with a harness marker, multiple markers, arbitrary prose still rejected, marker + trailing prose still rejected, clean JSON has empty list - Run `make render-templates` to propagate to all generated trees Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01VpTee1uc3iXXeGZspqkkNh --- .map/scripts/map_step_runner.py | 42 ++++++++ .../templates/map/scripts/map_step_runner.py | 42 ++++++++ .../map/scripts/map_step_runner.py.jinja | 42 ++++++++ tests/test_map_step_runner.py | 100 ++++++++++++++++++ 4 files changed, 226 insertions(+) diff --git a/.map/scripts/map_step_runner.py b/.map/scripts/map_step_runner.py index 187aa918..274da86e 100755 --- a/.map/scripts/map_step_runner.py +++ b/.map/scripts/map_step_runner.py @@ -11046,6 +11046,27 @@ def save_research( _MONITOR_REQUIRED_KEYS = ("valid", "summary", "issues") _ACTOR_REQUIRED_KEYS = tuple(AGENT_OUTPUT_SCHEMAS["actor"]["required_keys"]) +# Leading marker prepended by Claude Code v2.1.210+ when a subagent report +# matches instruction-shaped patterns. The scan is advisory; the payload is +# the original agent output and should be parsed normally once the marker +# is stripped. See: https://code.claude.com/docs/en/sub-agents +_HARNESS_MARKER_PREFIX = "[harness: subagent output matched instruction-shaped pattern(s):" + + +def _strip_harness_markers(text: str) -> tuple[str, list[str]]: + """Strip known provider harness marker lines from the start of ``text``. + + Returns ``(cleaned_text, [stripped_marker_line, ...])``. + Only lines whose stripped form starts with ``_HARNESS_MARKER_PREFIX`` + are removed; any other leading/trailing text is left intact so the + downstream parser still classifies it as unexpected prose. + """ + stripped_markers: list[str] = [] + lines = text.split("\n") + while lines and lines[0].strip().startswith(_HARNESS_MARKER_PREFIX): + stripped_markers.append(lines.pop(0)) + return "\n".join(lines), stripped_markers + def detect_truncated_agent_output( text: str, @@ -11074,6 +11095,9 @@ def detect_truncated_agent_output( # "response ends mid-sentence" "parsed": dict | None, # the parsed object, or None on parse failure "agent_kind": str, # echoed for downstream logging + "harness_markers_stripped": [str, ...], + # provider marker lines removed before + # parsing (empty when no markers present) } ``expected_keys`` defaults per ``agent_kind``: monitor expects @@ -11090,6 +11114,22 @@ def detect_truncated_agent_output( "reasons": ["empty response"], "parsed": None, "agent_kind": agent_kind, + "harness_markers_stripped": [], + } + + # Strip known Claude harness marker lines before JSON parsing (#380). + # Claude Code v2.1.210+ may prepend a marker line to a subagent report + # when the output matches instruction-shaped patterns; these lines are + # not model prose and must not trigger the "leading text" rejection. + stripped, harness_markers = _strip_harness_markers(stripped) + stripped = stripped.strip() + if not stripped: + return { + "truncated": True, + "reasons": ["empty response"], + "parsed": None, + "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } parsed: Optional[dict[str, object]] = None @@ -11129,6 +11169,7 @@ def detect_truncated_agent_output( "reasons": reasons, "parsed": None, "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } # Validate required keys. @@ -11156,6 +11197,7 @@ def detect_truncated_agent_output( "reasons": reasons, "parsed": parsed, "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } diff --git a/src/mapify_cli/templates/map/scripts/map_step_runner.py b/src/mapify_cli/templates/map/scripts/map_step_runner.py index 187aa918..274da86e 100755 --- a/src/mapify_cli/templates/map/scripts/map_step_runner.py +++ b/src/mapify_cli/templates/map/scripts/map_step_runner.py @@ -11046,6 +11046,27 @@ def save_research( _MONITOR_REQUIRED_KEYS = ("valid", "summary", "issues") _ACTOR_REQUIRED_KEYS = tuple(AGENT_OUTPUT_SCHEMAS["actor"]["required_keys"]) +# Leading marker prepended by Claude Code v2.1.210+ when a subagent report +# matches instruction-shaped patterns. The scan is advisory; the payload is +# the original agent output and should be parsed normally once the marker +# is stripped. See: https://code.claude.com/docs/en/sub-agents +_HARNESS_MARKER_PREFIX = "[harness: subagent output matched instruction-shaped pattern(s):" + + +def _strip_harness_markers(text: str) -> tuple[str, list[str]]: + """Strip known provider harness marker lines from the start of ``text``. + + Returns ``(cleaned_text, [stripped_marker_line, ...])``. + Only lines whose stripped form starts with ``_HARNESS_MARKER_PREFIX`` + are removed; any other leading/trailing text is left intact so the + downstream parser still classifies it as unexpected prose. + """ + stripped_markers: list[str] = [] + lines = text.split("\n") + while lines and lines[0].strip().startswith(_HARNESS_MARKER_PREFIX): + stripped_markers.append(lines.pop(0)) + return "\n".join(lines), stripped_markers + def detect_truncated_agent_output( text: str, @@ -11074,6 +11095,9 @@ def detect_truncated_agent_output( # "response ends mid-sentence" "parsed": dict | None, # the parsed object, or None on parse failure "agent_kind": str, # echoed for downstream logging + "harness_markers_stripped": [str, ...], + # provider marker lines removed before + # parsing (empty when no markers present) } ``expected_keys`` defaults per ``agent_kind``: monitor expects @@ -11090,6 +11114,22 @@ def detect_truncated_agent_output( "reasons": ["empty response"], "parsed": None, "agent_kind": agent_kind, + "harness_markers_stripped": [], + } + + # Strip known Claude harness marker lines before JSON parsing (#380). + # Claude Code v2.1.210+ may prepend a marker line to a subagent report + # when the output matches instruction-shaped patterns; these lines are + # not model prose and must not trigger the "leading text" rejection. + stripped, harness_markers = _strip_harness_markers(stripped) + stripped = stripped.strip() + if not stripped: + return { + "truncated": True, + "reasons": ["empty response"], + "parsed": None, + "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } parsed: Optional[dict[str, object]] = None @@ -11129,6 +11169,7 @@ def detect_truncated_agent_output( "reasons": reasons, "parsed": None, "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } # Validate required keys. @@ -11156,6 +11197,7 @@ def detect_truncated_agent_output( "reasons": reasons, "parsed": parsed, "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } diff --git a/src/mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja b/src/mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja index 187aa918..274da86e 100755 --- a/src/mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja +++ b/src/mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja @@ -11046,6 +11046,27 @@ def save_research( _MONITOR_REQUIRED_KEYS = ("valid", "summary", "issues") _ACTOR_REQUIRED_KEYS = tuple(AGENT_OUTPUT_SCHEMAS["actor"]["required_keys"]) +# Leading marker prepended by Claude Code v2.1.210+ when a subagent report +# matches instruction-shaped patterns. The scan is advisory; the payload is +# the original agent output and should be parsed normally once the marker +# is stripped. See: https://code.claude.com/docs/en/sub-agents +_HARNESS_MARKER_PREFIX = "[harness: subagent output matched instruction-shaped pattern(s):" + + +def _strip_harness_markers(text: str) -> tuple[str, list[str]]: + """Strip known provider harness marker lines from the start of ``text``. + + Returns ``(cleaned_text, [stripped_marker_line, ...])``. + Only lines whose stripped form starts with ``_HARNESS_MARKER_PREFIX`` + are removed; any other leading/trailing text is left intact so the + downstream parser still classifies it as unexpected prose. + """ + stripped_markers: list[str] = [] + lines = text.split("\n") + while lines and lines[0].strip().startswith(_HARNESS_MARKER_PREFIX): + stripped_markers.append(lines.pop(0)) + return "\n".join(lines), stripped_markers + def detect_truncated_agent_output( text: str, @@ -11074,6 +11095,9 @@ def detect_truncated_agent_output( # "response ends mid-sentence" "parsed": dict | None, # the parsed object, or None on parse failure "agent_kind": str, # echoed for downstream logging + "harness_markers_stripped": [str, ...], + # provider marker lines removed before + # parsing (empty when no markers present) } ``expected_keys`` defaults per ``agent_kind``: monitor expects @@ -11090,6 +11114,22 @@ def detect_truncated_agent_output( "reasons": ["empty response"], "parsed": None, "agent_kind": agent_kind, + "harness_markers_stripped": [], + } + + # Strip known Claude harness marker lines before JSON parsing (#380). + # Claude Code v2.1.210+ may prepend a marker line to a subagent report + # when the output matches instruction-shaped patterns; these lines are + # not model prose and must not trigger the "leading text" rejection. + stripped, harness_markers = _strip_harness_markers(stripped) + stripped = stripped.strip() + if not stripped: + return { + "truncated": True, + "reasons": ["empty response"], + "parsed": None, + "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } parsed: Optional[dict[str, object]] = None @@ -11129,6 +11169,7 @@ def detect_truncated_agent_output( "reasons": reasons, "parsed": None, "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } # Validate required keys. @@ -11156,6 +11197,7 @@ def detect_truncated_agent_output( "reasons": reasons, "parsed": parsed, "agent_kind": agent_kind, + "harness_markers_stripped": harness_markers, } diff --git a/tests/test_map_step_runner.py b/tests/test_map_step_runner.py index 0daad86f..86beffbe 100644 --- a/tests/test_map_step_runner.py +++ b/tests/test_map_step_runner.py @@ -5018,6 +5018,106 @@ def test_map_efficient_monitor_output_not_truncated(self): assert report["truncated"] is False, report assert report["reasons"] == [] + # --- Harness marker tolerance (issue #380) --- + + def test_harness_marker_before_valid_monitor_json_not_truncated(self): + """Claude Code v2.1.210+ may prepend a harness marker line to a + subagent report; that marker must not cause the truncation gate to + reject a valid Monitor JSON payload.""" + marker = ( + "[harness: subagent output matched instruction-shaped pattern(s): " + "contains instructions]" + ) + payload = json.dumps({ + "valid": True, + "summary": "all checks passed", + "issues": [], + }) + text = marker + "\n" + payload + report = map_step_runner.detect_truncated_agent_output( + text, agent_kind="monitor" + ) + assert report["truncated"] is False, report + assert report["reasons"] == [] + assert report["harness_markers_stripped"] == [marker] + + def test_harness_marker_before_valid_actor_json_not_truncated(self): + """Harness marker tolerance applies to non-monitor agents.""" + marker = ( + "[harness: subagent output matched instruction-shaped pattern(s): " + "multi-step instructions]" + ) + payload = json.dumps({ + "files_changed": ["src/foo.py"], + "tests_run": ["pytest: 5 passed"], + "validation_notes": "ok", + "blocker": None, + }) + text = marker + "\n" + payload + report = map_step_runner.detect_truncated_agent_output( + text, agent_kind="actor" + ) + assert report["truncated"] is False, report + assert report["harness_markers_stripped"] == [marker] + + def test_arbitrary_prose_before_json_still_truncated(self): + """Non-marker leading prose is not stripped and still triggers + 'trailing or leading text around JSON object'.""" + payload = json.dumps({"valid": True, "summary": "ok", "issues": []}) + text = "Here is my analysis:\n" + payload + report = map_step_runner.detect_truncated_agent_output( + text, agent_kind="monitor" + ) + assert report["truncated"] is True + assert any( + "trailing or leading text" in r for r in report["reasons"] + ) + assert report["harness_markers_stripped"] == [] + + def test_multiple_harness_markers_all_stripped(self): + """When multiple consecutive harness marker lines appear, all are + stripped and the payload is still accepted.""" + marker1 = ( + "[harness: subagent output matched instruction-shaped pattern(s): " + "pattern A]" + ) + marker2 = ( + "[harness: subagent output matched instruction-shaped pattern(s): " + "pattern B]" + ) + payload = json.dumps({"valid": False, "summary": "issue found", "issues": ["x"]}) + text = marker1 + "\n" + marker2 + "\n" + payload + report = map_step_runner.detect_truncated_agent_output( + text, agent_kind="monitor" + ) + assert report["truncated"] is False, report + assert report["harness_markers_stripped"] == [marker1, marker2] + + def test_no_markers_harness_markers_stripped_is_empty(self): + """Clean JSON with no harness markers has an empty + ``harness_markers_stripped`` list.""" + payload = json.dumps({"valid": True, "summary": "ok", "issues": []}) + report = map_step_runner.detect_truncated_agent_output( + payload, agent_kind="monitor" + ) + assert report["truncated"] is False + assert report["harness_markers_stripped"] == [] + + def test_harness_marker_with_trailing_prose_still_truncated(self): + """A harness marker followed by arbitrary prose (not the JSON payload) + is still classified as malformed — only the marker itself is exempt.""" + marker = ( + "[harness: subagent output matched instruction-shaped pattern(s): x]" + ) + payload = json.dumps({"valid": True, "summary": "ok", "issues": []}) + text = marker + "\nHere is some prose before the JSON.\n" + payload + report = map_step_runner.detect_truncated_agent_output( + text, agent_kind="monitor" + ) + # prose between marker and JSON must still be flagged + assert report["truncated"] is True + assert report["harness_markers_stripped"] == [marker] + class TestBlueprintContractAffectedFilesDrift: """Decomposer drift catch: when every declared affected_files path is