Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .map/scripts/map_step_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -11156,6 +11197,7 @@ def detect_truncated_agent_output(
"reasons": reasons,
"parsed": parsed,
"agent_kind": agent_kind,
"harness_markers_stripped": harness_markers,
}


Expand Down
42 changes: 42 additions & 0 deletions src/mapify_cli/templates/map/scripts/map_step_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -11156,6 +11197,7 @@ def detect_truncated_agent_output(
"reasons": reasons,
"parsed": parsed,
"agent_kind": agent_kind,
"harness_markers_stripped": harness_markers,
}


Expand Down
42 changes: 42 additions & 0 deletions src/mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -11156,6 +11197,7 @@ def detect_truncated_agent_output(
"reasons": reasons,
"parsed": parsed,
"agent_kind": agent_kind,
"harness_markers_stripped": harness_markers,
}


Expand Down
100 changes: 100 additions & 0 deletions tests/test_map_step_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down