Repository navigation
fix(recording-tools): resolve ffprobe instead of hardcoding the Homebrew path - #2370
Conversation
…rew 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
Coverage Gate✅ No Python changes — nothing to gate (bar: 95% on changed lines). |
|
Verified, not just read — LGTM.
CI: |
john-the-dev
left a comment
There was a problem hiding this comment.
Blocking on exact head 9b957c3d: the implementation does not contain the Intel Homebrew layout the PR says it fixes. ffprobeCandidates() only maps ffmpegSubtitleCandidates(), whose literals are bare ffmpeg, /opt/homebrew/bin/ffmpeg, /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg, and the runtime sibling. There is no /usr/local/bin/ffmpeg, so the mapped list has no /usr/local/bin/ffprobe. On an Intel launchd/service environment where /usr/local/bin is absent from PATH, duration probing still fails exactly as before.
Please add the Intel candidate at the shared source (or otherwise resolve it explicitly) and commit direct ffprobeCandidates() regressions covering bare PATH, Apple-Silicon Homebrew, Intel Homebrew, bundled sibling, and the anchored ffmpeg-full directory case. The existing 3 focused tests pass, but they exercise only ffmpegSubtitleCandidates() and currently call the two Apple-Silicon formulas “both Homebrew locations,” so they cannot catch this omission.
Verified: focused suite 3/3, full tsc --noEmit, repository review scanner, and git diff --check all pass. Worst-case disruption is this PR landing as an Intel portability fix while remaining dependent on an interactive PATH that the service may not have. Not merge-ready.
|
Changes requested on current head 9b957c3 (formal REQUEST_CHANGES is unavailable because the authenticated account owns this PR). Blocking finding: the new resolver still misses the Intel Homebrew path that this PR says it fixes. That leaves the common GUI-launched Intel/Homebrew case broken: if Focused verification in an isolated /private/tmp checkout: Reviewed by Qingyun's Personal Codex. |
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
|
Fixed in Root cause matches your read exactly. On the misleading test name: you were right that it was load-bearing, not cosmetic. Regressions added, all five cases you asked for, asserted against Reverting only One thing I did NOT include, and want your call on. I first added the Intel The I left it out and documented why inline, rather than bundle a Suites: |
|
Approval signal on current head No blockers found. The Intel Homebrew omission is fixed at the shared Focused verification passed:
At last check the refreshed GitHub Reviewed by Qingyun's Personal Codex. |
john-the-dev
left a comment
There was a problem hiding this comment.
Blocking on exact head 96f09af9fe7973db4c83fcda119c7b0ae7cfb2f3: the added absolute ffprobe candidates are never reached.
At src/recording-tools.ts:84-85, ffprobeCandidates() starts with bare ffprobe, and the finder predicate accepts every bare name immediately via !p.includes("/"). Therefore .find(...) always returns the first candidate, ffprobe, without testing /opt/homebrew/..., /usr/local/bin/ffprobe, or the bundled sibling. In the GUI/launchd case where PATH lacks ffprobe—the exact scenario this PR targets—the duration probe still fails and is silently swallowed at lines 817-824.
Exact-head reproduction with only the Intel candidate marked present:
{"candidates":["ffprobe","/opt/homebrew/bin/ffprobe","/usr/local/bin/ffprobe"],"existing":["/usr/local/bin/ffprobe"],"selected":"ffprobe"}Please prefer an existing absolute candidate and use bare ffprobe only as the final PATH fallback (or reorder with equivalent semantics), then add a shipped-resolver regression proving an existing Intel/Apple/bundled absolute path is actually selected rather than merely present in ffprobeCandidates().
Focused evidence otherwise passes: recording candidates 9/9, full tsc --noEmit, git diff --check, repository hardcoded-path scan, and all GitHub CI/CLA. The current tests validate list membership but not selection, which is why they miss the blocker. Not merge-ready.
…re 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>
|
Fixed at Extracted the selection into a pure, exported Locked with tests — the existing suite only checked the candidate LIST, never the finder. Re-review when you have a moment. |
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed exact head e43300cea8e97ad853c271cdffb663d9d81200ae; the prior resolver-selection blocker is fixed.
selectFfprobe() now checks existing slash-qualified candidates before accepting the bare PATH name. The committed regression proves the exact PATH-less Intel case: absent arm64 candidates plus existing /usr/local/bin/ffprobe selects the Intel binary, while no existing absolute falls back to bare ffprobe.
Independent verification passed: focused suite 13/13, npm run typecheck, child-only delta inspection from 96f09af9, git diff --check, repository review scanner, and explicit added-line host-path scan. GitHub CI and CLA are green. No remaining blocker found.
|
Approval signal for No blockers found. The latest head fixes the resolver-ordering bug: Focused verification in
GitHub CI and CLA are green on this head. Ready from this review lane; the branch is still behind Reviewed by Qingyun's Personal Codex. |
john-the-dev
left a comment
There was a problem hiding this comment.
Approved exact head f13f820e1893ef37256c42fdf7d327938aac8c67 after refresh onto current main.
The two PR-owned blobs are byte-identical to previously reviewed e43300ce; the added commit is merge-only. Current-head verification:
npx tsx --test tests/recording-tools-ffmpeg.test.ts— 13/13npm run typecheck— PASS- PR diff through
scripts/review-checks.sh— PASS git diff --check origin/main...HEAD— clean
The selector still reaches an existing Intel/Apple/bundled absolute ffprobe candidate before falling back to the bare PATH name. Fresh CI/CLA is running; one more qualifying exact-head maintainer approval remains required.
|
Code approval signal on current head No PR-code blockers found. The refresh onto current Focused/local verification passed in isolated
CI note: GitHub's refreshed Reviewed by Qingyun's Personal Codex. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Code approval signal for exact head f13f820e1893ef37256c42fdf7d327938aac8c67 (formal self-approval is unavailable because the authenticated account owns this PR).
The refresh is merge-only and both PR-owned blobs are byte-identical to reviewed e43300ce. The resolver still prefers an existing absolute Intel/Apple/bundled ffprobe before the bare PATH fallback. Reviewer-run checks pass: focused resolver suite 13/13, npm run typecheck, current diff check, and repository path scan; CLA is green and GitHub reports the head mergeable.
Required CI is currently red in tsc + tests (clean install): the TypeScript/typecheck stages passed, but the aggregate Python standalone step exited 1. I did not attribute that Python-only failure to this two-file TypeScript diff, but this head is not merge-ready until the required CI run is rerun or diagnosed green. No code blocker found in the PR-owned change.
|
CI: |
|
Rechecked exact head The two attempts did not fail in different modes; both point to
The relevant blobs are byte-identical between this PR and current
The PR diff remains only Merge gate remains unchanged: required CI is red and the PR still lacks a second qualifying formal approval, so it is not merge-ready. I would not use another blind rerun as a substitute for fixing or isolating this test teardown. |
|
Following up on the teardown-segfault hypothesis for There is no asynchronous work in this test's code path to tear down.
So a live non-daemon thread or an unreaped child at interpreter shutdown does not look like the mechanism here, and What I could not determine, stated plainly: I ran the file three times on this host and got exit 0 each time with the normal success line — but this host is Darwin and the failing job is Concrete way to convert this from hypothesis to fact, and it is cheap: a segfault that arrives after a clean success line is a crash during interpreter finalization, and Python will name the frame if asked. Running that one file under I agree with the scope conclusion regardless: the failing test, |
|
The unrelated clean-install segfault is now isolated in #2388. |
|
Correcting my previous comment — it was wrong, and @john-the-dev's telemetry-teardown hypothesis is right. I read a pre-#2274
The mechanism, now that I've traced it properly:
A daemon thread terminated inside a C-level socket/SSL call during interpreter shutdown is a well-known source of exactly this signature: a clean success line, then a segfault or a nonzero exit with no traceback. It also explains the non-determinism you documented — whether it crashes depends on where each sender thread happens to be when finalization starts, which is why it passed once and returned nonzero on the duplicate sweep two minutes later. The docstring for Neither the test nor Concrete fix, which is the isolation you said you'd prefer over another rerun: set I'd still run one job with This likely is not isolated to this PR: #2357 just went red on the same My apologies for the noise on the first pass; the scope conclusion I gave still holds (this PR's diff touches only |
|
Mechanism now verified rather than hypothesised, so nobody needs to keep chasing this. I drove the three Twilio handlers against So the chain is real: Why this reproduces on CI and not on a maintainer laptop: my host has an opt-out marker in the durable data dir, so Fix staged ( Two consequences worth flagging:
|
amkunte
left a comment
There was a problem hiding this comment.
Reviewed from a fork's checkout. No blocker. Two things I verified that go a bit past what the body claims, and one sharpening of the asymmetry you flagged.
The regex is correct for the whole reachable input domain, not just the four shown
The body demonstrates the anchor on four paths. What actually matters is stronger: is the swap valid for every string ffprobeCandidates() can receive? Since the diff shows it maps ffmpegSubtitleCandidates(execPath) and nothing else, that domain is closed and enumerable. Ran it:
ffmpeg -> ffprobe (bare, PATH)
/opt/homebrew/bin/ffmpeg -> /opt/homebrew/bin/ffprobe (arm64 brew)
/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -> /opt/homebrew/opt/ffmpeg-full/bin/ffprobe (the one that matters)
/usr/local/bin/ffmpeg -> /usr/local/bin/ffprobe (Intel, added here)
<node dir>/ffmpeg -> <node dir>/ffprobe (bundled sibling)
/opt/ffmpeg/bin/ffmpeg -> /opt/ffmpeg/bin/ffprobe (mine: ffmpeg ALSO a parent dir)
All correct. I also found the one input that would break it — a suffixed basename:
/opt/homebrew/bin/ffmpeg-full -> /opt/homebrew/bin/ffprobe-full (resolves nothing)
That is unreachable as written, and worth saying why rather than just noting it: no list entry has a suffixed basename, and — the part I checked specifically — ffprobeCandidates() derives from the list, not from the resolved binary, so $FFMPEG_SUBTITLE_BIN (which short-circuits at :49-50 and returns a user-supplied path directly) never feeds the swap. If a future change ever routes the resolved ffmpeg through this mapping, that case becomes live. Not a request — the current shape is the one that keeps it unreachable, and that seems worth a line in the docstring so the constraint survives a refactor.
The arm64/Intel asymmetry is a "when it was added" artifact, not a property of the paths
Your note says the gate flags /opt/ as a substring, so the Intel ffmpeg-full path trips it while /usr/local/bin/ffmpeg does not. Confirmed, and here is the mechanism: the gate is diff-scoped — scripts/lint-workspace-resolution.sh:88 branches on --diff, and its own comment at :29 says it runs that way "so existing offenders stay un-flagged until" touched. The already-listed /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg at recording-tools.ts:41 contains /opt/ and survives purely because it predates the lint.
So the two halves of the symmetry are treated differently based on when each line was added, not on anything about the paths. That makes your decision to defer it to #2369 correct rather than merely cautious — the fix is an allowlist scope, not a code change here. (Worth knowing for anyone re-litigating it: running the linter bare takes an early exit 0, so a clean bare run is not evidence either way; CI invokes it with --diff.)
Cleared, so noting rather than raising
Existence-only check with no capability probe is right — any ffprobe reports a duration, and probing would add startup cost for nothing. Keeping a bare name in the list preserves PATH resolution. And the positive-control on your own scanner run (planting a /Users/ path to confirm it fires) is the right shape; a scanner that returns clean is only meaningful if you have shown it can return dirty.
|
Approval signal on current head No PR-code blockers found. This head is a merge-only refresh from the previously reviewed Focused verification passed in an isolated
GitHub CI was still running on this refreshed head at review time, so merge should still wait for required checks to finish green and for a qualifying current-head approval. Reviewed by Qingyun's Personal Codex. |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Merge-readiness: ready to merge (contingent on the maintainer's own two-approval gate — see note at bottom).
Verified against current head dc77ddde (HEAD is a merge-only sync of main; confirmed via gh api repos/sonichi/sutando/compare/e43300ce...dc77ddde --jq '.files[].filename' — zero files under src/recording-tools.ts or tests/recording-tools-ffmpeg.test.ts in that range, so the reviewed code is byte-identical to e43300ce, which @john-the-dev already formally approved after two resolved REQUEST_CHANGES rounds).
Lesson 1 — bug confirmed on main:
$ gh api "repos/sonichi/sutando/contents/src/recording-tools.ts?ref=main" --jq '.content' | base64 -d | grep -n ffprobe
776: '/opt/homebrew/bin/ffprobe',
Hardcoded Apple-Silicon path, unconditional, no PATH/Intel/bundled-runtime fallback — matches the described bug.
Lesson 2 — whole activated path:
ffprobeCandidates()mapsffmpegSubtitleCandidates()via/ffmpeg(?=[^/]*$)/→ffprobe. Traced the anchoring by hand against/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg: the first literal "ffmpeg" (insideffmpeg-full) fails the lookahead because a/follows before end-of-string, so the regex engine advances to the trailing/ffmpegand rewrites only that segment →.../ffmpeg-full/bin/ffprobe, notffprobe-full. Correct.selectFfprobe()now triesp.includes('/') && exists(p)before the bare-name fallback — this actually fixes a second, independently-caught bug (the oldfind((p) => !p.includes('/') || existsSync(p))short-circuited on the leading bareffprobeand never reached the absolute candidates at all — dead code on exactly the Intel/PATH-less install this PR targets). Good catch, correctly fixed and tested.- Call site (
src/recording-tools.ts:833-839at head):execFileSync(findFfprobe(), …)is inside atry { … } catch {}—findFfprobe()itself can't throw (regex.replace,existsSyncnever throws on a missing path), so this is fail-safe either way; worst case on a total miss is the pre-fix behavior (duration_secondsstays unset), never worse. - Added
/usr/local/bin/ffmpegalso flows intofindFfmpegWithSubtitles()(shared candidate list) — a real, welcome side benefit (Intel Homebrew now gets subtitle-ffmpeg discovery too), not scope creep — it's the same "no second list to drift" design the PR argues for.
Lesson 3 — fix is proven, not just happy-path: tests/recording-tools-ffmpeg.test.ts — selectFfprobe — "does not short-circuit on the leading bare name when an absolute exists" is a genuine regression test for the dead-code bug found mid-review: it fails against the original find((p) => !p.includes('/') || existsSync(p)) and passes against the current implementation. tsc + tests (clean install) is green on dc77ddde.
Lesson 4/5 — blast radius / worst case: No destructive or auto-remediating action. Sole effect is which ffprobe binary a duration-probe shells out to, itself wrapped in a swallowing catch {}. Worst case if this PR were subtly wrong: duration_seconds stays '?' in the recording result — identical degradation to pre-fix behavior on unsupported layouts, never a regression. Opt-in improvement, no config/migration surface.
Lesson 6 — hardcoded-path scan, independently re-run (not just trusting the PR body's self-reported scan):
$ bash scripts/review-checks.sh --diff pr2370.diff
review-checks: PASS (hardcoded-paths clean)
Positive-control check (does the scanner actually fire on a real violation, per this repo's own past false-negative history): inserted const leak = "/Users/bassil/x"; into the src/recording-tools.ts hunk of the same diff —
review-checks: FAIL — hardcoded-paths:
src/recording-tools.ts:37: hardcoded path (/Users/bassil/x): const leak = "/Users/bassil/x";
Scanner correctly discriminates. The one new literal this PR does add, /usr/local/bin/ffmpeg, is a standard Unix prefix (not user/host-specific) and doesn't match any of REVIEW.md's flagged substrings — consistent with the pre-existing /opt/homebrew/... lines already in this file. No violation.
CI: all 16 checks green on dc77ddde (gh pr checks 2370), CLA signed, mergeable: MERGEABLE.
Nit (non-blocking): mergeStateStatus is currently BEHIND — main has moved since the last sync merge (dc77ddde). Not a code issue, just needs a final rebase/merge before landing.
Prior REQUEST_CHANGES status: @john-the-dev's two rounds of CHANGES_REQUESTED (missing Intel prefix; dead-code short-circuit in the finder) were both root-caused correctly by the author and fixed in 96f09af9 and e43300ce respectively, then formally APPROVED. Confirmed those fixes are still present and untouched at current head.
Approving as a second formal review; per CONTRIBUTING.md's merge gate this still needs the maintainer to confirm two recorded approvals before merging — I have not merged or pushed anything.
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed exact head dc77dddec67b4e448dd6635c17c2611867daa050.
Live-path blocker: this changes the executable selected by the recording duration path, but the body proves only candidate derivation and compilation. Please run the exact head against a real media file with a real ffprobe resolved through one of the newly supported non-hardcoded routes (PATH, Intel Homebrew, or bundled sibling) and paste the actual duration output from the production helper/recording flow. Include the resolved executable so the evidence discriminates this patch from the old /opt/homebrew/bin/ffprobe call.
The unit coverage is green but cannot prove the selected binary executes successfully. @qingyun-wu author action required.
…m, meeting-tools) (#2475) * fix(ts): resolve python instead of hardcoding the Xcode-CLT stub Two TypeScript call sites spawned /usr/bin/python3 directly: skills/zoom/tools.ts:404 execSync(`/usr/bin/python3 -c "…`) Join-button click src/meeting-tools.ts:68 execFileSync('/usr/bin/python3', …) camera toggle On macOS that path is not python — it is the Xcode-CLT stub, one inode hardlinked across python3 / git / swift / swiftc / clang / gcc / make. It exists whether or not the developer tools are installed; spawning it without them raises the modal "install command line developer tools" dialog and returns nothing. The absolute path also cannot be shadowed by a real install on PATH, so a user who installs python via Homebrew still gets the dialog. Adds src/python-binary.ts — the TS twin of src/git_binary.py (#2469) and SutandoConfig.resolvePython (#2473), carrying the same cascade the shell side already uses for $PY: $SUTANDO_PY -> bundle-vendored runtime -> bare `python3` gated on xcode-select -p -> null Tier 3 is deliberately the BARE name, not an absolute path. It lets a real interpreter earlier on PATH win, and it keeps the stub's path out of the module entirely. The xcode-select gate makes it safe: with the tools installed, whatever python3 resolves to is a working interpreter; without them we cannot know, so we decline. (A host with Homebrew python but no developer tools is not reachable in practice — Homebrew's installer requires them.) requirePython() throws PythonUnavailableError. Both call sites already wrap their spawn in `try { … } catch {}`, so an unavailable interpreter degrades through the handling they have rather than needing a new branch — same shape as git_argv in #2469. zoom builds a shell string rather than an argv array, so its path goes through shellQuote: the bundled runtime lives under the app bundle and $SUTANDO_PY is whatever the launcher exported, either of which can contain spaces. selectPython and bundledPythonCandidates are exported and dependency-injected so every tier is testable without touching the host toolchain — the pattern selectFfprobe established in #2370. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(src-map): index the new python-binary module scripts/gen-src-map.py regenerated after adding src/python-binary.ts — the lint-src-map CI check refuses a docs/src-map.md that is stale vs src/, and it also fails tests/src-map.test.py inside the clean-install job (which is why both checks were red for one cause). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ts): correct the bundled-runtime path; probe both call-site shapes Addresses both P1s from @john-the-dev's review. [P1 bundled-runtime path] bundledPythonCandidates appended the full relative path 'runtime/python/bin/python3' to BOTH dirname(execPath) and dirname(dirname(execPath)), doubling the segment: execPath /Applications/Sutando.app/Contents/Resources/runtime/bin/node emitted …/Resources/runtime/bin/runtime/python/bin/python3 …/Resources/runtime/runtime/python/bin/python3 wanted …/Resources/runtime/python/bin/python3 Neither exists, so on a packaged host WITH a vendored python but no $SUTANDO_PY the resolver fell through to the xcode-select tier and returned null without CLT — silently killing both GUI actions on exactly the install this module exists to serve. The vendored tree is <Resources>/runtime/{bin/node, python/bin/python3}, so python is a sibling of node's bin/, one level above dirname(node); the candidate is now built from the runtime ROOT. The flatter layout (node directly in the runtime root) is kept as a second candidate, and the engine-sibling tier from sutando-config.sh is unchanged. The old test asserted only that candidates ENDED with the relative path, which blessed both malformed forms — my error, and the same weak-assertion shape that let the health-check caller slip through on #2469. Replaced with an exact packaged-layout assertion plus a control that no candidate contains a doubled runtime segment. [P1 activated-path evidence] Both consumers swallow failures in a catch, so a unit-correct resolver can still leave them dead. Added executable probes that run the resolved interpreter through each call site's EXACT shape against a real on-disk packaged layout under a path containing a space: - meeting-tools shape: execFileSync(resolved, ['-c', …]) -> "meet-ok" - zoom shape: execSync(`${shellQuote(resolved)} -c …`) -> "zoom-ok" - the same zoom command WITHOUT shellQuote throws, proving the quoting is load-bearing rather than decorative - selectPython picks the vendored python from that layout with toolsInstalled false — the no-CLT case Tests 15 -> 21. A live Zoom/Meet confirmation is still outstanding and remains noted in the PR body. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(python-binary): make the probe fixture hermetic (no ambient python3) @john-the-dev's P1: the "no-CLT" fixture was `#!/bin/sh\nexec python3 "$@"`, which on the very clean Mac this PR targets resolves to the CLT stub. Running the regression suite there could raise the exact modal the fix prevents — and under a python-less PATH both probes failed outright (reviewer-run: 19/21). The fake interpreter now echoes the argument after `-c`, proving the resolved path was executed and received its args intact without any interpreter being installed. All three probes additionally run with `env: { PATH: '/nonexistent' }` so the hermeticity is enforced, not merely intended. Reviewer's negative control, this head: PATH=/nonexistent node --test --import tsx tests/python-binary.test.ts ℹ tests 21 ℹ pass 21 ℹ fail 0 (was: pass 19, fail 2) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ts): use a real PATH interpreter without requiring developer tools Owner feedback: the point of this module is to route interpreter lookup through ONE place, not to withhold an interpreter that works. selectPython collapsed tiers 3 and 4 into `toolsInstalled() ? 'python3' : null`, so it declined a perfectly good interpreter whenever the CLT was absent. The comment justifying that generalised from Homebrew — which does require the tools — to every install method. Not true: a python.org framework install (/usr/local/bin/python3 -> /Library/Frameworks/Python.framework/…, present on the maintainer's machine) or a pyenv build is entirely CLT-independent. It was also inconsistent with select_git in src/git_binary.py (#2469), which already asks the right question: is what PATH gave me actually the stub? Only that one is gated. Now mirrors it: $SUTANDO_PY -> bundled runtime -> PATH python3 if NOT the stub -> the stub only when xcode-select -p succeeds -> null isSystemStubPath compares the resolved DIRECTORY rather than the exact stub path — that keeps the flagged literal out of this file (so this PR stays clear of #2474's gate and carries no merge-order dependency) and also covers a versioned sibling in the same system location. pythonOnPath walks PATH on the filesystem rather than shelling out: spawning anything to find an interpreter risks spawning the stub, which is the failure mode being fixed. Tests 21 -> 25, including the case that motivated this — a non-stub PATH python with toolsInstalled deliberately unreachable, so a regression that consults it fails loudly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
|
Live-path evidence at exact head Discrimination from the old hardcoded call, both directions:
Media: |
qingyun-wu
left a comment
There was a problem hiding this comment.
Current-head approval signal for 77cabb4efc2d4d1c1d21871d46acbc6517f36bd1 (formal self-approval is unavailable because the authenticated account owns this PR).
The head update is merge-only and both PR-owned blobs remain byte-identical to the previously reviewed topic version. Reviewer-run focused resolver coverage, diff hygiene, REVIEW.md path checks, and hosted TypeScript/clean-install/CLA gates pass. The author's exact-head live evidence now executes the shipped helper against a real media file through both derived-absolute and bare-PATH routes, with both returning 28.228208 and the production rounding producing 28 seconds. I inspected that evidence but did not independently repeat the real-media run.
Worst-case disruption is losing recording duration metadata on supported non-Apple-Silicon layouts; the resolver remains inside the existing fail-safe call path and the live execution discriminates it from the old hardcoded binary. Code-ready from this lane, but not merge-ready while john-the-dev's formal REQUEST_CHANGES remains recorded; it must be replaced or dismissed before merge.
Reviewed by Qingyun's Personal Codex.
…ode-CLT shim (#2469) * fix(health-check,agent-api): resolve git instead of hardcoding the Xcode-CLT shim On macOS /usr/bin/git is not git — it is the Xcode Command Line Tools shim, one inode hardlinked across git / python3 / swiftc / clang / gcc / make. The file exists on every Mac whether or not the tools are installed, and invoking it without them pops the modal "install command line developer tools" dialog and returns nothing. Two call sites invoked it by ABSOLUTE path: src/health-check.py _file_unchanged_since() git log / git diff src/agent-api.py GET /activity git log --oneline Absolute means PATH cannot shadow it, so a user who installs a real git (Homebrew, git-scm.com, a static build) still gets the dialog. health-check runs on a timer, so on a clean machine the dialog re-appears indefinitely. Adds src/git_binary.py: resolve_git() prefers any non-shim git on PATH, falls back to the system git only when `xcode-select -p` reports an installed developer directory (the one probe that does not trigger the dialog, already the pattern in src/migrate.sh), and otherwise returns None so callers degrade. Both callers use git for optional provenance, so degrading is correct — a health check must not be able to raise a modal system dialog. select_git() is split out and takes an injected clt_installed callable so the ordering is testable without touching the host toolchain, mirroring selectFfprobe() in src/recording-tools.ts (#2370). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(git-binary): raise GitUnavailable so callers need no new branch Follow-up to the previous commit on this branch, addressing the 95% Python diff-coverage gate and simplifying both call sites. Adds GitUnavailable(FileNotFoundError) + git_argv(). Because GitUnavailable is an OSError, the "no runnable git" case flows through error handling both callers ALREADY have: health-check.py except (subprocess.TimeoutExpired, OSError, ValueError) -> False agent-api.py except Exception -> pass So each call site is now a one-line change with no added branch, and the whole decision lives in git_binary.py where it is cheap to unit-test. Previously each caller carried its own `if git is None:` guard — two untested branches in modules that are expensive to exercise. Tests: 11 -> 19. Adds resolve_git() (including its cache), git_argv() both ways, the OSError-subclass contract that the callers depend on, and health-check._file_unchanged_since in both states (git absent -> False; real git -> runs both invocations without raising). Diff coverage vs origin/main: 96% (30 lines, 1 missing — agent-api.py:104, the import line, which the ten existing agent-api tests cover in CI). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(src-map): index the new git_binary module scripts/gen-src-map.py regenerated after adding src/git_binary.py — the lint-src-map CI check refuses a docs/src-map.md that is stale vs src/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(health-check,git-binary): route the branch probe; stop caching a negative Addresses both current-head review findings. [@john-the-dev, P1] check_live_checkout_branch() still ran a bare `git -C … branch --show-current`, and it is registered UNCONDITIONALLY. On a Mac without developer tools PATH resolves that to /usr/bin/git — the stub — and the modal fires BEFORE the return-code degradation below it can run, on every health pass. The source-tied test only scanned the two originally-changed call sites, so it passed while this activated caller stayed broken. Now routed through git_argv; the existing `except (OSError, TimeoutExpired)` already returns the correct "git not runnable — skipping", so this is a one-line change with no new branch. [@sonichi] resolve_git() no longer caches a negative answer. @lru_cache memoised both outcomes, which is right for health-check.py (re-exec'd on a timer) but wrong for agent-api.py — a long-lived serve_forever() server calling it inside do_GET, where "user installs the tools mid-run" is the expected case, plausibly *because* something told them to. A cached None left GET /activity permanently empty until restart while health-check on the same host reported git as fine. Positive answers are still cached; a negative is re-probed. As noted in review, this ordering is unreachable by a post-restart test, so it is pinned by a unit test instead (test_install_after_start_is_picked_up). Tests 19 -> 22. Diff coverage 97% (37 lines, 1 missing — agent-api.py's import line, covered in CI by the ten existing agent-api suites). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(git-binary): walk every PATH candidate, not just the first @john-the-dev's P1. `shutil.which("git")` returns only the FIRST match, so a stub-first PATH — /usr/bin ahead of a real install, which service PATHs routinely are — handed back the stub. On a no-CLT host select_git() then returned None while a perfectly good git sat further along, contradicting this module's own documented order ("Any git on PATH that is not the system shim"). Reproduced with a symlink-to-stub first and a real git later: candidates: …/first/git -> /usr/bin/git …/later/git -> …/later/git before: resolved = None after: resolved = …/later/git (clt_installed never consulted) path_candidates() now enumerates every executable `git` on PATH in order, and select_git() walks them: first runnable non-stub wins; the stub is REMEMBERED and returned only when the developer tools are installed (which makes it a working git), never discarded outright. select_git's first argument changes from a single resolved path to the ordered candidate list — internal to this module, and the reason the old signature could not express "try the next one". Tests 22 -> 27: the stub-first/later-real ordering, the stub-as-fallback case, and path_candidates' PATH walk (order, empty entries, non-executables). ruff clean; diff coverage 100%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(git-binary): assert the probe spawns nothing, not one path's wording #2471 landed a second degradation path in check_live_checkout_branch: when resolve_git() returns None it returns BEFORE the try:, so the detail reads "no runnable git (resolver)" rather than the OSError path's "git not runnable". The old assertion pinned the latter string and failed on a merge that had made the degradation strictly earlier and safer. Assert the behaviour instead: status stays ok AND subprocess.run is never called. A spawn is what raises the CLT modal, so counting execs is the only instrument that sees the bug this test exists to catch — a wording check could not have. Negative control: forcing the probe back to git_bin = "git" fails the test ("warn" != "ok"); restoring resolve_git() passes 27/27. * docs(src-map): regenerate after the main refresh (183 -> 184) The merge auto-resolved the count line to main's 183 while the merged tree holds 184 modules (main added presenter_mode.py, this branch adds git_binary.py). Both entries were present; only the header count was stale, so nothing conflicted and nothing flagged it. Regenerated rather than hand-edited. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…non-checkout (#2478) * fix(health-check,agent-api): resolve git instead of hardcoding the Xcode-CLT shim On macOS /usr/bin/git is not git — it is the Xcode Command Line Tools shim, one inode hardlinked across git / python3 / swiftc / clang / gcc / make. The file exists on every Mac whether or not the tools are installed, and invoking it without them pops the modal "install command line developer tools" dialog and returns nothing. Two call sites invoked it by ABSOLUTE path: src/health-check.py _file_unchanged_since() git log / git diff src/agent-api.py GET /activity git log --oneline Absolute means PATH cannot shadow it, so a user who installs a real git (Homebrew, git-scm.com, a static build) still gets the dialog. health-check runs on a timer, so on a clean machine the dialog re-appears indefinitely. Adds src/git_binary.py: resolve_git() prefers any non-shim git on PATH, falls back to the system git only when `xcode-select -p` reports an installed developer directory (the one probe that does not trigger the dialog, already the pattern in src/migrate.sh), and otherwise returns None so callers degrade. Both callers use git for optional provenance, so degrading is correct — a health check must not be able to raise a modal system dialog. select_git() is split out and takes an injected clt_installed callable so the ordering is testable without touching the host toolchain, mirroring selectFfprobe() in src/recording-tools.ts (#2370). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(git-binary): raise GitUnavailable so callers need no new branch Follow-up to the previous commit on this branch, addressing the 95% Python diff-coverage gate and simplifying both call sites. Adds GitUnavailable(FileNotFoundError) + git_argv(). Because GitUnavailable is an OSError, the "no runnable git" case flows through error handling both callers ALREADY have: health-check.py except (subprocess.TimeoutExpired, OSError, ValueError) -> False agent-api.py except Exception -> pass So each call site is now a one-line change with no added branch, and the whole decision lives in git_binary.py where it is cheap to unit-test. Previously each caller carried its own `if git is None:` guard — two untested branches in modules that are expensive to exercise. Tests: 11 -> 19. Adds resolve_git() (including its cache), git_argv() both ways, the OSError-subclass contract that the callers depend on, and health-check._file_unchanged_since in both states (git absent -> False; real git -> runs both invocations without raising). Diff coverage vs origin/main: 96% (30 lines, 1 missing — agent-api.py:104, the import line, which the ten existing agent-api tests cover in CI). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(src-map): index the new git_binary module scripts/gen-src-map.py regenerated after adding src/git_binary.py — the lint-src-map CI check refuses a docs/src-map.md that is stale vs src/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(health-check,git-binary): route the branch probe; stop caching a negative Addresses both current-head review findings. [@john-the-dev, P1] check_live_checkout_branch() still ran a bare `git -C … branch --show-current`, and it is registered UNCONDITIONALLY. On a Mac without developer tools PATH resolves that to /usr/bin/git — the stub — and the modal fires BEFORE the return-code degradation below it can run, on every health pass. The source-tied test only scanned the two originally-changed call sites, so it passed while this activated caller stayed broken. Now routed through git_argv; the existing `except (OSError, TimeoutExpired)` already returns the correct "git not runnable — skipping", so this is a one-line change with no new branch. [@sonichi] resolve_git() no longer caches a negative answer. @lru_cache memoised both outcomes, which is right for health-check.py (re-exec'd on a timer) but wrong for agent-api.py — a long-lived serve_forever() server calling it inside do_GET, where "user installs the tools mid-run" is the expected case, plausibly *because* something told them to. A cached None left GET /activity permanently empty until restart while health-check on the same host reported git as fine. Positive answers are still cached; a negative is re-probed. As noted in review, this ordering is unreachable by a post-restart test, so it is pinned by a unit test instead (test_install_after_start_is_picked_up). Tests 19 -> 22. Diff coverage 97% (37 lines, 1 missing — agent-api.py's import line, covered in CI by the ten existing agent-api suites). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sutando-config): don't spawn git for the runtime descriptor on a non-checkout The `runtime` subcommand built its `code` block with five git spawns whose own comment already says "best-effort (None when not a git checkout)" — but it implemented that by SPAWNING git and catching the failure. On macOS that is exactly backwards: /usr/bin/git is the Xcode-CLT stub, and spawning it raises the modal install dialog BEFORE it can fail, so catching a non-zero exit is too late. This is the caller still firing on a packaged install. The desktop app polls this descriptor (ag2space-cinny-desktop src-tauri/src/core_terminal.rs:255), and the packaged engine is an rsync copy with NO .git — so all five calls returned None regardless and existed only to leak the dialog. Same shape as the startup.sh committer.* block #2459 identified. Resolves once, before any spawn, behind two gates: 1. no .git -> not a checkout; every field would be None anyway 2. no runnable git -> resolve_git() from src/git_binary.py (#2469), which prefers a real install and never returns the stub Measured on an rsync copy with .git excluded, git shimmed to count execs: BEFORE (origin/main): 5 git spawns AFTER (this fix): 0 git spawns A real checkout is unaffected — commit/branch/describe/tree_sha/dirty still populate (verified on this branch: commit f477f7c, dirty True). Stacked on #2469: uses resolve_git(). Merge after it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(git-binary): walk every PATH candidate, not just the first @john-the-dev's P1. `shutil.which("git")` returns only the FIRST match, so a stub-first PATH — /usr/bin ahead of a real install, which service PATHs routinely are — handed back the stub. On a no-CLT host select_git() then returned None while a perfectly good git sat further along, contradicting this module's own documented order ("Any git on PATH that is not the system shim"). Reproduced with a symlink-to-stub first and a real git later: candidates: …/first/git -> /usr/bin/git …/later/git -> …/later/git before: resolved = None after: resolved = …/later/git (clt_installed never consulted) path_candidates() now enumerates every executable `git` on PATH in order, and select_git() walks them: first runnable non-stub wins; the stub is REMEMBERED and returned only when the developer tools are installed (which makes it a working git), never discarded outright. select_git's first argument changes from a single resolved path to the ordered candidate list — internal to this module, and the reason the old signature could not express "try the next one". Tests 22 -> 27: the stub-first/later-real ordering, the stub-as-fallback case, and path_candidates' PATH walk (order, empty entries, non-executables). ruff clean; diff coverage 100%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sutando-config): a linked worktree's .git is a FILE, not a directory @john-the-dev's P1. The checkout gate used os.path.isdir, so a linked worktree or submodule — whose .git is a FILE containing a `gitdir:` pointer — was treated as a non-checkout and lost commit/branch/describe/tree_sha. That contradicted this PR's own "byte-identical on a checkout" claim and would blank the desktop app's runtime identity/attach diagnostics for worktree-based installs, which are a supported shape. os.path.exists covers both, and still spawns nothing when the marker is absent — the property the original change exists for. Reproduced and fixed in a detached linked worktree (.git confirmed a FILE): before: {'commit': None, 'branch': None, 'describe': None, 'tree_sha': None, 'dirty': False} after: {'commit': '564dc96', 'branch': 'HEAD', 'describe': 'v0.9.0-44-g564dc96-dirty', 'tree_sha': '3c60692a…'} Adds tests/sutando-config-runtime-git.test.sh pinning all three shapes, as the review asked: no .git marker -> 0 git spawns, valid JSON, empty code block linked worktree -> commit + tree_sha populated ordinary checkout -> commit populated The shim counts EXECS rather than exit codes: the dialog is raised by the spawn itself, so a test asserting on return values passes while the bug is fully present. That is the same mistake the original code made. Also merges the parent (#2469) so the PATH-walking resolve_git is in scope. shellcheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(git-binary): assert the probe spawns nothing, not one path's wording #2471 landed a second degradation path in check_live_checkout_branch: when resolve_git() returns None it returns BEFORE the try:, so the detail reads "no runnable git (resolver)" rather than the OSError path's "git not runnable". The old assertion pinned the latter string and failed on a merge that had made the degradation strictly earlier and safer. Assert the behaviour instead: status stays ok AND subprocess.run is never called. A spawn is what raises the CLT modal, so counting execs is the only instrument that sees the bug this test exists to catch — a wording check could not have. Negative control: forcing the probe back to git_bin = "git" fails the test ("warn" != "ok"); restoring resolve_git() passes 27/27. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> 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.
Verified current head 77cabb4e — it is byte-identical to the exact commit I formally APPROVED on 07-31 (gh api repos/sonichi/sutando/pulls/2370/reviews shows my prior review's commit_id matches headRefOid exactly). Independently reran tests/recording-tools-ffmpeg.test.ts (13/13 green) and, to confirm the fix is proven rather than happy-path-only, reverted selectFfprobe() to the pre-fix find((p) => !p.includes('/') || exists(p)) short-circuit locally: the "does not short-circuit on the leading bare name when an absolute exists" test fails against that old logic (expected '/usr/local/bin/ffprobe', got 'ffprobe') and passes against the current code — genuine discrimination, not just green-on-both. Diff stays scoped to src/recording-tools.ts + its test; the call site is still inside the pre-existing catch {}, so worst case on a resolver miss is unchanged pre-fix behavior (duration_seconds unset), never a regression. No hardcoded user/host paths added. CI is fully green (16/16 checks + CLA) on this head.
Merge-readiness: not merge-ready as a formal matter — GitHub's reviewDecision is still CHANGES_REQUESTED from @john-the-dev's review submitted 08-01 asking for live-media execution proof. That evidence was posted afterward (qingyun-wu's 08-02 comment, run against the same unchanged blobs), but the CHANGES_REQUESTED review itself hasn't been dismissed or superseded. I don't find a remaining code defect, but a maintainer/john-the-dev needs to clear that stale review state before this can land — not stacking another approval on top of my existing one.
|
@john-the-dev — you asked for a real Candidate list the PR produces, in order: Three runs, each executing the resolved binary on the media file: B and C are routes the old hardcoded call could not have taken, and both returned a real duration from a real file — so the evidence discriminates. This exercises the production composition, not a lookalike: Three limits, stated so you can weigh the evidence rather than take it:
One thing worth noting in passing: candidate 0 is the bare name and sits first, and case A still resolved to the absolute Comment-only; no approval authority here, and dropping your block stays your call. Stand: Echo Act IV Mini |
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 77cabb4efc2d4d1c1d21871d46acbc6517f36bd1 after the new live evidence.
No blocking finding. The production duration call now reaches the derived selector; the absolute-candidate-before-bare ordering, Intel prefix, bundled sibling, anchored ffmpeg-full rename, and PATH fallback are covered by 13/13 focused checks, and the full TypeScript typecheck passes. The REVIEW.md path scan and diff hygiene are clean.
The new real-media evidence directly closes the prior runtime-discrimination gap: with the arm64 absolute path denied, the bundled sibling executes and returns duration=65.045000; with all absolute candidates denied, the bare PATH route executes and returns the same duration. Those are routes the old hardcoded /opt/homebrew/bin/ffprobe call could not take. The stated limitation—no genuine Intel host—is acceptable because the exact Intel selection branch is independently pinned and the alternate live routes prove the production composition does not stop at the old path.
Code-ready from this review lane. Formal approval is unavailable because the authenticated account owns this PR; the stale external request/current-head maintainer gate still needs repository-side resolution.
john-the-dev
left a comment
There was a problem hiding this comment.
APPROVE at 77cabb4e — clearing my 2026-08-01 CHANGES_REQUESTED. It was answered on 08-02 and again on 08-05, and I left it standing. That delay was mine.
My ask was: run the exact head against real media with a real ffprobe resolved through one of the newly supported non-hardcoded routes, and name the resolved executable so the evidence discriminates this patch from the old /opt/homebrew/bin/ffprobe literal.
It is answered, at the exact current head. 77cabb4e is the head the 08-05 capture was run at — I checked rather than assuming (compare 77cabb4e...HEAD → status=identical, ahead 0 / behind 0), so there is nothing between the evidence and what would merge.
Three runs on a real 6.4 MB / 65s mp4:
- [A] real host →
/opt/homebrew/bin/ffprobe→duration=65.045000 - [B]
/opt/homebrewdenied → bundled runtime sibling →duration=65.045000 - [C] all absolute candidates denied → bare
ffprobevia PATH →duration=65.045000
B and C are exactly the discrimination I asked for — neither route was reachable from the old hardcoded literal, and both executed a real binary against a real file rather than proving candidate derivation. The 08-02 capture independently covers the Apple-Silicon + PATH pair at dc77ddde with the same shape.
It also exercises the production composition rather than a lookalike: findFfprobe() is selectFfprobe(ffprobeCandidates(process.execPath), existsSync), which is what the duration call site uses, and that is what was invoked.
My two earlier blockers (missing Intel candidate; bare ffprobe short-circuiting before absolute candidates were tested) were both fixed before this and I had already approved those. Nothing further from me.
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 690398cb1660e3884345e576e8ff270759eb1b53 after the current-main merge. Changes requested; not merge-ready.
The refresh is merge-only: its first parent is previously reviewed/code-ready 77cabb4e, its second parent is current main 041f7c5f, and range-diff preserves all three topic commits. The resolver behavior remains sound: the 13-case focused suite passes, absolute candidates are tried before the bare PATH name, the Intel and bundled candidates remain reachable, the anchored basename substitution preserves ffmpeg-full, and the repository hardcoded-path scan is clean.
[P2 repository contract] The cumulative added comments/docstrings violate the current two-line/no-history rule. Representative production blocks are src/recording-tools.ts:41-46, :50-56, :61-75, and :82-90; the test carries the same issue at tests/recording-tools-ffmpeg.test.ts:30-33, :47-52, :77-79, and :90-94. These include defect/review/PR history and exceed two lines. Please keep only each load-bearing invariant in at most two lines and move the incident/review narrative to the PR body.
Because the authenticated account owns this PR, GitHub does not permit a formal self-request-changes review; this COMMENT is the blocking verdict. Hosted clean-install CI was still pending at review time.
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-checked merge-main head 690398cb1660e3884345e576e8ff270759eb1b53. The PR diff is still scoped to src/recording-tools.ts and tests/recording-tools-ffmpeg.test.ts, and I do not see a new blocker in the ffprobe resolution change.
Focused verification:
npm ci --ignore-scriptscompleted in the isolated worktreenpx tsx --test --test-force-exit tests/recording-tools-ffmpeg.test.ts(13/13 passed)npm run typecheckpassedgit diff --check origin/main...HEADpassedbash scripts/lint-workspace-resolution.sh --diff origin/main...HEADpassed
I am not posting an approval signal on this head because the required tsc + tests (clean install) CI check is currently red: tests/task-workstream-session-worker.test.py fails in test_required_team_handler_shutdown_never_falls_through because TASK_FILE: appears in stdout. That looks outside this PR's ffprobe diff, but it still leaves the head non-green.
Reviewed by Qingyun's Personal Codex.
The duration probe in the recording flow invoked the Apple-Silicon Homebrew
ffprobepath directly, so on an Intel or PATH-only install it threw and the recording reported no duration.Same class as #2368's screen-record fix, different layer — this is
src/core, so it's a separate change rather than widening that PR's scope. It was found while re-auditing after a reviewer showed my "only file in the repo" claim there was wrong.Derived, not restated
ffmpegSubtitleCandidates()already lists the prefixes this repo supports — PATH, both Homebrew layouts, and the bundled-runtime sibling of the running node.ffprobeCandidates()maps that list to its ffprobe siblings, so there's one place to add a prefix when a new install layout appears and no second copy to drift. ffprobe ships beside ffmpeg in every distribution that has it.The basename swap is anchored
/ffmpeg(?=[^/]*$)/— so a directory component named ffmpeg is left alone:The third is the one that matters: a naive replace yields
ffprobe-fulland resolves nothing.Unlike
findFfmpegWithSubtitlesthis doesn't 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.Independent of #2369
Adds no new path literal, so unlike #2368 this doesn't wait on the 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:That control exists because my first run of this check scanned an empty diff — I'd diffed
origin/main...HEADwith the change still uncommitted — and reported "clean". A clean result on no input proves nothing, and the added-line count is what caught it.tsc --noEmitclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01DZ5eEAmPuFnSA2TkqtQhzZ