Twinned from https://github.com/cfsh/ninefold/issues/1065 by tools/beads/import_github.py's reconcile pass.
> "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"
./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.
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.digest — tools/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).
- 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.
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).
- 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).
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.
None.
No comments.