From 8ba96e6771499c555a06769cd1748fd98393af34 Mon Sep 17 00:00:00 2001 From: Andrew Wint Date: Fri, 10 Jul 2026 09:17:38 -0400 Subject: [PATCH] fix(ledger): close-out count derives from ledger's own lines, never below them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A real integration test surfaced a visible contradiction: the Stop close-out printed "lanes recorded this session: 0" directly above 2 lane LINES. Cause — the count read ONLY .agents/runs/lane_spawns.jsonl (the separate record_lane_spawn.py sidecar), while the lines came from ledger.py's own PostToolUse handler; when the sibling hasn't fired the count reads 0 despite ledger.py having just written N lines. Fix: lane_count() now returns max(own lane lines in ledger.md, sibling lane_spawns.jsonl), so the count is never lower than the lines shown and a sibling-only observation isn't lost. A shared _LANE_MARK couples the writer (lane_line) and reader (_own_lane_lines) so they can't drift. New test group G runs ONLY ledger.py through N spawns + a Stop (no lane_spawns.jsonl seeded) and asserts the count matches the N lines — the coupling can't hide. Robustness/consistency fix; in a fully-wired session both hooks co-fire and the count was already right. Independent cold-read review: pass, no blockers. Co-Authored-By: Claude Opus 4.8 --- .claude/skills/baton/hooks/ledger.py | 32 ++++++++++++++++++++--- .claude/skills/baton/hooks/ledger_test.py | 29 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/.claude/skills/baton/hooks/ledger.py b/.claude/skills/baton/hooks/ledger.py index 767ac18..e25dcf3 100644 --- a/.claude/skills/baton/hooks/ledger.py +++ b/.claude/skills/baton/hooks/ledger.py @@ -115,9 +115,15 @@ def lane_from_event(event): return {"subagent_type": subagent_type, "task_id": task_id} +# The distinctive marker of a lane line — shared by lane_line() (writer) and lane_count() (reader) so the +# close-out count is derived from the SAME lines it displays and cannot drift from them. No other line the +# ledger writes (header, close-out, verdicts) contains this phrase. +_LANE_MARK = "lane spawned:" + + def lane_line(lane, ts): tid = f" · task `{lane['task_id']}`" if lane.get("task_id") else "" - return f"- {ts} · lane spawned: `{lane['subagent_type']}`{tid}" + return f"- {ts} · {_LANE_MARK} `{lane['subagent_type']}`{tid}" # ---- Stop: idempotent closeout ------------------------------------------------------------------ @@ -144,8 +150,28 @@ def sensitive_triaged(path=TRIAGED_SEAMS_PATH): return classes -def lane_count(path=LANE_SPAWNS_PATH): - """How many real lanes the machine sidecar recorded this session (authoritative spawn count).""" +def _own_lane_lines(ledger_path=LEDGER): + """How many lane lines ledger.py has written to its OWN trail this session — the count that MUST match + the lines shown directly above the close-out (they are literally those lines).""" + try: + return sum(1 for line in open(ledger_path).read().splitlines() if _LANE_MARK in line) + except OSError: + return 0 + + +def lane_count(ledger_path=LEDGER, spawns_path=LANE_SPAWNS_PATH): + """Lanes recorded this session, for the close-out. Reconciles TWO observers so the number is never + LOWER than the lane LINES printed right above it (the reported bug direction): + - ledger.py's OWN recorded lane lines (what the reader sees), and + - the forge-proof sibling ledger `lane_spawns.jsonl` (record_lane_spawn.py), which may hold spawns + seen when ledger's PostToolUse handler did not fire. + Returns the MAX. This fixes the real-test bug where reading ONLY the sibling ledger printed + "lanes recorded: 0" while ledger.py had just written N lane lines (the sibling had not fired): the count + is now never lower than the lines present, and a sibling-only observation is still not lost.""" + return max(_own_lane_lines(ledger_path), _sibling_spawns(spawns_path)) + + +def _sibling_spawns(path=LANE_SPAWNS_PATH): try: return sum(1 for line in open(path).read().splitlines() if line.strip()) except OSError: diff --git a/.claude/skills/baton/hooks/ledger_test.py b/.claude/skills/baton/hooks/ledger_test.py index 9dbd14b..baa18ef 100644 --- a/.claude/skills/baton/hooks/ledger_test.py +++ b/.claude/skills/baton/hooks/ledger_test.py @@ -141,6 +141,35 @@ def read_ledger(): check("read_event normalization is dict-or-empty (non-dict -> {})", all(isinstance(x, dict) for x in [{}]), True) +print("G. close-out count is derived from ledger's OWN lines — never contradicts them") +with tempfile.TemporaryDirectory() as t: + # Run ONLY ledger.py (record_lane_spawn.py NOT involved), so lane_spawns.jsonl never exists and the + # count cannot borrow from the sibling ledger — exactly the real-integration-test scenario that + # printed "lanes recorded: 0" above N lane lines. Don't pre-seed lane_spawns.jsonl: the coupling + # must not be able to hide. + lanes = ["triage", "implementer", "code-reviewer"] + for lane in lanes: + subprocess.run([sys.executable, LEDGER_PY], + input=json.dumps({"hook_event_name": "PostToolUse", "tool_name": "Task", + "tool_input": {"subagent_type": lane}}), + capture_output=True, text=True, cwd=t) + subprocess.run([sys.executable, LEDGER_PY], input='{"hook_event_name":"Stop"}', + capture_output=True, text=True, cwd=t) + body = open(os.path.join(t, ".agents", "runs", "ledger.md")).read() + check("only ledger.py ran -> no sibling lane_spawns.jsonl", + os.path.exists(os.path.join(t, ".agents", "runs", "lane_spawns.jsonl")), False) + check("3 lane lines written", body.count("lane spawned:"), 3) + check("close-out count MATCHES the 3 lines (no 0-vs-N contradiction)", + "lanes recorded this session: 3" in body, True) +with tempfile.TemporaryDirectory() as t: + # Reconciliation: a sibling-only observation (ledger's PostToolUse didn't fire, but record_lane_spawn + # did) is still counted — never lower than either source. + os.makedirs(os.path.join(t, ".agents", "runs")) + seed_jsonl(os.path.join(t, ".agents", "runs", "lane_spawns.jsonl"), + [{"subagent_type": "s1"}, {"subagent_type": "s2"}]) + os.chdir(t) + check("sibling-only spawns still counted (max of both sources)", L.lane_count(), 2) + if failures: print(f"\nLEDGER SELFTEST FAILED ({failures})") sys.exit(1)