Skip to content

feat(devtools): add bead-landing-check, empty-diff evidence verification - #3424

Closed
Sinity wants to merge 4 commits into
masterfrom
feature/devtools/bead-landing-check
Closed

feat(devtools): add bead-landing-check, empty-diff evidence verification#3424
Sinity wants to merge 4 commits into
masterfrom
feature/devtools/bead-landing-check

Conversation

@Sinity

@Sinity Sinity commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

Adds devtools workspace bead-landing-check, a tool intended to verify whether a bead's cited implementation evidence already landed on master, before a human or dispatched agent burns a full investigation cycle on it.

FINAL VERDICT (2026-07-31): do not merge. Recommend closing this PR unmerged. Five independent human reviewers labelled all 190 beads flagged by the first sweep (STALE/PARTIAL/LIVE) — a complete ground-truth set, not a sample. The corrected tool (commits d7cd95d52, c94edc037) was measured against it below. Precision is still ~6% and the three downgrade-only fixes provably removed real STALE beads alongside false positives, so recall is also poor. The full reasoning is in Measured against ground truth below; read it before the rest of this PR.

Problem (why this was attempted)

Five separate open beads cost a dispatched agent a full 30-60+ minute investigation cycle before discovering the described work was already done on master (polylogue-o4j2, polylogue-hiu, polylogue-0jf4, polylogue-pbuh, polylogue-cijx.4). That is a recurring failure mode of a 1,260-bead backlog, not a one-off. The technique that worked twice — cherry-picking a cited commit onto master and checking for an empty diff — is real and survives squash-merge id rewriting, unlike git log --is-ancestor or grepping for issue ids. The rest of this PR is the record of why that one true technique was not, in the end, enough to make the sweep trustworthy.

Measured against ground truth (read this first)

The ground truth. All five review groups have now reported. Every one of the 190 beads flagged by the first (pre-fix) sweep carries a bd note from its reviewer with an explicit VERDICT: STALE|PARTIAL|LIVE plus evidence — these are durable bd notes, already committed to the shared bd database, independent of whether this PR merges (queryable today via bd show <id> or bd sql "SELECT id, notes FROM issues WHERE notes LIKE '%VERDICT%'"). Reported group tallies:

group 1   STALE 2   PARTIAL 14   LIVE  22
group 2   STALE 4   PARTIAL 11   LIVE  23
group 3   STALE 0   PARTIAL 13   LIVE  25
group 4   STALE 3   PARTIAL 17   LIVE  18
group 5   STALE 0   PARTIAL  2   LIVE  36
        ─────────────────────────────────
          STALE 9   PARTIAL 57   LIVE 124   = 190

I parsed the actual verdict text out of the live bd notes (VERDICT:/VERIFICATION (groupN...): followed by STALE/PARTIAL/LIVE) and matched 185 of the 190 unambiguously (7 STALE / 49 PARTIAL / 129 LIVE) — 5 beads' notes use phrasing my regex didn't catch; I did not force a match rather than guess. The STALE count I can attribute to specific bead ids is 7, not 9 — noted honestly below; both numbers point to the same conclusion.

The corrected tool's current output (re-run today, commit c94edc037): 66 LIKELY-STALE (10 strong-confidence, 56 weak-confidence) out of 629 open+in_progress beads.

PRECISION — of the 66 now flagged, how many carry a STALE ground-truth label:

Confidence Flagged STALE PARTIAL LIVE Precision (STALE / flagged)
strong 10 2 7 1 20.0%
weak 56 2 10 42 (+2 unlabelled) 3.6%
all 66 4 17 43 (+2 unlabelled) 6.1%

Strong confidence (the live-consumer-verified commit evidence) is meaningfully better than weak (PR-only), but 8 of every 10 strong verdicts are still wrong. This is a real improvement over the pre-fix ~5% but not a usable one.

RECALL — of the labelled STALE beads, how many does the corrected tool still flag:

polylogue-07g6         FLAGGED  (weak)
polylogue-4fm3         MISSED
polylogue-6pii         MISSED
polylogue-7mtf         MISSED
polylogue-9x22         FLAGGED  (strong)
polylogue-e6a0         FLAGGED  (strong)
polylogue-j8yo         FLAGGED  (weak)

4 of 7 found (57.1%), or 4 of the coordinator's reported 9 (44.4%) if the 2 I couldn't locate were also missed. The three fixes are downgrade-only, so every miss is a fix removing a genuine STALE it could not distinguish from a false positive:

  • polylogue-4fm3: the live-consumer check returned "inconclusive" (non-Python change) for a commit the reviewer confirmed both ACs landed on. Inconclusive was deliberately treated as non-strong evidence — correct in general, wrong here.
  • polylogue-6pii: the live-consumer check found no external git grep hit for the changed symbol on a bead the reviewer confirmed was a closable no-AC chore. The consumer check's blind spot: some genuinely-done changes don't leave a grep-visible caller (config/data-only diffs, or a caller added in the same squash-merged commit that the touched-files exclusion also removes).
  • polylogue-7mtf: an own-goal. The suppression-phrase check's xfail keyword — added specifically to catch polylogue-hg97's "Marked xfail" admission of incomplete work — fired on this bead's own text, which merely describes a Marked xfail(condition=py>=3.14, strict=True) regression guard the fix itself added. Same word, opposite meaning. This is exactly the risk of a lexical suppression check: it cannot tell "this admits incompleteness" from "this describes a completed change that happens to mention an xfail marker."

2x2 over the 185 beads with a ground-truth label:

                    STALE      not-STALE
  flagged                4         60
  not flagged            3        118

Conclusion, per the coordinator's decision framework: this is the "precision is still poor" outcome (6.1%, not materially different from the pre-fix ~5% at the level that matters for trusting a headline count) compounded by the "recall collapsed on the fixes' own mechanism" finding — the downgrade-only fixes got the tool quieter without getting it reliable. Recommendation: close this PR unmerged. The operator's framing is right: "is this work done?" is a question about whether acceptance criteria are semantically satisfied, and a git/text query can only check whether artifacts exist or specific phrases are absent. polylogue-aggz remains the clearest illustration — two directly-matching MERGED PRs (#3401, #3405), and the PR bodies themselves state 2 of the bead's 3 declared invariants are untouched. No commit-graph query reaches that; only reading the PR body against the bead's own AC list does, which is a human (or a much more expensive, semantic-reading agent) task, not a git query.

Worth preserving if any of this is revisited: the suppression-phrase check reads the bead's own most recent note, not the commit graph — that is the signal the human reviewers actually used, and several flagged beads said "no code written" or "not attempted" in their own latest note. The polylogue-7mtf false-negative shows the current lexical version isn't safe as-is, but a future tool aimed at note staleness (has this bead's own most-recent-note-implied status been contradicted by newer master state?) rather than commit archaeology (does a cited commit exist?) is the more promising direction the coordinator identified, not this one.

What shipped in this branch (for the record)

devtools/bead_landing_check.py, registered via CommandSpec in devtools/command_catalog.py:

  • Evidence extraction from bead text (commit hashes, PR numbers, file paths), with two precision fixes found via dogfooding: excluding this repo's "Generated from master @ <hash>" snapshot-anchor idiom, and dropping a github.com/.../pull/N URL pattern that matched unrelated PR numbers quoted inside sample payloads.
  • CommitChecker: a single reused throwaway worktree classifies cited commits as unknown-revision / already-on-master / empty-diff / content-equivalent / non-empty-diff / does-not-apply, plus a live-consumer check (added_symbols/has_live_consumer/consumer_check) that git greps for a reference to a landed commit's new symbols outside its own touched files and outside tests.
  • build_open_parent_child_dependents_index and find_suppression_signal: the open-dependents and suppression-phrase downgrade checks described above.
  • PrChecker: disk-cached gh pr view merge-state lookups.
  • Full test suite: tests/unit/devtools/test_bead_landing_check.py, 54 tests including a labelled-evaluation regression block that runs the real pipeline against 6 named confirmed false positives using real git history and real .beads/issues.jsonl (no network).

All of this is real, tested, and does what it claims to do at the mechanism level — extraction is accurate, the worktree/cache/safety machinery works, the git plumbing is correct. It simply does not clear the bar for a trustworthy sweep, which is the thing that was asked for.

Verification

  • ruff check / ruff format --check, mypy --strict — clean.
  • devtools render all --check — no drift.
  • devtools test tests/unit/devtools/test_bead_landing_check.py — 54 passed.
  • devtools verify --quick — exit 0.
  • The precision/recall analysis above is reproducible: devtools workspace bead-landing-check --json for the current flagged set, bd sql "SELECT id, notes FROM issues WHERE notes LIKE '%VERDICT%'" (or bd show <id> --json) for the ground-truth labels.
  • Not run: full devtools verify (testmon not seeded) and devtools lab policy bead-graph (pre-existing, unrelated failures).

Ref #4960 (beads upstream, for context only — not filed against this repo).

Co-Authored-By: Claude noreply@anthropic.com

Sinity and others added 3 commits July 31, 2026 07:07
Problem: five open beads (o4j2, hiu, 0jf4, pbuh, cijx.4) each cost a
dispatched agent a full investigation cycle (30-60+ min) on 2026-07-30/31
before turning out to describe work already completed on master. A one-off
manual sweep of the 606-open-bead backlog is not a repeatable fix.

Solution: devtools workspace bead-landing-check extracts commit hashes, PR
numbers, and file paths cited in a bead's description/design/AC/notes/
close_reason/comments, then verifies them two ways: cherry-picks cited
commits onto master in a single reused throwaway worktree (empty diff, or a
per-file content-equivalence fallback when the raw cherry-pick conflicts,
both prove landing regardless of squash-merge id rewriting -- more reliable
than `git log --is-ancestor` or grepping for issue ids), and checks cited PR
merge state via `gh pr view` (disk-cached, MERGED results reused
permanently). Emits LIKELY-STALE / LIKELY-LIVE / UNDETERMINED per bead with
a confidence tier and the evidence, never auto-closing anything.

Beads with no cited evidence are reported UNDETERMINED, not guessed --
verified against o4j2 (closed, correctly flagged LIKELY-STALE from its
cited merged PRs) and against hiu/0jf4/pbuh/cijx.4 (still open, correctly
UNDETERMINED because the resolving evidence was never written back into the
bead text itself -- exactly why they cost a fresh investigation cycle).

Registered as a CommandSpec in devtools/command_catalog.py per the
documented devtools-command pattern; docs/devtools.md regenerated via
`devtools render devtools-reference`.

Verification: ruff check/format, mypy --strict (clean), devtools render all
--check (no drift). Manual runs against the five motivating beads confirm
correct verdicts including a real does-not-apply/content-divergence case
(polylogue-0jf4's cited commits landed but were further edited afterward --
tool correctly reports UNDETERMINED rather than a false empty-diff match).

Co-Authored-By: Claude <noreply@anthropic.com>
…tree safety

Covers: PR/commit/file evidence extraction (including the pull-URL
false-positive fix), the LIKELY-STALE/LIKELY-LIVE/UNDETERMINED verdict
matrix (empty-diff, already-on-master, content-equivalent, non-empty-diff,
merged/open PR, unresolvable/conflicted evidence -- each must not be
guessed into a confident verdict), CommitChecker against a real throwaway
git repo (unknown-revision, already-on-master, non-empty-diff, and an
empty-diff case built from independently-converging history to model a
squash-merge equivalent), worktree reuse across checks, remove_worktree's
live-process guard (spawns a real subprocess with cwd inside the worktree
and confirms removal is blocked and the directory survives), and PrChecker
disk-cache/offline/refresh semantics with `_run` mocked out (no live gh
calls in tests).

Verification: devtools test tests/unit/devtools/test_bead_landing_check.py
-- 30 passed. ruff check/format and mypy --strict clean on the new file.

Co-Authored-By: Claude <noreply@anthropic.com>
Problem: the first full sweep over the live backlog (629 open/in_progress
beads) flagged 174 "strong" LIKELY-STALE verdicts, but spot-checking them
found systematic false positives. This repo's prework-packet convention
writes "Generated from master @ <hash>" (185+ occurrences) and
verification-pass notes write "re-checked ... against current master
(<hash>)" -- both record what master looked like when a note was written,
not that the cited hash is the bead's own resolving commit.

Confirmed false positive: polylogue-lkrc cites its own verification-pass
master snapshot this way in a note that explicitly says the named gaps are
STILL open -- the tool nonetheless flagged it LIKELY-STALE (strong) because
the snapshot hash is, trivially, an ancestor of current master.

Fix: extract_evidence() now excludes a hex token when the ~24 characters
immediately preceding it match "master @", "master (", "master@", or bare
"master " (case-insensitive) -- this repo's snapshot-anchor idiom. Re-run
over the same 629 beads: strong LIKELY-STALE dropped from 174 to 71,
UNDETERMINED rose from 351 to 435 (favoring "not verifiable" over a false
positive, per the anti-vacuity requirement that a wrong LIKELY-STALE risks
dropping real work). Also added a standing CAVEAT reason line on every
LIKELY-STALE verdict: proving a cited commit/PR exists on master is not
proof every acceptance criterion is satisfied.

Verification: devtools test tests/unit/devtools/test_bead_landing_check.py
-- 33 passed (3 new regression tests reproducing the lkrc/2qx false
positives and a still-genuine self-citation case). ruff check/format and
mypy --strict clean.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds devtools workspace bead-landing-check, which extracts bead evidence, verifies cited commits and pull requests against master, classifies beads as stale, live, or undetermined, supports cache and output options, and includes unit and integration tests.

Changes

Bead landing verification

Layer / File(s) Summary
Evidence extraction and classification
devtools/bead_landing_check.py, tests/unit/devtools/test_bead_landing_check.py
The command extracts PR numbers, commit candidates, and supported file paths. Tests cover valid evidence and excluded snapshot or sample-payload references.
Commit, worktree, and PR verification
devtools/bead_landing_check.py, tests/unit/devtools/test_bead_landing_check.py
Commit checks use ancestry, cherry-pick, and content-equivalence checks. PR checks use cached gh queries with offline and refresh behavior. Worktree cleanup blocks active processes and dirty state.
Verdict computation and command execution
devtools/bead_landing_check.py, tests/unit/devtools/test_bead_landing_check.py
The CLI loads JSONL beads, applies selection and check options, computes confidence-rated verdicts, and renders human or JSON output. Tests cover verdicts, loading, and integration behavior.
Command catalog and documentation
devtools/command_catalog.py, docs/devtools.md
Registers workspace bead-landing-check and documents its evidence-based landing check.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BeadLandingCheck
  participant CommitChecker
  participant GitWorktree
  participant PrChecker
  participant GitHubCLI
  BeadLandingCheck->>CommitChecker: verify cited commits
  CommitChecker->>GitWorktree: check ancestry or content equivalence
  GitWorktree-->>CommitChecker: commit result
  BeadLandingCheck->>PrChecker: inspect cited PRs
  PrChecker->>GitHubCLI: query merge state
  GitHubCLI-->>PrChecker: cached or refreshed PR result
  BeadLandingCheck-->>BeadLandingCheck: compute and render verdict
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the new bead-landing-check tool and its empty-diff evidence verification.
Description check ✅ Passed The description covers the change, problem, solution, verification, risks, and measured limitations, but it omits the required Changelog heading.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/devtools/bead-landing-check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7cd95d52a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +265 to +267
self._ensure_worktree()
self._reset_worktree()
result = _run(["git", "cherry-pick", "--no-commit", "--allow-empty", commit], cwd=self.worktree_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Lock the shared worktree before resetting it

When two agents run the default full-backlog check concurrently, both processes reuse .cache/bead-landing-check/worktree, but the reset, cherry-pick, diff, and cleanup operations are not protected by a lock. One process can reset the index after the other's cherry-pick but before its diff, causing that commit to be falsely classified as empty-diff, or it can replace the staged changes with another commit. Serialize the complete worktree transaction or allocate a per-process worktree.

Useful? React with 👍 / 👎.

Comment on lines +260 to +263
if not self._is_real_commit(commit):
return "unknown-revision"
if self._is_ancestor(commit):
return "already-on-master"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fetch before rejecting locally unknown commits

In a checkout whose remote-tracking refs have not been fetched recently, a valid cited commit that has landed on origin/master but is absent from the local object database is immediately returned as unknown-revision. The fetch only happens later inside _is_ancestor, which this early return never reaches, so the advertised check against current master becomes UNDETERMINED until the user manually fetches. Fetch first, or retry cat-file after fetching before rejecting the hash.

Useful? React with 👍 / 👎.

Comment on lines +391 to +393
def check(self, number: int) -> PrResult:
key = str(number)
cached = self._cache.get(key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Namespace cached PR results by repository

When --repo-slug is used to check a fork or another repository, this cache key contains only the PR number. A result previously cached for Sinity/polylogue—especially a permanently cached MERGED result—is therefore reused for the same PR number in the other repository without calling gh, potentially producing a false LIKELY-STALE verdict. Include repo_slug in the cache key or use a repository-specific cache file.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with 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.

Inline comments:
In `@devtools/bead_landing_check.py`:
- Around line 164-165: Update _run to catch subprocess.TimeoutExpired and
FileNotFoundError from subprocess.run, returning a synthetic CompletedProcess
with a non-zero returncode and suitable error text in its captured output.
Preserve normal CompletedProcess results so existing returncode != 0 handling
classifies timeouts and missing binaries as unverified without aborting the
sweep.
- Around line 245-252: Update the blob comparison loop in _commit_touched_files
to compare the base and commit versions using Git blob object IDs or hashes
rather than _run’s decoded stdout. Preserve the existing return-code handling
for paths present on only one side, and ensure binary or large files are
compared without text decoding or loading their contents into memory.
- Around line 480-488: Update the LIKELY-STALE reporting branch around landed,
live, and inapplicable so commits in inapplicable are explicitly included in
reasons alongside the existing landed and live details. Ensure unresolved
commits receive the same treatment, preserving the ambiguous evidence in the
summary and avoiding a strong stale conclusion when conflicted commits are
present.
- Around line 279-291: Remove the unused check_file_paths function and
_MAX_FILE_CHECKS_PER_BEAD constant from the bead landing check module. Keep
main’s existing behavior of copying raw evidence.file_paths into the payload,
without adding path verification.
- Line 636: Propagate the parsed offline flag from main to CommitChecker, then
update CommitChecker._ensure_fetch to return without running git fetch origin
when offline is enabled. Preserve the existing fetch behavior for normal online
runs and ensure _is_ancestor uses the offline-aware checker.
- Around line 643-648: Update CommitChecker._ensure_worktree to validate
ownership before reusing an existing directory containing a .git marker. Compare
the worktree’s resolved common Git directory with repo_root, and only permit
reuse when the resolved path is under the tool’s cache prefix or matches an
explicitly acknowledged custom path; otherwise reject it before _reset_worktree
or --remove-worktree can operate.

In `@tests/unit/devtools/test_bead_landing_check.py`:
- Around line 104-113: Add a parametrized test alongside
test_extract_evidence_reads_comments_too that iterates over title, description,
design, acceptance_criteria, notes, and close_reason, constructs a bead with
“Landed in `#3390`.” in the selected field, and asserts extract_evidence returns
pr_numbers [3390].
- Around line 428-447: Add a TTL-expiry test alongside
test_pr_checker_refresh_ignores_cache that uses the existing frozen_clock
fixture to control time, advances beyond the configured TTL, and verifies
PrChecker.check refetches the cached non-MERGED entry by asserting the mocked
_run call count increases. Cover the expiry boundary explicitly without relying
on the host wall clock.
- Around line 193-199: Update the tests for both strong-commit and weak-PR
LIKELY-STALE cases to assert that the verdict’s reasons include CAVEAT. In
test_verdict_merged_pr_only_is_likely_stale_weak and the corresponding
strong-commit test, preserve the existing verdict and confidence assertions
while validating the appended caveat reason.
- Around line 35-43: Update _make_repo to configure the throwaway repository
with commit signing disabled and hooks isolated from host configuration before
creating the initial commit. Add the repository-local Git settings needed to
override commit.gpgsign and core.hooksPath, while preserving the existing user
identity and commit setup.
- Around line 326-337: Update the second-commit setup in the test around
feature_sha and third_sha so the commit is created on a branch not reachable
from master, matching the earlier feature-branch setup. Ensure
checker.check(third_sha) must perform a second worktree operation and
cherry-pick, allowing the existing worktree directory and mtime assertions to
verify reuse.
- Around line 356-380: The worktree tests relying on /proc must run only on
Linux. Add an explicit Linux platform guard to
test_remove_worktree_blocks_on_live_process and
test_remove_worktree_removes_clean_unoccupied_worktree, and replace the host
time.sleep call in the live-process test with the repository-approved test-clock
sleep mechanism.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ef55644f-a098-4926-9000-b5642ef41186

📥 Commits

Reviewing files that changed from the base of the PR and between 561a14f and d7cd95d.

📒 Files selected for processing (4)
  • devtools/bead_landing_check.py
  • devtools/command_catalog.py
  • docs/devtools.md
  • tests/unit/devtools/test_bead_landing_check.py

Comment on lines +164 to +165
def _run(cmd: list[str], *, cwd: Path | None = None, timeout: float | None = 30) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Convert subprocess timeouts and a missing binary into a failed result.

_run lets subprocess.TimeoutExpired and FileNotFoundError escape. Every caller assumes a CompletedProcess. One slow git fetch, one slow gh pr view, or a host without gh installed aborts the whole sweep with a traceback. The PR cache is then never flushed, so every result fetched during that run is lost. This matters for the documented 600+ bead sweep.

Return a synthetic non-zero result instead, so the existing returncode != 0 paths classify the evidence as unverified.

🛡️ Proposed fix
 def _run(cmd: list[str], *, cwd: Path | None = None, timeout: float | None = 30) -> subprocess.CompletedProcess[str]:
-    return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
+    try:
+        return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
+    except subprocess.TimeoutExpired:
+        return subprocess.CompletedProcess(cmd, 124, stdout="", stderr=f"timeout after {timeout}s: {' '.join(cmd)}")
+    except (FileNotFoundError, OSError) as exc:
+        return subprocess.CompletedProcess(cmd, 127, stdout="", stderr=f"failed to run {cmd[0]}: {exc}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _run(cmd: list[str], *, cwd: Path | None = None, timeout: float | None = 30) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
def _run(cmd: list[str], *, cwd: Path | None = None, timeout: float | None = 30) -> subprocess.CompletedProcess[str]:
try:
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired:
return subprocess.CompletedProcess(cmd, 124, stdout="", stderr=f"timeout after {timeout}s: {' '.join(cmd)}")
except (FileNotFoundError, OSError) as exc:
return subprocess.CompletedProcess(cmd, 127, stdout="", stderr=f"failed to run {cmd[0]}: {exc}")
🧰 Tools
🪛 ast-grep (0.45.0)

[error] 164-164: Use of unsanitized data to create processes
Context: subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(os-system-unsanitized-data)


[error] 164-164: Command coming from incoming request
Context: subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 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 `@devtools/bead_landing_check.py` around lines 164 - 165, Update _run to catch
subprocess.TimeoutExpired and FileNotFoundError from subprocess.run, returning a
synthetic CompletedProcess with a non-zero returncode and suitable error text in
its captured output. Preserve normal CompletedProcess results so existing
returncode != 0 handling classifies timeouts and missing binaries as unverified
without aborting the sweep.

Comment on lines +245 to +252
for path in files:
base = _run(["git", "show", f"{self.base_ref}:{path}"], cwd=self.repo_root)
theirs = _run(["git", "show", f"{commit}:{path}"], cwd=self.repo_root)
if (base.returncode == 0) != (theirs.returncode == 0):
return False
if base.returncode == 0 and base.stdout != theirs.stdout:
return False
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the blob comparison against binary content.

_run uses text=True, so git show decodes the blob with the locale codec. _commit_touched_files returns every path the commit touched, including binary fixtures such as PNG or GIF files. A binary blob raises UnicodeDecodeError inside subprocess.run, and the whole sweep aborts.

Compare blob hashes instead of decoded content. This removes the decoding risk and avoids holding large blobs in memory.

🛡️ Proposed fix
         for path in files:
-            base = _run(["git", "show", f"{self.base_ref}:{path}"], cwd=self.repo_root)
-            theirs = _run(["git", "show", f"{commit}:{path}"], cwd=self.repo_root)
+            base = _run(["git", "rev-parse", f"{self.base_ref}:{path}"], cwd=self.repo_root)
+            theirs = _run(["git", "rev-parse", f"{commit}:{path}"], cwd=self.repo_root)
             if (base.returncode == 0) != (theirs.returncode == 0):
                 return False
-            if base.returncode == 0 and base.stdout != theirs.stdout:
+            if base.returncode == 0 and base.stdout.strip() != theirs.stdout.strip():
                 return False
         return True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for path in files:
base = _run(["git", "show", f"{self.base_ref}:{path}"], cwd=self.repo_root)
theirs = _run(["git", "show", f"{commit}:{path}"], cwd=self.repo_root)
if (base.returncode == 0) != (theirs.returncode == 0):
return False
if base.returncode == 0 and base.stdout != theirs.stdout:
return False
return True
for path in files:
base = _run(["git", "rev-parse", f"{self.base_ref}:{path}"], cwd=self.repo_root)
theirs = _run(["git", "rev-parse", f"{commit}:{path}"], cwd=self.repo_root)
if (base.returncode == 0) != (theirs.returncode == 0):
return False
if base.returncode == 0 and base.stdout.strip() != theirs.stdout.strip():
return False
return True
🤖 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 `@devtools/bead_landing_check.py` around lines 245 - 252, Update the blob
comparison loop in _commit_touched_files to compare the base and commit versions
using Git blob object IDs or hashes rather than _run’s decoded stdout. Preserve
the existing return-code handling for paths present on only one side, and ensure
binary or large files are compared without text decoding or loading their
contents into memory.

Comment on lines +279 to +291
def check_file_paths(repo_root: Path, paths: list[str], base_ref: str) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for p in paths[:_MAX_FILE_CHECKS_PER_BEAD]:
exists = _run(["git", "cat-file", "-e", f"{base_ref}:{p}"], cwd=repo_root).returncode == 0
entry: dict[str, Any] = {"path": p, "exists_on_master": exists}
if exists:
log = _run(["git", "log", "-1", "--format=%H|%cI", base_ref, "--", p], cwd=repo_root)
if log.stdout.strip():
sha, _, date = log.stdout.strip().partition("|")
entry["last_commit"] = sha
entry["last_commit_date"] = date
out.append(entry)
return out

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify check_file_paths and _MAX_FILE_CHECKS_PER_BEAD have no callers outside their definition.
rg -nP --type=py -C3 '\bcheck_file_paths\b|_MAX_FILE_CHECKS_PER_BEAD'

Repository: Sinity/polylogue

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -u

echo "files:"
git ls-files | rg '(^|/)devtools/bead_landing_check\.py$|(^|/)test.*bead.*\.py$|test.*bead|bead.*test|command_catalog\.py' || true

echo
echo "tracked python files under devtools:"
git ls-files 'devtools/*.py' | sort

echo
echo "search identifiers without rg maybe due tooling:"
python3 - <<'PY'
from pathlib import Path
for p in Path('.').rglob('*.py'):
    if '.git' in p.parts or 'node_modules' in p.parts:
        continue
    try:
        txt = p.read_text(errors='ignore')
    except Exception:
        continue
    if 'check_file_paths' in txt or '_MAX_FILE_CHECKS_PER_BEAD' in txt:
        print(f"\n--- {p}")
        for i,line in enumerate(txt.splitlines(),1):
            if 'check_file_paths' in line or '_MAX_FILE_CHECKS_PER_BEAD' in line:
                print(f"{i}: {line}")
PY

Repository: Sinity/polylogue

Length of output: 6982


Remove the unused file-check helpers.

check_file_paths and _MAX_FILE_CHECKS_PER_BEAD are defined in devtools/bead_landing_check.py, but main only copies raw evidence.file_paths into the payload and no caller resolves whether those paths exist. Remove them, or wire up verification and test the result.

🤖 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 `@devtools/bead_landing_check.py` around lines 279 - 291, Remove the unused
check_file_paths function and _MAX_FILE_CHECKS_PER_BEAD constant from the bead
landing check module. Keep main’s existing behavior of copying raw
evidence.file_paths into the payload, without adding path verification.

Comment thread devtools/bead_landing_check.py Outdated
Comment on lines +480 to +488
if landed:
verdict, confidence = "LIKELY-STALE", "strong"
detail = ", ".join(f"{c} ({commit_results[c]})" for c in landed)
reasons.append(f"cited commit(s) already on master: {detail}")
if live:
reasons.append(
f"NOTE: commit(s) {', '.join(live)} still produce a non-empty diff -- "
"possibly a partial landing, verify remaining scope before closing"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report conflicted commits in the LIKELY-STALE branch.

The landed branch adds a NOTE for commits in live, but it never mentions commits in inapplicable. A bead that cites one landed commit and one commit that fails to cherry-pick is reported as LIKELY-STALE with strong confidence, and the conflicted commit disappears from reasons. A reader who follows the summary line loses the ambiguous evidence. unresolved has the same gap.

🐛 Proposed fix
     if landed:
         verdict, confidence = "LIKELY-STALE", "strong"
         detail = ", ".join(f"{c} ({commit_results[c]})" for c in landed)
         reasons.append(f"cited commit(s) already on master: {detail}")
         if live:
             reasons.append(
                 f"NOTE: commit(s) {', '.join(live)} still produce a non-empty diff -- "
                 "possibly a partial landing, verify remaining scope before closing"
             )
+        if inapplicable:
+            reasons.append(
+                f"NOTE: commit(s) {', '.join(inapplicable)} could not be cherry-picked cleanly onto master "
+                "(conflict) -- landing state of that evidence is unknown"
+            )
+        if unresolved:
+            reasons.append(
+                f"NOTE: cited hex token(s) {', '.join(unresolved)} are not real commits in this repo"
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if landed:
verdict, confidence = "LIKELY-STALE", "strong"
detail = ", ".join(f"{c} ({commit_results[c]})" for c in landed)
reasons.append(f"cited commit(s) already on master: {detail}")
if live:
reasons.append(
f"NOTE: commit(s) {', '.join(live)} still produce a non-empty diff -- "
"possibly a partial landing, verify remaining scope before closing"
)
if landed:
verdict, confidence = "LIKELY-STALE", "strong"
detail = ", ".join(f"{c} ({commit_results[c]})" for c in landed)
reasons.append(f"cited commit(s) already on master: {detail}")
if live:
reasons.append(
f"NOTE: commit(s) {', '.join(live)} still produce a non-empty diff -- "
"possibly a partial landing, verify remaining scope before closing"
)
if inapplicable:
reasons.append(
f"NOTE: commit(s) {', '.join(inapplicable)} could not be cherry-picked cleanly onto master "
"(conflict) -- landing state of that evidence is unknown"
)
if unresolved:
reasons.append(
f"NOTE: cited hex token(s) {', '.join(unresolved)} are not real commits in this repo"
)
🤖 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 `@devtools/bead_landing_check.py` around lines 480 - 488, Update the
LIKELY-STALE reporting branch around landed, live, and inapplicable so commits
in inapplicable are explicitly included in reasons alongside the existing landed
and live details. Ensure unresolved commits receive the same treatment,
preserving the ambiguous evidence in the summary and avoiding a strong stale
conclusion when conflicted commits are present.

Comment thread devtools/bead_landing_check.py
Comment on lines +104 to +113
def test_extract_evidence_reads_comments_too() -> None:
bead = _bead(comments=[{"text": "Landed in #3390."}])
ev = extract_evidence(bead)
assert ev.pr_numbers == [3390]


def test_extract_evidence_finds_file_paths() -> None:
bead = _bead(description="See polylogue/sources/parsers/claude/code_parser.py:87 for the skip list.")
ev = extract_evidence(bead)
assert "polylogue/sources/parsers/claude/code_parser.py" in ev.file_paths

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover every field _bead_text reads.

_bead_text concatenates title, description, design, acceptance_criteria, notes, close_reason, and comment text. These tests exercise only description and comments. A change that drops design, acceptance_criteria, notes, or close_reason from _bead_text keeps this suite green, and the sweep then silently misses evidence.

Add one parametrized test over the field names.

💚 Proposed test
`@pytest.mark.parametrize`(
    "field",
    ["title", "description", "design", "acceptance_criteria", "notes", "close_reason"],
)
def test_extract_evidence_reads_every_text_field(field: str) -> None:
    ev = extract_evidence(_bead(**{field: "Landed in `#3390`."}))
    assert ev.pr_numbers == [3390]
🤖 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 `@tests/unit/devtools/test_bead_landing_check.py` around lines 104 - 113, Add a
parametrized test alongside test_extract_evidence_reads_comments_too that
iterates over title, description, design, acceptance_criteria, notes, and
close_reason, constructs a bead with “Landed in `#3390`.” in the selected field,
and asserts extract_evidence returns pr_numbers [3390].

Comment on lines +193 to +199
def test_verdict_merged_pr_only_is_likely_stale_weak() -> None:
bead = _bead("polylogue-a")
ev = Evidence(pr_numbers=[3390])
pr_results = {3390: PrResult(number=3390, found=True, state="MERGED")}
v = verdict_for_bead(bead, ev, {}, pr_results)
assert v.verdict == "LIKELY-STALE"
assert v.confidence == "weak"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the CAVEAT reason on LIKELY-STALE verdicts.

verdict_for_bead appends a CAVEAT reason for every LIKELY-STALE verdict. The PR describes that caveat as a deliberate honesty guarantee. No test asserts it, so a change that drops the append stays green.

Add the assertion to the strong-commit case and to this weak-PR case.

💚 Proposed test change
     v = verdict_for_bead(bead, ev, {}, pr_results)
     assert v.verdict == "LIKELY-STALE"
     assert v.confidence == "weak"
+    assert any(r.startswith("CAVEAT:") for r in v.reasons)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_verdict_merged_pr_only_is_likely_stale_weak() -> None:
bead = _bead("polylogue-a")
ev = Evidence(pr_numbers=[3390])
pr_results = {3390: PrResult(number=3390, found=True, state="MERGED")}
v = verdict_for_bead(bead, ev, {}, pr_results)
assert v.verdict == "LIKELY-STALE"
assert v.confidence == "weak"
def test_verdict_merged_pr_only_is_likely_stale_weak() -> None:
bead = _bead("polylogue-a")
ev = Evidence(pr_numbers=[3390])
pr_results = {3390: PrResult(number=3390, found=True, state="MERGED")}
v = verdict_for_bead(bead, ev, {}, pr_results)
assert v.verdict == "LIKELY-STALE"
assert v.confidence == "weak"
assert any(r.startswith("CAVEAT:") for r in v.reasons)
🤖 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 `@tests/unit/devtools/test_bead_landing_check.py` around lines 193 - 199,
Update the tests for both strong-commit and weak-PR LIKELY-STALE cases to assert
that the verdict’s reasons include CAVEAT. In
test_verdict_merged_pr_only_is_likely_stale_weak and the corresponding
strong-commit test, preserve the existing verdict and confidence assertions
while validating the appended caveat reason.

Comment on lines +326 to +337
# A second, distinct commit must reuse the same worktree directory rather
# than creating a fresh one.
(repo / "third.txt").write_text("y\n")
_run_git(["add", "third.txt"], cwd=repo)
_run_git(["commit", "-m", "add third"], cwd=repo)
third_sha = _run_git(["rev-parse", "HEAD"], cwd=repo).stdout.strip()
_run_git(["checkout", "master~1"], cwd=repo) # detach so third_sha isn't already master's tip
_run_git(["checkout", "master"], cwd=repo)

checker.check(third_sha)
assert wt_dir.exists()
assert (wt_dir / ".git").stat().st_mtime == marker_mtime

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The reuse assertion does not exercise a second worktree operation.

Trace the repository state. Line 320 checks out master, so the commit created on Lines 328-330 lands on master. third_sha on Line 331 is therefore master's tip. Lines 332-333 detach and re-attach but leave the tip unchanged.

checker.check(third_sha) on Line 335 then hits the _is_ancestor short-circuit and returns already-on-master. It never calls _ensure_worktree and never runs a cherry-pick. The .git mtime assertion on Line 337 passes because no worktree work happens at all, so the test does not prove worktree reuse.

Create the second commit on a branch that is not reachable from master, as the earlier part of the test does for feature_sha.

💚 Proposed fix
     # A second, distinct commit must reuse the same worktree directory rather
     # than creating a fresh one.
+    _run_git(["checkout", "feature"], cwd=repo)
     (repo / "third.txt").write_text("y\n")
     _run_git(["add", "third.txt"], cwd=repo)
     _run_git(["commit", "-m", "add third"], cwd=repo)
     third_sha = _run_git(["rev-parse", "HEAD"], cwd=repo).stdout.strip()
-    _run_git(["checkout", "master~1"], cwd=repo)  # detach so third_sha isn't already master's tip
     _run_git(["checkout", "master"], cwd=repo)
 
-    checker.check(third_sha)
+    assert checker.check(third_sha) == "non-empty-diff"
     assert wt_dir.exists()
     assert (wt_dir / ".git").stat().st_mtime == marker_mtime
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# A second, distinct commit must reuse the same worktree directory rather
# than creating a fresh one.
(repo / "third.txt").write_text("y\n")
_run_git(["add", "third.txt"], cwd=repo)
_run_git(["commit", "-m", "add third"], cwd=repo)
third_sha = _run_git(["rev-parse", "HEAD"], cwd=repo).stdout.strip()
_run_git(["checkout", "master~1"], cwd=repo) # detach so third_sha isn't already master's tip
_run_git(["checkout", "master"], cwd=repo)
checker.check(third_sha)
assert wt_dir.exists()
assert (wt_dir / ".git").stat().st_mtime == marker_mtime
# A second, distinct commit must reuse the same worktree directory rather
# than creating a fresh one.
_run_git(["checkout", "feature"], cwd=repo)
(repo / "third.txt").write_text("y\n")
_run_git(["add", "third.txt"], cwd=repo)
_run_git(["commit", "-m", "add third"], cwd=repo)
third_sha = _run_git(["rev-parse", "HEAD"], cwd=repo).stdout.strip()
_run_git(["checkout", "master"], cwd=repo)
assert checker.check(third_sha) == "non-empty-diff"
assert wt_dir.exists()
assert (wt_dir / ".git").stat().st_mtime == marker_mtime
🤖 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 `@tests/unit/devtools/test_bead_landing_check.py` around lines 326 - 337,
Update the second-commit setup in the test around feature_sha and third_sha so
the commit is created on a branch not reachable from master, matching the
earlier feature-branch setup. Ensure checker.check(third_sha) must perform a
second worktree operation and cherry-pick, allowing the existing worktree
directory and mtime assertions to verify reuse.

Comment on lines +356 to +380
def test_remove_worktree_blocks_on_live_process(tmp_path: Path) -> None:
repo = _make_repo(tmp_path / "repo")
wt = tmp_path / "wt"
_run_git(["worktree", "add", "--detach", str(wt), "master"], cwd=repo)

proc = subprocess.Popen(["sleep", "5"], cwd=wt)
try:
# Give the OS a moment to publish /proc/<pid>/cwd.
for _ in range(50):
if Path(f"/proc/{proc.pid}/cwd").exists():
break
time.sleep(0.05)
assert remove_worktree(repo, wt) == "blocked-live-process"
assert wt.exists() # never removed while occupied
finally:
proc.kill()
proc.wait()


def test_remove_worktree_removes_clean_unoccupied_worktree(tmp_path: Path) -> None:
repo = _make_repo(tmp_path / "repo")
wt = tmp_path / "wt"
_run_git(["worktree", "add", "--detach", str(wt), "master"], cwd=repo)
assert remove_worktree(repo, wt) == "removed"
assert not wt.exists()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check which clock APIs devtools verify test-clock-hygiene blocks and what the allowlist contains.
fd -t f 'verify_test_clock_hygiene.py' devtools --exec rg -n 'time\.|datetime\.|BANNED|FORBIDDEN|allowlist'
fd -t f 'test-clock-allowlist.yaml' docs --exec cat

Repository: Sinity/polylogue

Length of output: 10797


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate files =="
fd -t f 'test_bead_landing_check.py|*.py' tests/unit/devtools | sed -n '1,80p'

echo
echo "== Target test file outline =="
ast-grep outline tests/unit/devtools/test_bead_landing_check.py --view compact 2>/dev/null | sed -n '1,160p' || true

echo
echo "== Source around remove_worktree tests =="
sed -n '320,390p' tests/unit/devtools/test_bead_landing_check.py

echo
echo "== Search for _worktree_has_live_process and remove_worktree =="
rg -n "_worktree_has_live_process|def remove_worktree|remove_worktree\(" tests/unit tests -g '*.py' | sed -n '1,200p'

Repository: Sinity/polylogue

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate target and related search terms =="
git ls-files | rg '(^|/)tests/unit/devtools/test_bead_landing_check\.py$|(_worktree_has_live_process|remove_worktree)\.py' || true

echo
echo "== Target source around remove_worktree tests =="
sed -n '320,390p' tests/unit/devtools/test_bead_landing_check.py

echo
echo "== Search for _worktree_has_live_process and remove_worktree =="
rg -n "def _worktree_has_live_process|def remove_worktree|remove_worktree\(|_worktree_has_live_process" tests -g '*.py' | sed -n '1,200p'

echo
echo "== Top-level import/setup of this test file =="
sed -n '1,80p' tests/unit/devtools/test_bead_landing_check.py

Repository: Sinity/polylogue

Length of output: 5601


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate devtools.bead_landing_check =="
git ls-files | rg '(^|/)devtools/.*bead.*landing.*check.*\.py$|(^|/)devtools/b.*\.py$|(^|/)devtools/__init__\.py$' | sed -n '1,120p'

echo
echo "== Search worktree/live process implementation =="
rg -n "def remove_worktree|def _worktree_has_live_process|worktree_has_live_process|blocked-live-process|remove-worktree|worktree remove" . -g '*.py' | sed -n '1,240p'

echo
echo "== If devtools.bead_landing_check.py exists, inspect workspace handling =="
if [ -f devtools/bead_landing_check.py ]; then
  wc -l devtools/bead_landing_check.py
  rg -n "def remove_worktree|def _worktree_has_live_process|from pathlib import Path|Path\("/proc|cwd" or 'cwd' in|subprocess" devtools/bead_landing_check.py
  sed -n '1,260p' devtools/bead_landing_check.py
fi

Repository: Sinity/polylogue

Length of output: 1855


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== devtools/bead_landing_check.py relevant implementation =="
sed -n '260,340p' devtools/bead_landing_check.py

echo
echo "== module-level imports =="
sed -n '1,40p' devtools/bead_landing_check.py

Repository: Sinity/polylogue

Length of output: 5402


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check whether time.sleep is allowlisted =="
python3 - <<'PY'
from pathlib import Path
import re
yaml = Path("docs/plans/test-clock-allowlist.yaml").read_text()
m = re.search(r'(^files:\n(?:^  - path: .*?\n(?:^      .*?\n)?)*?- path: .*?\n(?:^      .*?\n)?)', yaml, re.M | re.S)
if m:
    print("allowlist:")
    print(m.group(1))
else:
    print("no allowfile found")

print("contains target_path_entry:", str(Path("tests/unit/devtools/test_bead_landing_check.py")) in yaml)
PY

echo
echo "== Read-only AST/source probe of time.sleep and /proc use =="
python3 - <<'PY'
import ast
from pathlib import Path

paths = [
    Path("devtools/bead_landing_check.py"),
    Path("tests/unit/devtools/test_bead_landing_check.py"),
]
for path in paths:
    print(f"\n--- {path} ---")
    source = path.read_text()
    tree = ast.parse(source)
    print("time.sleep Calls:")
    found = False
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Attribute):
                if isinstance(node.func.value, ast.Name) and node.func.value.id == "time" and node.func.attr == "sleep":
                    print(f"  line {node.lineno}")
                    found = True
    print("time.sleep" if found else "no time.sleep Calls")
    print("absolute Path('/proc' ...), Path(\"/proc\" ...), or '/proc' found:")
    for i, line in enumerate(source.splitlines(), 1):
        if "/proc" in line or "Path(\"/proc\"" in line or "Path('/proc'" in line:
            print(f"  {i}: {line}")
PY

Repository: Sinity/polylogue

Length of output: 783


Make the /proc-dependent worktree tests Linux-only.

_worktree_has_live_process returns True when /proc is absent, so on macOS these tests do not call the normal removal paths:

  • test_remove_worktree_removes_clean_unoccupied_worktree returns blocked-live-process instead of removed.
  • test_remove_worktree_blocks_on_live_process only checks whether remove_worktree reports the conservative unavailable state.

Add an explicit platform guard, and ensure the host time.sleep call does not violate the test clock allowlist.

🧰 Tools
🪛 ast-grep (0.45.0)

[error] 360-360: Command coming from incoming request
Context: subprocess.Popen(["sleep", "5"], cwd=wt)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 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 `@tests/unit/devtools/test_bead_landing_check.py` around lines 356 - 380, The
worktree tests relying on /proc must run only on Linux. Add an explicit Linux
platform guard to test_remove_worktree_blocks_on_live_process and
test_remove_worktree_removes_clean_unoccupied_worktree, and replace the host
time.sleep call in the live-process test with the repository-approved test-clock
sleep mechanism.

Source: Coding guidelines

Comment on lines +428 to +447
def test_pr_checker_refresh_ignores_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
calls = {"n": 0}

def fake_run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
calls["n"] += 1
return subprocess.CompletedProcess(
cmd, 0, stdout=json.dumps({"state": "OPEN", "mergedAt": None, "mergeCommit": None, "title": "x"}), stderr=""
)

monkeypatch.setattr("devtools.bead_landing_check._run", fake_run)
cache_path = tmp_path / "cache.json"
checker = PrChecker("Sinity/polylogue", cache_path, ttl_days=7)
checker.check(300)
assert calls["n"] == 1
checker.check(300)
assert calls["n"] == 1 # OPEN is TTL-cached, reused within TTL

checker_refresh = PrChecker("Sinity/polylogue", cache_path, ttl_days=7, refresh=True)
checker_refresh.check(300)
assert calls["n"] == 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the TTL-expiry branch with a controlled clock.

PrChecker.check reuses a non-MERGED cache entry only while now - checked_at < ttl_seconds. This test proves the reuse side and the refresh=True side. No test proves the expiry side, so a change to the TTL comparison keeps this suite green while the tool serves stale OPEN PR state and produces a wrong LIKELY-LIVE verdict.

Add a case that ages the cached entry. Use frozen_clock so the assertion does not depend on the host wall clock.

💚 Proposed test
def test_pr_checker_refetches_after_ttl_expiry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    calls = {"n": 0}

    def fake_run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        calls["n"] += 1
        return subprocess.CompletedProcess(
            cmd, 0, stdout=json.dumps({"state": "OPEN", "mergedAt": None, "mergeCommit": None, "title": "x"}), stderr=""
        )

    monkeypatch.setattr("devtools.bead_landing_check._run", fake_run)
    cache_path = tmp_path / "cache.json"
    PrChecker("Sinity/polylogue", cache_path, ttl_days=7).check(300)
    assert calls["n"] == 1

    # ttl_days=0 makes every non-MERGED entry immediately stale.
    PrChecker("Sinity/polylogue", cache_path, ttl_days=0).check(300)
    assert calls["n"] == 2

As per coding guidelines, "Use frozen_clock for timestamp-sensitive tests".

🧰 Tools
🪛 ast-grep (0.45.0)

[info] 433-433: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"state": "OPEN", "mergedAt": None, "mergeCommit": None, "title": "x"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 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 `@tests/unit/devtools/test_bead_landing_check.py` around lines 428 - 447, Add a
TTL-expiry test alongside test_pr_checker_refresh_ignores_cache that uses the
existing frozen_clock fixture to control time, advances beyond the configured
TTL, and verifies PrChecker.check refetches the cached non-MERGED entry by
asserting the mocked _run call count increases. Cover the expiry boundary
explicitly without relying on the host wall clock.

Source: Coding guidelines

…s LIKELY-STALE

Problem: five independent human reviewers checked all 190 LIKELY-STALE
verdicts from the first full sweep against 114 real beads. Measured
precision was catastrophic -- roughly 6 of 114 (~5%) were genuinely safe to
close; ~95% were false positives. The root cause, found independently by
two reviewers: the heuristic keyed off "a cited commit/PR exists on
master" without checking whether the shipped code has any live production
consumer -- exactly the "code shipped, nothing reads it" defect class this
repo has spent the night finding elsewhere. Concrete confirmed false
positives: polylogue-rxdo.9.6 (blind_items() has zero callers outside its
own test), polylogue-rxdo.6 (ReferenceQueryPipeline has zero CLI/MCP/daemon
references and still hard-errors), polylogue-rxdo.9.7 (ClaimWithControls
has zero callers outside its own test), polylogue-hg97 (cost_outlook absent
from polylogue/mcp/, its contract test still xfail), polylogue-dcz5 (3.14t
live in prod, but daemon_parse_stage_split is still False), polylogue-yp0
(EventBus core landed, notes explicitly say "NOT wired to a live
producer/consumer"). A second failure mode: epic/parent beads flagged
because their own commit landed while most dotted-id/parent-child
dependents remain open (polylogue-2qx: 9 open dependents; polylogue-3tl:
17; polylogue-rxdo.9: 7 of 9 children).

Solution -- three corrective checks, all downgrade-only (never upgrade a
verdict, per the standing rule that a wrong LIKELY-STALE is worse than an
honest UNDETERMINED):

1. LIVE-CONSUMER CHECK (CommitChecker.added_symbols/has_live_consumer/
   consumer_check): a landed commit only counts as strong evidence if at
   least one top-level symbol it added has a `git grep` hit outside the
   commit's own touched files and outside tests/. Landed-but-unconsumed or
   landed-with-inconclusive-symbols both downgrade to UNDETERMINED.
2. OPEN-DEPENDENTS CHECK (build_open_parent_child_dependents_index): a bead
   with unresolved `parent-child` dependents (open/in_progress) is never
   LIKELY-STALE regardless of its own evidence.
3. SUPPRESSION-PHRASE CHECK (find_suppression_signal): if the bead's own
   text already says "deferred"/"xfail"/"not wired"/"is NOT satisfied"/etc,
   that overrides commit/PR evidence and forces UNDETERMINED.

Re-run over the same 629 open+in_progress beads: LIKELY-STALE dropped from
190 to 84 (UNDETERMINED rose from 435 to 541), runtime 14s warm (was 8s --
consumer checks add `git show`/`git grep` per landed commit, still far
below the cold-sweep baseline). Known residual gap: polylogue-a7xr.16
remains a weak-confidence LIKELY-STALE from a merged-PR citation alone --
its own note claims "WORK COMPLETE" while only the INSERT half of the
column-spec refactor actually landed (SELECT's 14 methods/503 accessors
untouched); no cited commit and no suppression phrase exists in its text
for this tool to catch, which is exactly why the CAVEAT reason on every
LIKELY-STALE verdict says to read the AC list, not just the tool's verdict.

Verification: devtools test tests/unit/devtools/test_bead_landing_check.py
-- 54 passed, including a new labelled-evaluation regression block that
runs the real pipeline (real git history, real .beads/issues.jsonl, no
network) against the six named false positives and asserts none regress
back to a strong-confidence LIKELY-STALE verdict. ruff check/format and
mypy --strict clean. devtools verify --quick exit 0. devtools render all
--check: no drift.

Co-Authored-By: Claude <noreply@anthropic.com>
@Sinity

Sinity commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Closing unmerged. Measured against the 190-bead human-verified ground truth (5 independent review groups, complete VERDICT set in bd notes — see PR body "Measured against ground truth"):

  • Precision of the corrected tool's 66 flagged beads: 6.1% overall (20.0% at strong confidence, 3.6% at weak) — a real improvement over the pre-fix ~5%, but not a usable one.
  • Recall: 4 of 7 confirmed STALE beads still flagged (57.1%; 44.4% against the coordinator's reported 9). The three downgrade-only fixes provably removed genuine STALE beads (polylogue-4fm3, polylogue-6pii, polylogue-7mtf) via the same mechanisms meant to filter false positives — most concretely, the suppression-phrase check's "xfail" keyword (added to catch polylogue-hg97) fired on polylogue-7mtf's own unrelated use of the same word, suppressing a genuine positive.

This matches the coordinator's stated decision framework for "precision is still poor" / "recall collapsed on the fixes' own mechanism": close unmerged rather than merge a tool that produces a headline count nobody should trust.

The 190 labelled verdicts are the durable asset from this investigation. They already survive independent of this PR, as bd notes on the reviewed beads themselves (VERDICT: STALE|PARTIAL|LIVE plus evidence, queryable via bd show <id> or bd sql "SELECT id, notes FROM issues WHERE notes LIKE '%VERDICT%'") — no further action needed to preserve them.

If this direction is revisited, the promising kernel per the coordinator's read (which the data supports) is note-staleness detection — has a bead's own most-recent note been contradicted by newer master state — rather than commit-graph archaeology. The current suppression-phrase check is a first, not-yet-safe attempt at that; its one demonstrated false negative (7mtf) shows a lexical keyword match isn't sufficient on its own.

Co-Authored-By: Claude noreply@anthropic.com

@Sinity Sinity closed this Jul 31, 2026
@Sinity
Sinity deleted the feature/devtools/bead-landing-check branch August 10, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant