Skip to content
Closed
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
2 changes: 1 addition & 1 deletion WORKFLOW_USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ Issue: "Add user authentication"
| `agents:optimize` | Issue | Analyze and suggest improvements |
| `agents:apply-suggestions` | Issue | Apply optimization suggestions |
| `agents:auto-pilot` | Issue | Full end-to-end automation |
| `agents:allow-change` | PR | Permission signal for `agents-guard`; automatically applied to dependency-bot PRs by `maint-auto-label-dep-prs.yml` |
| `agents:capability-check` | Issue | Check if agent can complete |
| `agents:decompose` | Issue | Break into smaller issues |
| `agents:dedup` | Issue | Check for duplicates |
Expand All @@ -166,6 +165,7 @@ Issue: "Add user authentication"
| `agent:needs-attention` | Human intervention required |
| `agents:auto-pilot-pause` | Auto-pilot paused |
| `agents:auto-pilot-failed` | Auto-pilot stopped due to errors |
| `agents:allow-change` | Permission signal for `agents-guard`; bypasses CODEOWNER approval only for automated dependency PRs from Dependabot/Renovate. Auto-applied by `maint-auto-label-dep-prs.yml`; manual application does not bypass guard enforcement. |
| `needs-human` | Escalated to human |
| `follow-up` | Created as follow-up to another issue/PR |
| `duplicate` | Potential duplicate detected |
Expand Down
62 changes: 60 additions & 2 deletions scripts/aggregate_agent_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
from pathlib import Path
from typing import Any

from src.ndjson_parser import read_ndjson_file

_DEFAULT_METRICS_DIR = "agent-metrics"
_DEFAULT_OUTPUT = "agent-metrics-summary.md"
_DEFAULT_JSON_OUTPUT = "agent-metrics-summary.json"
Expand Down Expand Up @@ -305,6 +303,66 @@ def _canonical_parse_error_detail(path: Path, error: str) -> ParseErrorDetail:
return _parse_error_detail(path, line, reason)


def read_ndjson_file(path: Path) -> tuple[list[dict[str, Any]], list[str]]:
"""Read an NDJSON file without depending on Workflows-only modules."""
try:
handle = path.open("r", encoding="utf-8")
except OSError as exc:
return [], [f"{path}: {exc}"]

entries: list[dict[str, Any]] = []
errors: list[str] = []
raw_lines_for_fallback: list[str] = []
raw_fallback_bytes = 0
raw_fallback_truncated = False
with handle:
for line_number, line in enumerate(handle, start=1):
raw = line.strip()
if not raw:
continue
if not entries and not raw_fallback_truncated:
raw_bytes = len(raw.encode("utf-8")) + 1
fallback_within_limit = (
len(raw_lines_for_fallback) < _MAX_LEGACY_JSON_FALLBACK_LINES
and raw_fallback_bytes + raw_bytes <= _MAX_LEGACY_JSON_FALLBACK_BYTES
)
if fallback_within_limit:
raw_fallback_bytes += raw_bytes
raw_lines_for_fallback.append(raw)
else:
raw_fallback_truncated = True
raw_lines_for_fallback = []
try:
parsed = json.loads(raw)
except json.JSONDecodeError as exc:
errors.append(f"{path}:{line_number}: invalid JSON ({exc.msg})")
continue
if isinstance(parsed, dict):
entries.append(parsed)
raw_lines_for_fallback = []
else:
errors.append(f"{path}:{line_number}: expected object, got {type(parsed).__name__}")

Comment on lines +318 to +345

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Catch read/decode failures during iteration.

path.open() can succeed while the later for line in handle raises UnicodeDecodeError or OSError. In the weekly metrics workflow, one malformed artifact would crash aggregation instead of being reported through parse-error details.

🛡️ Proposed fix
-    with handle:
-        for line_number, line in enumerate(handle, start=1):
-            raw = line.strip()
-            if not raw:
-                continue
-            if not entries and not raw_fallback_truncated:
-                raw_bytes = len(raw.encode("utf-8")) + 1
-                fallback_within_limit = (
-                    len(raw_lines_for_fallback) < _MAX_LEGACY_JSON_FALLBACK_LINES
-                    and raw_fallback_bytes + raw_bytes <= _MAX_LEGACY_JSON_FALLBACK_BYTES
-                )
-                if fallback_within_limit:
-                    raw_fallback_bytes += raw_bytes
-                    raw_lines_for_fallback.append(raw)
-                else:
-                    raw_fallback_truncated = True
-                    raw_lines_for_fallback = []
-            try:
-                parsed = json.loads(raw)
-            except json.JSONDecodeError as exc:
-                errors.append(f"{path}:{line_number}: invalid JSON ({exc.msg})")
-                continue
-            if isinstance(parsed, dict):
-                entries.append(parsed)
-                raw_lines_for_fallback = []
-            else:
-                errors.append(f"{path}:{line_number}: expected object, got {type(parsed).__name__}")
+    try:
+        with handle:
+            for line_number, line in enumerate(handle, start=1):
+                raw = line.strip()
+                if not raw:
+                    continue
+                # existing per-line parsing logic...
+    except (OSError, UnicodeDecodeError) as exc:
+        return entries, errors + [f"{path}: {exc}"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/aggregate_agent_metrics.py` around lines 318 - 345, The code within
the `with handle:` block fails to catch read/decode errors that can occur during
the `for line_number, line in enumerate(handle, start=1):` iteration. Wrap the
entire file iteration loop and all its logic in a try-except block that catches
both `UnicodeDecodeError` and `OSError`. When either exception occurs during
iteration, append an appropriate error message to the `errors` list (similar to
how JSON parsing errors are appended) that includes the path and the exception
details, allowing aggregation to continue and report the malformed artifact
rather than crashing.

if entries or not errors:
return entries, errors

raw_text = "\n".join(raw_lines_for_fallback)
if raw_fallback_truncated:
errors.append(f"{path}: legacy-json-fallback-buffer-limit")
return entries, errors

try:
parsed_file = json.loads(raw_text)
except json.JSONDecodeError:
return entries, errors

if isinstance(parsed_file, dict):
return [parsed_file], []
if isinstance(parsed_file, list) and all(isinstance(item, dict) for item in parsed_file):
return list(parsed_file), []
return entries, errors
Comment on lines +323 to +363

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t let an interior object disable the legacy JSON fallback.

A valid pretty-printed legacy JSON array can contain a line that is itself a JSON object, especially the last element. That makes entries non-empty, clears the fallback buffer, and Line 346 returns partial data with parse errors instead of parsing the whole legacy JSON file. Keep the bounded full-file fallback candidate until parsing is complete, and try it whenever line parsing produced errors.

🐛 Proposed fix
-            if not entries and not raw_fallback_truncated:
+            if not raw_fallback_truncated:
                 raw_bytes = len(raw.encode("utf-8")) + 1
                 fallback_within_limit = (
                     len(raw_lines_for_fallback) < _MAX_LEGACY_JSON_FALLBACK_LINES
                     and raw_fallback_bytes + raw_bytes <= _MAX_LEGACY_JSON_FALLBACK_BYTES
                 )
@@
             if isinstance(parsed, dict):
                 entries.append(parsed)
-                raw_lines_for_fallback = []
             else:
                 errors.append(f"{path}:{line_number}: expected object, got {type(parsed).__name__}")
 
-    if entries or not errors:
+    if not errors:
         return entries, errors
 
-    raw_text = "\n".join(raw_lines_for_fallback)
     if raw_fallback_truncated:
-        errors.append(f"{path}: legacy-json-fallback-buffer-limit")
+        if not entries:
+            errors.append(f"{path}: legacy-json-fallback-buffer-limit")
         return entries, errors
 
+    raw_text = "\n".join(raw_lines_for_fallback)
     try:
         parsed_file = json.loads(raw_text)
     except json.JSONDecodeError:
         return entries, errors
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/aggregate_agent_metrics.py` around lines 323 - 363, The legacy JSON
fallback mechanism is being disabled prematurely when a valid JSON object is
encountered on a single line. When the code finds a valid dict and appends it to
entries, it clears raw_lines_for_fallback, which prevents the full-file fallback
parse from working correctly for pretty-printed JSON arrays where individual
elements are valid JSON objects. Remove the line that clears
raw_lines_for_fallback when isinstance(parsed, dict) is true (currently on line
342), so the fallback buffer continues accumulating lines. This ensures that
when parsing errors occur, the complete buffered content is still available for
the full-file fallback parse attempt at line 350, allowing pretty-printed legacy
JSON files to be parsed correctly instead of returning partial data with errors.



def read_metric_ndjson_files(
files: Iterable[Path],
) -> tuple[list[dict[str, Any]], list[ParseErrorDetail]]:
Expand Down
Loading