dev.py digest: fold tools/pr/digest.py's required sid/tracked-prs/label args into the front door nf-wpu ← Beads

open priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/1065 by tools/beads/import_github.py's reconcile pass.

Aedan's ask, verbatim (chat, 2026-08-18)

> "does this differ from dev.py check" / "should digest.py be a dev command > instead? and not need to take tr[a]cked-prs, sid etc"

Context

./tools/pr/digest.py <tracked-prs.txt> <sid> <session-label> is a dev's one-shot read of every PR/issue feedback channel (digest.py:1-51's own docstring). All three positional arguments are required, with no default — deliberately, since design 006 S10 killed the silent defaults that used to resolve tracked-prs.txt against the CWD and $SID/$SESSION_LABEL against nothing (digest.py:763-778).

That refusal is correct — a guessed sid is worse than none — but it is also exactly the shape ./dev.py's front door exists to close. `design_docs/006- the-coders-front-door.md` §5.2's verb table (line ~1500) already names dev.py status as replacing *"poll.py's 3 args"* for the queue-oracle use case, and tools/nflib/devcli.py's own module docstring states the rule directly: **"the sid, the label, the state dir and the ledger path are read from the worktree"** (§5.5, §6.3 item 1) — enforced, not just documented: `tools/tests/test_nflib_devcli.py::VerbSignatureTest.test_no_verb_accepts_a_ session_specific_option` walks every registered subparser and fails the build if one accepts an option or positional naming a session, sid, label, state path or ledger (test_nflib_devcli.py:98-123).

DevSession (devcli.py:182-209) already carries every field digest.py needs — .session (the label), .sid, .tracked (the ledger path) — the exact resolution verb_status already uses (devcli.py:1765: sessions.read_tracked_prs([session.tracked])). So the three required arguments are not actually unknowable at the call site inside a dev worktree; they are known and simply not being passed.

This is narrower than dev.py status absorbing digest.py's job — don't conflate the two. status (devcli.py:1743-1787) already reimplements the *queue-oracle* half of digest.py's value (per-PR merge_pr.py --why, plus the same nflib.ledger unanswered-reply predicate digest.py uses, per digest.py:67-69's own comment) as a compact, ready-set-first view. What status does not do — and what remains digest.py's unique job — is the full verbatim read of every channel (CLAUDE.md: *"a compact grep once hid real aedanpope comments for hours"* — the read-it-whole guarantee) and the issue-tracker section (every open issue, its CLAIM/RELEASE marker, PR coverage). This issue is only about removing the CLI friction on that still-needed capability, not about re-scoping status.

Prescription

Add a dev.py digest verb to tools/nflib/devcli.py that thinly wraps tools/pr/digest.py, resolving its three required arguments from the already-resolved DevSession instead of the command line:

- Register it in parser()'s subparser block (devcli.py:2098-2112 is the function; check's registration at :2114-2115 is the minimal-case pattern to copy). No arguments beyond what check takes — this verb is exactly as argument-free as check/status, by the same rule they follow. Also add a "digest": verb_digest entry to the VERBS dict (devcli.py:2081-2092) — test_nflib_devcli.py:104 (test_the_scan_finds_every_verb) cross-checks VERBS against the registered subparser names and fails the build if they drift apart. - verb_digest(args, session, *, runner=_run, echo=None, cwd=Path.cwd), matching every other verb's signature. Refuse exactly like verb_pr does when not session.resolved (devcli.py:1178,1206 is the precedent) — digest.py cannot answer "is this yours" without an identity, so neither can this verb. - Shell out, streamed, the same way gate() shells out to presubmit.py (devcli.py:502-513) rather than importing tools.pr.digesttools/pr/ is not a package (the same reason engine.py's _publish_authority_check shells out to perimeter_check.py instead of importing it). Run `[sys.executable, ROOT/"tools"/"pr"/"digest.py", str(session.tracked), session.sid, session.session], stream=True`, check=False, and return the child's exit code — digest.py's ~390-line output is meant to be read as it prints (CLAUDE.md: *"read the output whole"*), the same reason gate() streams rather than captures. - Do not add an --issues-only equivalent. That mode is explicitly for a reader that is *not* a session (/triage, per digest.py:783-797's own comment) and has no worktree to resolve a DevSession from — it keeps calling ./tools/pr/digest.py --issues-only directly, unchanged. - Update CLAUDE.md's Feedback section (the literal three-argument invocation, named digest.py there since #1030's rename — still poll.py if #1030's PR 4/#1059 has not merged yet when this is picked up, check the live file rather than assuming) to ./dev.py digest, and sweep the same handful of other current-tense mentions of the three-argument invocation (notes/operating.md:132 and its three poll.py/digest.py prose mentions at :37,55,80,111, design_docs/006's own table entry if it still names the old form).

Principles: C1 (the command should predict what it does — dev.py digest next to dev.py check/status/pr reads the same way those already do), B7 (the fix is a verb that makes "which three arguments" impossible to get wrong, not a reminder to run the substitution command correctly), B10 (reuse DevSession/gate()'s subprocess pattern — this is the third caller of "shell out to a tools/pr/*.py script, streamed", after check and merge's job-launch, not a new mechanism).

Test plan

- tools/tests/test_nflib_devcli.py: a DigestVerbTest class, same shape as PrVerbTest/CheckVerbTest — the plumbing command dev.py digest runs names session.tracked/.sid/.session verbatim (via FakeRunner, matching test_it_creates_the_pr_on_main_and_pushes_with_an_upstream's style of asserting on runner.commands()); an unresolved session refuses with session.reason, same shape as CheckVerbTest's unresolved case; the child's exit code comes back unchanged (ExitCodeTest's class, one more instance of it). - VerbSignatureTest.test_no_verb_accepts_a_session_specific_option already covers the new subparser once it exists — no new test needed there, just don't add an argument that trips it. - Live (E5): ./dev.py digest from a real dev worktree prints the same report `./tools/pr/digest.py "$(./tools/pr/session_state.py tracked-prs.txt)" <sid> <label>` does, byte-for-byte modulo the session values being resolved instead of typed.

PR shape

One PR. Mechanical wrapper over an existing, unchanged tool — no behaviour in digest.py itself changes (E3: this is the "extraction/plumbing" shape, not a behaviour PR).

Non-goals

- Not re-scoping dev.py status — it stays the compact queue view; dev.py digest stays the full-read/issue-tracker view. Two different jobs, per the Context section above. - Not touching --issues-only / the /triage non-session path. - Not retiring ./tools/pr/digest.py as a directly-callable script — dev.py digest is a resolving wrapper over it, the same relationship dev.py check has with presubmit.py --all ("presubmit.py --all still runs directly whenever you want to", per dev.py's own epilog at devcli.py:2110-2111).

Open questions (defaults marked — nothing blocks)

1. Verb name — default dev.py digest. feedback is already taken (friction logging, devcli.py:1987); digest matches the tool's own name and its "one-shot digest" self-description. 2. Streamed vs captured output — default streamed (matches check). Capturing and re-printing would add nothing and risks losing partial output on a digest.py crash mid-run, the same argument gate()'s own docstring makes for streaming presubmit.

Dependencies

None.

Comments

No comments.

Add a comment