Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
367 commits
Select commit Hold shift + click to select a range
bf8eeb9
CI: retrigger
jbachorik Sep 18, 2026
a66a5dd
CI: retrigger
jbachorik Sep 18, 2026
f20ffb2
CI: retrigger
jbachorik Sep 18, 2026
e77f0ce
CI: retrigger
jbachorik Sep 18, 2026
3328341
CI: retrigger
jbachorik Sep 18, 2026
eedc031
CI: retrigger
jbachorik Sep 18, 2026
02eedf9
CI: retrigger
jbachorik Sep 18, 2026
3471392
CI: retrigger
jbachorik Sep 18, 2026
af670d1
CI: retrigger
jbachorik Sep 18, 2026
64580c0
CI: retrigger
jbachorik Sep 18, 2026
bce81d6
CI: retrigger
jbachorik Sep 18, 2026
1ba007d
CI: retrigger
jbachorik Sep 18, 2026
df5b6ee
CI: retrigger
jbachorik Sep 18, 2026
11e77fd
CI: retrigger
jbachorik Sep 18, 2026
25a5bb0
CI: retrigger
jbachorik Sep 18, 2026
e7dc3d8
CI: retrigger
jbachorik Sep 18, 2026
f207a5b
CI: retrigger
jbachorik Sep 18, 2026
b95b7c8
CI: retrigger
jbachorik Sep 18, 2026
79a7941
CI: retrigger
jbachorik Sep 18, 2026
fe4a793
CI: retrigger
jbachorik Sep 18, 2026
7e38b0f
CI: retrigger
jbachorik Sep 18, 2026
48fcf0f
CI: retrigger
jbachorik Sep 18, 2026
a5fc7a9
CI: retrigger
jbachorik Sep 18, 2026
f728b70
CI: retrigger
jbachorik Sep 18, 2026
64501fc
CI: retrigger
jbachorik Sep 18, 2026
055551a
CI: retrigger
jbachorik Sep 18, 2026
21f2d99
CI: retrigger
jbachorik Sep 18, 2026
6009767
CI: retrigger
jbachorik Sep 18, 2026
e6bedba
CI: retrigger
jbachorik Sep 18, 2026
6debeac
CI: retrigger
jbachorik Sep 18, 2026
7684f8f
CI: retrigger
jbachorik Sep 18, 2026
a7a0496
CI: retrigger
jbachorik Sep 18, 2026
99460cb
CI: retrigger
jbachorik Sep 18, 2026
de15a13
CI: retrigger
jbachorik Sep 18, 2026
f86bad2
CI: retrigger
jbachorik Sep 18, 2026
8d8fd27
CI: retrigger
jbachorik Sep 18, 2026
a221186
CI: retrigger
jbachorik Sep 18, 2026
0ecdb4a
CI: retrigger
jbachorik Sep 18, 2026
9c9ad1d
CI: retrigger
jbachorik Sep 18, 2026
768d091
CI: retrigger
jbachorik Sep 18, 2026
a16f323
CI: retrigger
jbachorik Sep 18, 2026
bb13ca0
CI: retrigger
jbachorik Sep 18, 2026
c7a7694
CI: retrigger
jbachorik Sep 18, 2026
509ede1
CI: retrigger
jbachorik Sep 18, 2026
5100855
CI: retrigger
jbachorik Sep 18, 2026
e4ea782
CI: retrigger
jbachorik Sep 18, 2026
2a6e81d
CI: retrigger
jbachorik Sep 18, 2026
aa1942a
CI: retrigger
jbachorik Sep 18, 2026
8fc6675
CI: retrigger
jbachorik Sep 18, 2026
1a461a8
CI: retrigger
jbachorik Sep 18, 2026
a620a1c
CI: retrigger
jbachorik Sep 18, 2026
089e4bd
CI: retrigger
jbachorik Sep 18, 2026
83c1d0f
CI: retrigger
jbachorik Sep 18, 2026
220f982
CI: retrigger
jbachorik Sep 18, 2026
6139b4f
CI: retrigger
jbachorik Sep 18, 2026
c48f682
CI: retrigger
jbachorik Sep 18, 2026
77d3e93
CI: retrigger
jbachorik Sep 18, 2026
6b0f45b
CI: retrigger
jbachorik Sep 18, 2026
55d28aa
CI: retrigger
jbachorik Sep 18, 2026
4d131ad
CI: retrigger
jbachorik Sep 18, 2026
eed3431
CI: retrigger
jbachorik Sep 18, 2026
5534daa
CI: retrigger
jbachorik Sep 18, 2026
724786f
CI: retrigger
jbachorik Sep 18, 2026
6048f39
CI: retrigger
jbachorik Sep 18, 2026
ca0c4bb
CI: retrigger
jbachorik Sep 18, 2026
0353934
CI: retrigger
jbachorik Sep 18, 2026
cee730a
CI: retrigger
jbachorik Sep 18, 2026
9a81844
CI: retrigger
jbachorik Sep 18, 2026
a5e0315
CI: retrigger
jbachorik Sep 18, 2026
394ecdc
CI: retrigger
jbachorik Sep 18, 2026
b05af46
CI: retrigger
jbachorik Sep 18, 2026
2938505
CI: retrigger
jbachorik Sep 18, 2026
bf6280d
CI: retrigger
jbachorik Sep 18, 2026
645bf53
CI: retrigger
jbachorik Sep 18, 2026
8900fc5
CI: retrigger
jbachorik Sep 18, 2026
c7013e2
CI: retrigger
jbachorik Sep 18, 2026
18e042e
CI: retrigger
jbachorik Sep 18, 2026
2610640
CI: retrigger
jbachorik Sep 18, 2026
5a65a3e
CI: retrigger
jbachorik Sep 18, 2026
e9336dd
CI: retrigger
jbachorik Sep 18, 2026
64fe356
CI: retrigger
jbachorik Sep 18, 2026
780ff3c
CI: retrigger
jbachorik Sep 18, 2026
ef3db4c
CI: retrigger
jbachorik Sep 18, 2026
94435c1
CI: retrigger
jbachorik Sep 18, 2026
29007e7
CI: retrigger
jbachorik Sep 18, 2026
fcb1124
CI: retrigger
jbachorik Sep 18, 2026
5a302b1
CI: retrigger
jbachorik Sep 18, 2026
e68bc08
CI: retrigger
jbachorik Sep 18, 2026
fb84f5c
CI: retrigger
jbachorik Sep 18, 2026
1964ae0
CI: retrigger
jbachorik Sep 18, 2026
8d193c9
CI: retrigger
jbachorik Sep 18, 2026
49fdfc8
CI: retrigger
jbachorik Sep 18, 2026
ed2a33c
CI: retrigger
jbachorik Sep 18, 2026
e0e6be7
CI: retrigger
jbachorik Sep 18, 2026
ad06863
CI: retrigger
jbachorik Sep 18, 2026
3bd6439
CI: retrigger
jbachorik Sep 18, 2026
220483f
CI: retrigger
jbachorik Sep 18, 2026
95497ca
CI: retrigger
jbachorik Sep 18, 2026
61560e2
CI: retrigger
jbachorik Sep 18, 2026
95907ac
CI: retrigger
jbachorik Sep 18, 2026
886fa49
CI: retrigger
jbachorik Sep 18, 2026
7b86c50
CI: retrigger
jbachorik Sep 18, 2026
c925ba4
CI: retrigger
jbachorik Sep 18, 2026
e2dfe3b
CI: retrigger
jbachorik Sep 18, 2026
39ab5d3
CI: retrigger
jbachorik Sep 18, 2026
e94c679
CI: retrigger
jbachorik Sep 18, 2026
64b4adc
CI: retrigger
jbachorik Sep 18, 2026
89fdd02
CI: retrigger
jbachorik Sep 18, 2026
64ab9bd
CI: retrigger
jbachorik Sep 18, 2026
9267d80
CI: retrigger
jbachorik Sep 18, 2026
6bb3470
CI: retrigger
jbachorik Sep 18, 2026
dd510f7
CI: retrigger
jbachorik Sep 18, 2026
16d49c1
CI: retrigger
jbachorik Sep 18, 2026
68c7714
CI: retrigger
jbachorik Sep 18, 2026
a215f28
CI: retrigger
jbachorik Sep 18, 2026
1f151da
CI: retrigger
jbachorik Sep 18, 2026
287c7be
CI: retrigger
jbachorik Sep 18, 2026
57a7ab8
CI: retrigger
jbachorik Sep 18, 2026
a3bf390
CI: retrigger
jbachorik Sep 18, 2026
92d666b
CI: retrigger
jbachorik Sep 18, 2026
235b166
CI: retrigger
jbachorik Sep 18, 2026
773f3f4
CI: retrigger
jbachorik Sep 18, 2026
7819e05
CI: retrigger
jbachorik Sep 18, 2026
b82a3b2
CI: retrigger
jbachorik Sep 18, 2026
82b845e
CI: retrigger
jbachorik Sep 18, 2026
01bc201
CI: retrigger
jbachorik Sep 18, 2026
25d1f5d
CI: retrigger
jbachorik Sep 18, 2026
9efc943
CI: retrigger
jbachorik Sep 18, 2026
f51efd6
CI: retrigger
jbachorik Sep 18, 2026
b581a26
CI: retrigger
jbachorik Sep 18, 2026
fcaebc0
CI: retrigger
jbachorik Sep 18, 2026
04d9d44
CI: retrigger
jbachorik Sep 18, 2026
1e52d33
CI: retrigger
jbachorik Sep 18, 2026
7dd7365
CI: retrigger
jbachorik Sep 18, 2026
6675f96
CI: retrigger
jbachorik Sep 18, 2026
6144b64
CI: retrigger
jbachorik Sep 18, 2026
3c36de8
CI: retrigger
jbachorik Sep 18, 2026
0258137
CI: retrigger
jbachorik Sep 18, 2026
c7eded5
CI: retrigger
jbachorik Sep 18, 2026
f744919
CI: retrigger
jbachorik Sep 18, 2026
bb0d3cc
CI: retrigger
jbachorik Sep 18, 2026
4ae09f8
CI: retrigger
jbachorik Sep 18, 2026
1edf790
CI: retrigger
jbachorik Sep 18, 2026
73947be
CI: retrigger
jbachorik Sep 18, 2026
76690dd
CI: retrigger
jbachorik Sep 18, 2026
de4c64a
CI: retrigger
jbachorik Sep 18, 2026
086f7d9
CI: retrigger
jbachorik Sep 18, 2026
f363cd8
CI: retrigger
jbachorik Sep 18, 2026
dab1c84
CI: retrigger
jbachorik Sep 18, 2026
2f8d9d5
CI: retrigger
jbachorik Sep 18, 2026
f3751dd
CI: retrigger
jbachorik Sep 18, 2026
a9aad53
CI: retrigger
jbachorik Sep 18, 2026
4c26030
CI: retrigger
jbachorik Sep 18, 2026
446ad75
CI: retrigger
jbachorik Sep 18, 2026
29f8766
CI: retrigger
jbachorik Sep 18, 2026
aea8546
CI: retrigger
jbachorik Sep 18, 2026
842a0e7
CI: retrigger
jbachorik Sep 18, 2026
239496f
CI: retrigger
jbachorik Sep 18, 2026
4d65d4f
CI: retrigger
jbachorik Sep 18, 2026
dd0de67
CI: retrigger
jbachorik Sep 18, 2026
d8c948b
CI: retrigger
jbachorik Sep 18, 2026
ef108a8
CI: retrigger
jbachorik Sep 18, 2026
3269c10
CI: retrigger
jbachorik Sep 18, 2026
24c188c
CI: retrigger
jbachorik Sep 18, 2026
b061949
CI: retrigger
jbachorik Sep 18, 2026
b5d4a43
CI: retrigger
jbachorik Sep 18, 2026
d623d0a
CI: retrigger
jbachorik Sep 18, 2026
947fd9b
CI: retrigger
jbachorik Sep 18, 2026
32f483e
CI: retrigger
jbachorik Sep 18, 2026
4169e83
CI: retrigger
jbachorik Sep 18, 2026
50089b8
CI: retrigger
jbachorik Sep 18, 2026
6e1e468
CI: retrigger
jbachorik Sep 18, 2026
38aec1b
CI: retrigger
jbachorik Sep 18, 2026
c257768
CI: retrigger
jbachorik Sep 18, 2026
fa33521
CI: retrigger
jbachorik Sep 18, 2026
4307176
CI: retrigger
jbachorik Sep 18, 2026
cbb367d
CI: retrigger
jbachorik Sep 18, 2026
4ac46ed
CI: retrigger
jbachorik Sep 18, 2026
efb1bbb
CI: retrigger
jbachorik Sep 18, 2026
97c229b
CI: retrigger
jbachorik Sep 18, 2026
7fa5822
CI: retrigger
jbachorik Sep 18, 2026
18c94db
CI: retrigger
jbachorik Sep 18, 2026
6e44ada
CI: retrigger
jbachorik Sep 18, 2026
ee95e33
CI: retrigger
jbachorik Sep 18, 2026
37d7c64
CI: retrigger
jbachorik Sep 18, 2026
f5f0c11
CI: retrigger
jbachorik Sep 18, 2026
19b6eb2
CI: retrigger
jbachorik Sep 18, 2026
7d474d2
CI: retrigger
jbachorik Sep 18, 2026
c1d1636
CI: retrigger
jbachorik Sep 18, 2026
398f168
CI: retrigger
jbachorik Sep 18, 2026
516ed87
CI: retrigger
jbachorik Sep 18, 2026
e7e033b
CI: retrigger
jbachorik Sep 18, 2026
87f58c5
CI: retrigger
jbachorik Sep 18, 2026
d43581e
CI: retrigger
jbachorik Sep 18, 2026
b478f54
CI: retrigger
jbachorik Sep 18, 2026
ac5f630
CI: retrigger
jbachorik Sep 18, 2026
d032fec
CI: retrigger
jbachorik Sep 18, 2026
a080ed7
CI: retrigger
jbachorik Sep 18, 2026
ce59fa6
CI: retrigger
jbachorik Sep 18, 2026
d5711d2
CI: retrigger
jbachorik Sep 18, 2026
a0c22c7
CI: retrigger
jbachorik Sep 18, 2026
259240e
CI: retrigger
jbachorik Sep 18, 2026
c923b37
CI: retrigger
jbachorik Sep 18, 2026
68301f0
CI: retrigger
jbachorik Sep 18, 2026
7677fef
CI: retrigger
jbachorik Sep 18, 2026
1f92f1a
CI: retrigger
jbachorik Sep 18, 2026
f94ef9d
CI: retrigger
jbachorik Sep 18, 2026
60b5b4c
CI: retrigger
jbachorik Sep 18, 2026
cdf6271
CI: retrigger
jbachorik Sep 18, 2026
ee9456b
CI: retrigger
jbachorik Sep 18, 2026
9bda1fc
CI: retrigger
jbachorik Sep 18, 2026
605a9dd
CI: retrigger
jbachorik Sep 18, 2026
11bc183
CI: retrigger
jbachorik Sep 18, 2026
edee7d9
CI: retrigger
jbachorik Sep 18, 2026
e63ec9d
CI: retrigger
jbachorik Sep 18, 2026
02c1173
CI: retrigger
jbachorik Sep 18, 2026
9215495
CI: retrigger
jbachorik Sep 18, 2026
833752a
CI: retrigger
jbachorik Sep 18, 2026
bf111b4
CI: retrigger
jbachorik Sep 18, 2026
e92c6b1
CI: retrigger
jbachorik Sep 18, 2026
1d8b050
CI: retrigger
jbachorik Sep 18, 2026
d16c7f7
CI: retrigger
jbachorik Sep 18, 2026
d8f533b
CI: retrigger
jbachorik Sep 18, 2026
3d43f65
CI: retrigger
jbachorik Sep 18, 2026
3d74416
CI: retrigger
jbachorik Sep 18, 2026
b25ba15
CI: retrigger
jbachorik Sep 18, 2026
5fa320f
CI: retrigger
jbachorik Sep 18, 2026
7640059
CI: retrigger
jbachorik Sep 18, 2026
69f89f6
CI: retrigger
jbachorik Sep 18, 2026
b2bf10d
CI: retrigger
jbachorik Sep 18, 2026
50b136f
CI: retrigger
jbachorik Sep 18, 2026
a1384d1
CI: retrigger
jbachorik Sep 18, 2026
c0fca46
CI: retrigger
jbachorik Sep 18, 2026
5f8a519
CI: retrigger
jbachorik Sep 18, 2026
6eb69ac
CI: retrigger
jbachorik Sep 18, 2026
2cda7fa
CI: retrigger
jbachorik Sep 18, 2026
f253975
CI: retrigger
jbachorik Sep 18, 2026
df11592
CI: retrigger
jbachorik Sep 18, 2026
fe31150
CI: retrigger
jbachorik Sep 18, 2026
91df7aa
CI: retrigger
jbachorik Sep 18, 2026
09253b8
CI: retrigger
jbachorik Sep 18, 2026
74af37b
CI: retrigger
jbachorik Sep 18, 2026
a82a926
CI: retrigger
jbachorik Sep 18, 2026
92ba0dc
CI: retrigger
jbachorik Sep 18, 2026
9aa6e6a
CI: retrigger
jbachorik Sep 18, 2026
d6220a6
CI: retrigger
jbachorik Sep 18, 2026
e5eb625
CI: retrigger
jbachorik Sep 18, 2026
7bb42fd
CI: retrigger
jbachorik Sep 18, 2026
46d1ab9
CI: retrigger
jbachorik Sep 18, 2026
447759c
Add reference-chain repro, sweep, and reporting tooling
jbachorik Sep 17, 2026
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
169 changes: 160 additions & 9 deletions .claude/commands/build-and-summarize
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,164 @@ else
echo "✖ Gradle failed with status $status. Full log at: $LOG"
fi

# Hand over to your logs analyst agent — keep the main session output tiny.
# Generate the summary artifacts deterministically — no LLM, no claude spawn.
# (The old tail spawned a headless `claude` per build; managed settings pin that
# to sonnet-5 via the AI gateway, billing real money for every log parse.)
echo
echo "Delegating to gradle-logs-analyst agent…"
# If your CLI supports non-streaming, set it here to avoid verbose output.
# Example (uncomment if supported): export CLAUDE_NO_STREAM=1

# Sub-agents would inherit the full parent env, including CLAUDECODE, which
# apparently causes problems with some versions of Claude (e.g. 4.6).
unset CLAUDECODE
claude "Act as the gradle-logs-analyst agent to parse the build log at: $LOG. Generate the required Gradle summary artifacts as specified in the gradle-logs-analyst agent definition."
echo "Generating gradle summary…"
python3 - "$LOG" <<'PYEOF' || echo "⚠ gradle summary generation failed — full log at: $LOG"
import json, os, re, sys

log_path = sys.argv[1] if len(sys.argv) > 1 else ""
OUT_DIR = os.path.join("build", "reports", "claude")
MD = os.path.join(OUT_DIR, "gradle-summary.md")
JS = os.path.join(OUT_DIR, "gradle-summary.json")

status, total_time, cur_task = "UNKNOWN", None, None
failed_tasks, headlines, dep_issues, test_failures = [], [], [], []
warnings, seen_warnings = [], set()
tests = {"total": 0, "failed": 0, "skipped": 0, "modules": {}}

RE_STATUS = re.compile(r"^BUILD (SUCCESSFUL|FAILED)(?: in (.+?))?\s*$")
RE_TASK = re.compile(r"^> Task (\S+)")
RE_GSUM = re.compile(r"(\d+) tests? (successful|failed|skipped)")
RE_COMP = re.compile(r"(\d+) tests? completed(?:, (\d+) failed)?(?:, (\d+) skipped)?")
RE_GTEST_P = re.compile(r"\[ PASSED \] (\d+) tests?")
RE_GTEST_F = re.compile(r"\[ FAILED \] (\d+) tests?")
RE_DEP = re.compile(r"Could not (?:resolve|find|get)|timed out|status code: 40[13]|artifact .* not found", re.I)
RE_WARN = re.compile(r"\bw: |warning:|deprecat", re.I)

def mod(name):
return tests["modules"].setdefault(name, {"total": 0, "failed": 0, "skipped": 0})

def add_counts(name, ok, failed, skipped):
m = mod(name)
m["total"] += ok + failed + skipped
m["failed"] += failed
m["skipped"] += skipped
tests["total"] += ok + failed + skipped
tests["failed"] += failed
tests["skipped"] += skipped

if not log_path or not os.path.isfile(log_path) or os.path.getsize(log_path) == 0:
why = f"log not found: {log_path!r}" if not log_path or not os.path.isfile(log_path) else "log is empty"
os.makedirs(OUT_DIR, exist_ok=True)
open(MD, "w").write(f"# Gradle Summary\n\n- Status: UNKNOWN ({why})\n\nFull log unavailable or empty.\n")
json.dump({"status": "UNKNOWN", "totalTime": None, "failedTasks": [], "warnings": [],
"tests": tests, "slowTasks": [], "depIssues": [why], "actions": []}, open(JS, "w"), indent=1)
print(f"Gradle log unusable ({why}); wrote {MD} and {JS}")
sys.exit(0)

grab_headline = False
with open(log_path, encoding="utf-8", errors="replace") as f:
for line in f:
line = line.rstrip("\n")
m = RE_STATUS.match(line)
if m:
status = m.group(1)
total_time = m.group(2)
continue
m = RE_TASK.match(line)
if m:
cur_task = m.group(1)
if line.endswith("FAILED") and m.group(1) not in failed_tasks:
failed_tasks.append(m.group(1))
continue
if "FAILURE: Build failed with an exception" in line:
grab_headline = True
continue
if grab_headline and line.strip().startswith("* What went wrong"):
headlines.append("")
continue
if grab_headline:
s = line.strip()
if s.startswith("*") or not s:
if headlines and headlines[-1]:
grab_headline = False
continue
if len(headlines[-1]) < 200:
headlines[-1] += (" " if headlines[-1] else "") + s
continue
m = RE_COMP.search(line)
if m:
total = int(m.group(1))
failed = int(m.group(2) or 0)
skipped = int(m.group(3) or 0)
add_counts(cur_task or "(unknown)", total - failed - skipped, failed, skipped)
continue
m = RE_GSUM.search(line)
if m:
n, verb = int(m.group(1)), m.group(2)
mm = mod(cur_task or "(unknown)")
mm["total"] += n
tests["total"] += n
if verb == "failed":
mm["failed"] += n
tests["failed"] += n
elif verb == "skipped":
mm["skipped"] += n
tests["skipped"] += n
continue
m = RE_GTEST_P.search(line)
if m:
add_counts(cur_task or "(gtest)", int(m.group(1)), 0, 0)
continue
m = RE_GTEST_F.search(line)
if m:
add_counts(cur_task or "(gtest)", 0, int(m.group(1)), 0)
continue
if re.search(r" > \S+(?:\.\S+)+.* FAILED$", line) or re.search(r"\[ FAILED \] (\S+)", line):
test_failures.append(line.strip())
continue
if RE_DEP.search(line) and len(dep_issues) < 20:
if line not in dep_issues:
dep_issues.append(line.strip())
continue
wm = RE_WARN.search(line)
if wm and len(warnings) < 40:
key = line.strip()[:160]
if key not in seen_warnings:
seen_warnings.add(key)
warnings.append(key)

failed = [t + (" — " + h if h else "") for t, h in zip(failed_tasks, headlines + [""] * len(failed_tasks))]
data = {"status": status, "totalTime": total_time, "failedTasks": failed, "warnings": warnings,
"tests": tests, "slowTasks": [], "depIssues": dep_issues, "actions": []}

os.makedirs(OUT_DIR, exist_ok=True)
with open(JS, "w") as f:
json.dump(data, f, indent=1)

L = ["# Gradle Summary\n", f"- Status: {status}", f"- Total time: {total_time or 'UNKNOWN'}",
f"- Log: {log_path}\n"]
if failed:
L.append("## Failing Tasks")
L += [f"- {t}" for t in failed]
L.append("")
if headlines:
L.append("## Primary Failure")
L += [f"```text\n{h}\n```" for h in headlines[:3] if h]
L.append("")
if tests["total"]:
L.append("## Tests")
L.append(f"- Total: {tests['total']}, failed: {tests['failed']}, skipped: {tests['skipped']}")
for name, m in tests["modules"].items():
L.append(f" - {name}: {m['total']} total, {m['failed']} failed, {m['skipped']} skipped")
if test_failures:
L.append("- Top failing tests:")
L += [f" - {t}" for t in test_failures[:10]]
L.append("")
if warnings:
L.append("## Warnings")
L += [f"- {w}" for w in warnings[:20]]
L.append("")
if dep_issues:
L.append("## Dependency / Network Issues")
L += [f"- {d}" for d in dep_issues[:10]]
L.append("")
L.append("- Per-task durations are not present in plain `-i` console logs; slowTasks omitted.")
open(MD, "w").write("\n".join(L) + "\n")

print(f"{status} (time: {total_time or 'n/a'}); {len(failed)} failing task(s), {tests['failed']} test failure(s)")
print(f"Wrote {MD} and {JS}")
PYEOF
15 changes: 14 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,17 @@ doc/temp/
# CLAUDE.md is auto-generated from AGENTS.md bootstrap instructions
CLAUDE.md

.sphinx
# OS/editor cruft
.DS_Store

# AI review/agent tooling scratch state
.sphinx/
.skill-builder-temp/
.claude/scheduled_tasks.lock

# Python bytecode cache
__pycache__/
*.pyc

# AI investigation scratch state (never committed)
.investigations/
39 changes: 39 additions & 0 deletions ddprof-stresstest/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,45 @@ tasks.register<Jar>("chaosJar") {
}
}

// --- reference-chains repro app ---------------------------------------------
// Plain, dependency-free demo app for manually reproducing the reference-chains
// feature: launched directly with `-agentpath:libjavaProfiler.so=start,...`
// (no dd-trace-java agent, no dynamic attach), so it deliberately has zero
// compile/runtime dependency on ddprof-lib or dd-trace - the profiler is
// entirely out-of-process from this app's own point of view.

sourceSets {
create("repro")
}

dependencies {
// ddprof-lib's Java API only - not the native library itself (that's the one
// already loaded in-process via -agentpath). Used solely to call
// JavaProfiler.getInstance().dump(...) periodically: Profiler::dump()
// (profiler.cpp) is the only code path that drains ReferenceChainTracker's
// pending chain-event queue and actually writes datadog.ReferenceChain into
// the JFR file - nothing does this automatically on a timer, and in
// production it's dd-trace-java's own recording-chunk rotation that calls
// it. Without this dependency+call, chain events get built and enqueued
// (visible in TEST_LOG output) but are silently dropped when the process
// exits, and the JFR file never shows a single datadog.ReferenceChain event.
"reproImplementation"(project(mapOf("path" to ":ddprof-lib", "configuration" to "debug")))
}

tasks.register<Jar>("reproJar") {
group = "build"
description = "Demo app that leaks memory in a controlled way, for manually reproducing reference-chains via -agentpath"
archiveFileName.set("refchains_repro.jar")
from(sourceSets["repro"].output)
from({
configurations["reproRuntimeClasspath"].map { if (it.isDirectory) it else zipTree(it) }
})
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest {
attributes("Main-Class" to "com.datadoghq.profiler.repro.ReferenceChainLeakDemo")
}
}

tasks.register<Exec>("runStressTests") {
dependsOn(tasks.named("jmhJar"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ private static Antagonist create(String name) {
return new WeakRefWaveAntagonist();
case "dump-storm":
return new DumpStormAntagonist();
case "reference-chain-leak":
return new ReferenceChainLeakAntagonist();
case "reapply-context-value":
return new ReapplyContextValueAntagonist();
// Deferred: dlopen-churn (needs per-arch dummy .so built in CI prep).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2026, Datadog, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package com.datadoghq.profiler.chaos;

import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Grows an ever-referenced, never-evicted cache to give the reference-chains
* feature (LivenessTracker/ReferenceChainTracker) a real, steadily growing
* population to detect and walk. Growth is capped, not open-ended: once
* {@link #CAP_ENTRIES} is reached the cache stops growing and the antagonist
* just holds it there for the rest of the run, so a long chaos duration
* (hours) still produces the same bounded leak instead of eventually OOMing
* the JVM out from under the other antagonists sharing its heap.
*
* <p>Same object shape as {@code LeakingCacheScenario.CachedPayload}
* (ddprof-test's reference-chain integration test) — plain long fields
* rather than a nested array, so a single instance clears the allocation
* sampler's real size floor without a second, competing heap allocation per
* entry.
*/
public final class ReferenceChainLeakAntagonist implements Antagonist {

// ~64MiB of CachedEntry payloads at steady state (entries are a little
// over 300 bytes with object header/field overhead) — enough for the
// liveness tracker's population-growth trend detection to have a real,
// sustained trend to lock onto, without meaningfully competing with the
// other antagonists' own memory budget for the run's whole duration.
private static final int CAP_ENTRIES = 200_000;
private static final int BATCH_SIZE = 200;
private static final long BATCH_INTERVAL_MS = 50L;

private final Map<String, CachedEntry> cache = new ConcurrentHashMap<>();
private volatile boolean running;
private Thread growthDriver;
private long nextKey;

@Override
public String name() {
return "reference-chain-leak";
}

@Override
public void start() {
running = true;
growthDriver = new Thread(this::growthLoop, "chaos-refchain-leak");
growthDriver.setDaemon(true);
growthDriver.start();
}

@Override
public void stopGracefully(Duration timeout) {
running = false;
try {
growthDriver.join(timeout.toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

private void growthLoop() {
while (running && cache.size() < CAP_ENTRIES) {
for (int i = 0; i < BATCH_SIZE && cache.size() < CAP_ENTRIES; i++) {
String key = "refchain-leak-" + (nextKey++);
cache.put(key, new CachedEntry(key));
MemoryGovernor.pace();
}
try {
Thread.sleep(BATCH_INTERVAL_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// Cap reached: keep the reference alive (do nothing) for the rest of
// the run instead of exiting the thread, so `cache` stays reachable
// via this antagonist's own field for as long as the harness runs.
while (running) {
try {
Thread.sleep(1_000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}

static final class CachedEntry {
final String key;
long p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15;
long p16, p17, p18, p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30, p31;

CachedEntry(String key) {
this.key = key;
}
}
}
Loading
Loading