Phase 0 — Gates: cross-platform CI matrix + regression fixture + release smoke - #71
Conversation
Phase 0 research synthesizes the cross-platform CI matrix, frozen omnivoice_data fixture, installer post-build smoke, SHA-256 checksum publishing, and PR-template extension into copy-paste-ready YAML and Python snippets composed entirely from existing in-repo patterns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 0 — Gates is the hard pre-condition for v0.3.x stabilization. Lays cross-platform CI matrix (macos-14/windows-2022/ubuntu-22.04), regression fixture (≤200 KB), installer smoke on tag push, SHA-256 checksums in release body + per-OS SHA256SUMS-*.txt assets, PR template with RC cadence + fixture line, and the open-PR landing for #51. Plan covers GATE-01..06; structured into 7 slices (A–G) with explicit Slice C → Slice G dependency reordering so the new smoke-matrix lands on main before PR #51 (CONTEXT.md L86 interleave decision). Plan-checker iteration 2: APPROVED — all 3 BLOCKERs + 3 MAJORs from iteration 1 resolved (file truncation/Slice-G missing, GATE-06 sibling PR verification, Slice C ordering, Truth #5 wording, macOS Tauri WebView avoidance per Pitfall #5, Windows taskkill per Pitfall #2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/seed-test-fixture.py — deterministic builder for tests/fixtures/omnivoice_data/
- wipes + rebuilds; fixed created_at=1700000000.0; all-zero PCM for byte-deterministic diffs
- calls backend.core.db.init_db() directly (alembic versions/ is empty — see CONTEXT.md)
- checkpoints WAL → DELETE on close so no -shm/-wal sidecars pollute git status
- exits non-zero if fixture > 200 KB
- tests/fixtures/omnivoice_data/{omnivoice.db, README.md} — 8-table empty DB + 1 voice_profiles row
- tests/fixtures/omnivoice_data/voices/test-voice/{profile.json, sample.wav} — 1-sec 24 kHz mono silence
- .gitignore — explicit allow-list (!tests/fixtures/omnivoice_data/**) so the existing
omnivoice_data/, *.db, *.wav patterns don't hide the fixture from git
Verifies: du = 144 KB on disk; sqlite_master lists 8 init_db tables + sqlite_sequence;
voice_profiles has exactly 1 row id='test-voice'; 0 rows in generation_history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- tests/smoke/__init__.py — package marker so pytest treats tests/smoke/ as a module
- tests/smoke/test_boot_smoke.py — 4 in-process FastAPI TestClient smoke tests:
* test_health_returns_ok — /health returns 200 + {status:ok, device:...}
* test_profiles_endpoint_lists_fixture_voice — /profiles surfaces the seeded
test-voice row (validates OMNIVOICE_DATA_DIR wiring → DB_PATH → init_db schema)
* test_system_info_includes_data_dir — /system/info resolves data_dir
* test_history_endpoint_empty — /history reaches DB and returns []
Test isolation env vars (OMNIVOICE_MODEL=test, OMNIVOICE_DISABLE_FILE_LOG=1)
set at module top BEFORE any backend import — pattern from tests/test_router_smoke.py.
Fixture is copied to a per-session temp dir so the test never mutates the
checked-in artifact (SQLite file-change counter + runtime subdirs like dub_jobs/
would otherwise dirty `git status` after every run).
Failure mode: if tests/fixtures/omnivoice_data/ is missing, pytest.fail at
import time with the regenerate command.
- .gitignore — tighten the GATE-01 allow-list to ONLY the seed-produced files
(README.md, omnivoice.db, voices/test-voice/profile.json, sample.wav).
Prevents future runtime subdirs the backend may create under the fixture
from being accidentally committed.
Verifies: `uv run pytest tests/smoke/ -q --tb=short` → 4 passed in 1.31 s
(target was < 30 s). `git status` clean after a test run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… OOS deferrals - GATE-06: mark #53 + #61 merged (2026-05-16); add #62 (Wave 1 quick wins) to gate set - INST-01: note PR #62 implements setuptools pin (closes #58) - INST-04: note PR #62 lands README docs for #56 workaround - INST-12: new requirement for #65 Windows Triton/torch.compile OOM (filed post-planning) - Out of Scope: defer #67/PR #68 (audio effects), #64 (custom model dir), PR #66 zh-CN (i18n milestone), #63 (empty-template bug) PR #62 is the user's own Wave 1 work landed as a separate PR while GSD planning ran in parallel. Merging it eliminates duplicate work in Phase 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New smoke-matrix job on macos-14, windows-2022, ubuntu-22.04 - needs: test, fail-fast: false, timeout-minutes: 10 - Pinned actions: checkout@v4, setup-python@v5, setup-uv@v3 (cache enabled) - Per-OS ffmpeg + libsndfile install (brew/choco/apt via awalsh128 cache) - UV_HTTP_TIMEOUT=120, UV_HTTP_RETRIES=5 for restricted-network resilience - Narrow scope: uv run pytest tests/smoke/ -q --tb=short - Existing `test` and `tauri-cross-platform` jobs untouched Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…TE-03) - argparse on __main__ block; --health-check boots uvicorn in a daemon thread and polls http://127.0.0.1:3900/health every 5s for up to 60s. - Prints 'OK — /health responded 200 after Ns' and exits 0 on first 200. - Prints 'FAIL — /health did not respond 200 within 60s' to stderr and exits 1 on timeout. Default invocation behavior unchanged. - No new deps (stdlib argparse/threading/time/urllib.request/sys + uvicorn). - Consumed by per-OS installer-smoke step in .github/workflows/release.yml. Verified locally: exits 0 in 5s against tests/fixtures/omnivoice_data/.
Adds three matrix-leg-specific steps after 'Build + release (Tauri)', each gated by runner.os with timeout-minutes: 5: - macOS (macos-14): hdiutil attach DMG → locate bundled Python backend inside *.app/Contents (NOT the Tauri WebView shell — RESEARCH Pitfall #5: WebView hangs on headless runners) → invoke --health-check → hdiutil detach. Falls back to *.app/Contents/Resources and hard-fails with a directory listing if no backend binary found. - Windows (windows-2022): msiexec /quiet install → find backend.exe under 'C:/Program Files/OmniVoice Studio' → invoke --health-check in background, wait, then taskkill //F //T //PID to cleanup orphaned PyInstaller child processes on port 3900 (RESEARCH Pitfall #2). - Linux (ubuntu-22.04): --appimage-extract (no FUSE on GH runners), locate binary or AppRun, run under xvfb-run -a. Bundle-only regressions (PyInstaller missing-module, Tauri sidecar path mismatch) are invisible to ci.yml's in-process smoke matrix — this step closes that gap before any release is published. Verified: YAML parses; all three steps present; gating + timeout correct; Pitfall #2/#5 mitigations preserved.
…ATE-05) - Add 'Compute SHA-256 checksums' step writing SHA256SUMS-<label>.txt per matrix leg using native shasum/sha256sum (Git Bash on Windows). - Add 'Append checksums to release + attach SHA256SUMS file' step using softprops/action-gh-release@v2 with append_body: true so the hashes land in the release body alongside tauri-action's content (not replacing it) and the file is uploaded as a release asset for 'shasum -c SHA256SUMS-<label>.txt' verification. - Both steps gated by 'github.event_name == push && refs/tags/v*' so workflow_dispatch dry-runs do not attempt to attach to a non-existent release (per CONTEXT.md L70 + RESEARCH Pitfall #7 deferral of any aggregate cross-leg SHA256SUMS job). - fail_on_unmatched_files: true to surface path-resolution errors loudly.
…template (GATE-04)
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Phase 0 Gates: a deterministic regression fixture and PR-blocking in-process smoke tests, backend --health-check mode, CI smoke-matrix, installer post-build health checks and SHA-256 publishing, PR template/gitignore/config tweaks, frontend pill/launch persistence and pill-launch support, and extensive planning/research documentation. ChangesPhase 0 Gates Implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (3)
.planning/research/STACK.md (2)
139-156: ⚡ Quick winAdd language identifier to pseudocode block.
The pseudocode block would benefit from a language identifier for syntax highlighting.
📝 Proposed fix
-```python +```python # Pseudocode for the bootstrap MIRRORS = [🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.planning/research/STACK.md around lines 139 - 156, The fenced pseudocode block for MIRRORS (the triple-backtick block containing MIRRORS, try_uv_venv, env and the bootstrap logic) is missing a language identifier; update the opening fence to include "python" (i.e., change ``` to ```python) so the pseudocode is syntax-highlighted and clearly marked as Python in the STACK.md content.
70-70: ⚡ Quick winAdd language identifier to code block.
The code block is missing a language specifier.
📝 Proposed fix
If this is meant to be a bash/shell example, add the language identifier:
-``` +```textOr if this is meant to show actual shell code:
-``` +```bash🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.planning/research/STACK.md at line 70, The fenced code block ending with ``` is missing a language identifier; update the opening fence (the matching ```) to include the appropriate language tag (e.g., ```bash for shell commands or ```text for plain text) so the block is syntax-highlighted; look for the code fence markers (```) surrounding the snippet and add the language specifier to the opening fence..planning/research/SUMMARY.md (1)
22-27: 💤 Low valueAdd language identifier to ASCII diagram.
The code fence for the build order diagram is missing a language specifier.
📝 Proposed fix
-``` +```text Phase 0 (gates) → Phase 1 (token+docs+UI) → Phase 2 (engine isolation, IndexTTS)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.planning/research/SUMMARY.md around lines 22 - 27, The fenced ASCII diagram in .planning/research/SUMMARY.md is missing a language identifier; update the opening code fence for the diagram (the triple-backtick that precedes the diagram block) to include a language tag such as "text" (i.e., change ``` to ```text) so the block is explicitly marked as plain text and leave the closing ``` unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/pull_request_template.md:
- Around line 38-45: The PR template has an OS-count mismatch: the phrase
"clean-VM exercise on 4 OSes (per `REL-01`)" conflicts with the PR
objectives/smoke-matrix that list "macOS, Windows, and Linux"; either change the
parenthetical phrase to "clean-VM exercise on 3 OSes" or add the missing fourth
platform to the smoke-matrix/PR objectives so both places match; update the
string "clean-VM exercise on 4 OSes (per `REL-01`)" or the smoke-matrix list
accordingly to keep the template consistent.
In @.github/workflows/ci.yml:
- Around line 220-236: The CI workflow has inconsistent cross-platform deps and
a suppressed macOS install failure: the "System deps (macOS)" step currently
appends "|| true" (remove this so brew failures fail the job) and the "System
deps (Windows)" step only installs ffmpeg via choco but omits libsndfile; update
the Windows job "System deps (Windows)" to install libsndfile using an
alternative (e.g., install Miniconda/conda and conda install -c conda-forge
libsndfile, or use vcpkg to install libsndfile) and verify ffmpeg, ensuring the
step uses bash/powershell accordingly and fails on error.
In @.github/workflows/release.yml:
- Around line 434-435: The find invocation used to set BIN incorrectly applies
-type f only to the first -name due to operator precedence; update the command
so -type f applies to both patterns by grouping the name tests, e.g. change the
assignment in BIN to use: find squashfs-root -type f \( -name "OmniVoice Studio"
-o -name "omnivoice-studio" \) 2>/dev/null | head -1 (use escaped parentheses as
shown) so both "OmniVoice Studio" and "omnivoice-studio" are constrained to
files.
- Around line 492-500: The release append step using softprops/action-gh-release
with append_body: true and files: ${{ steps.checksums.outputs.checksums_file }}
is causing race conditions when multiple matrix legs run in parallel; fix by
serializing those updates—either add a GitHub Actions concurrency group keyed by
the tag (e.g., concurrency: group: release-${{ github.ref_name }}) on the job
that runs softprops/action-gh-release so only one runner appends at a time, or
refactor to a two-step flow where matrix jobs produce checksums and a single
follow-up job (depends-on all matrix jobs) performs the append/upload to
softprops/action-gh-release. Ensure the job referencing append_body and
steps.checksums.outputs.checksums_file is the only one allowed to run the append
step.
- Around line 368-369: The MOUNT extraction truncates volume names with spaces
because awk's default field separator splits on whitespace; update the hdiutil
parsing that sets MOUNT so it preserves tabs and captures the full mount path
(e.g., change the awk invocation used when assigning MOUNT to use a tab FS and
print the last field, or use a sed/cut variant that extracts everything after
the final tab), ensuring the MOUNT variable contains the full "/Volumes/..."
path before the subsequent find "$MOUNT" call.
In @.planning/phases/00-gates/PLAN.md:
- Around line 201-202: The verify command in the PLAN.md automated block
hardcodes a machine-specific absolute path
(/Users/user4/Desktop/voice-design/OmniVoice); update the command(s) to use
repo-relative paths or a repository root variable (e.g., $REPO_ROOT) instead of
the hardcoded path so the verify steps are portable; apply the same change to
the other occurrences referenced (lines around 242-243, 347-348, 371-372,
524-525, 640-641, 748-749, 819-821, 906-907) and ensure commands like the
seed-test-fixture.py invocation and sqlite3 checks use relative paths (or
$REPO_ROOT) so they work across macOS, Windows, and Linux.
- Around line 561-565: The snippet uses the non-portable/deprecated find
predicate "-perm +111" to populate BACKEND and similar searches and also relies
on "mapfile -t" elsewhere; replace calls that search for executable files (the
BACKEND assignment and the PyInstaller fallback that use -perm +111) with a
POSIX-friendly pattern that finds files and tests executable bit via "-type f
-exec test -x {} \; -print -quit" (or equivalent find + test combo) to ensure
portability across macOS/Linux/Windows runners, and replace any uses of "mapfile
-t" with a portable while-read loop (e.g., while IFS= read -r line; do ...;
done) or a read loop that collects lines into an array so the rest of the code
that references the same variable names still works; update the code paths that
set BACKEND and the block referenced as the mapfile usage to use these
replacements.
In @.planning/ROADMAP.md:
- Around line 40-47: Mismatch between REL-01’s four-target OS list and the PR
template / CI description: update the PR template or CI matrix so they’re
consistent by either (A) explicitly listing the four platforms referenced in
REL-01 (macOS Sequoia, Windows 11, Ubuntu 24.04, Fedora 44) or (B) stating that
the smoke-matrix CI covers three runners (macOS, Windows, Ubuntu) while release
verification (REL-01) additionally exercises Fedora; modify the PR template text
and any `smoke-matrix` CI description to clearly state which platforms are part
of PR smoke tests versus release verification and reference REL-01 and the “PR
template” block so reviewers can see the alignment.
In `@backend/main.py`:
- Around line 470-480: The loop currently increments elapsed by INTERVAL_S which
ignores time spent in urllib.request.urlopen(timeout=2) and can exceed
TIMEOUT_S; change to use wall-clock timing by recording a start = time.time()
and replace the condition with while time.time() - start < TIMEOUT_S, compute
elapsed = int(time.time() - start) for messages, and keep the same try/except
around urllib.request.urlopen(HEALTH_URL, timeout=2) and the same sleep of
INTERVAL_S to avoid busy-waiting (refer to symbols: elapsed, TIMEOUT_S,
HEALTH_URL, INTERVAL_S, urllib.request.urlopen).
In `@CLAUDE.md`:
- Around line 76-77: The two orphaned comment lines "Pseudocode for the
bootstrap" and "Final fallback: don't download Python at all" are outside any
fenced block in CLAUDE.md; either delete them if redundant, or move them into a
proper fenced code block (triple backticks) or convert to regular
prose/rephrased sentences so they render correctly—look for those exact strings
to locate and update them.
- Around line 104-115: The 4-column technology table header "Technology |
Version | Purpose | Why Recommended" has been corrupted by an inserted 2-column
"Project | What they do" table; remove or move the "Project" table so the
4-column table contains only its intended rows and the header separator (the ---
line) has four columns of separators, then place the "Project | What they do"
table after the technology table as its own separate table; ensure each table
uses consistent pipe separators and that the technology table rows align with
the four headers while the Project table remains a distinct 2-column block.
- Around line 26-36: The markdown tables in CLAUDE.md were merged incorrectly;
split them into two valid Markdown tables by ending the 4-column "Technology |
Version | Purpose | Why Recommended" table with a blank line after its final
row, then create a new 2-column table with its own header row and separator
(e.g., "| Shell | One-liner to persist `HF_TOKEN` |" followed by
"|-------|---------------------------------|") and move the macOS/Linux/Windows
rows under that header; remove the stray merged separator row and ensure each
table has matching header and separator lines so rendering is restored.
In `@tests/smoke/test_boot_smoke.py`:
- Line 43: The global _FIXTURE_COPY created via tempfile.mkdtemp leaks temp
directories; replace it with a session-scoped pytest fixture (e.g.,
`@pytest.fixture`(scope="session") def fixture_copy()) that creates the tempdir
(use tempfile.TemporaryDirectory() or tempfile.mkdtemp()), yields Path(tmpdir)
to tests, and performs cleanup (shutil.rmtree or TemporaryDirectory.__exit__) in
teardown; update usages of the global _FIXTURE_COPY to accept the new fixture
parameter in tests (reference symbol: _FIXTURE_COPY and the new fixture name
like fixture_copy) so tempdirs are removed after the test session.
---
Nitpick comments:
In @.planning/research/STACK.md:
- Around line 139-156: The fenced pseudocode block for MIRRORS (the
triple-backtick block containing MIRRORS, try_uv_venv, env and the bootstrap
logic) is missing a language identifier; update the opening fence to include
"python" (i.e., change ``` to ```python) so the pseudocode is syntax-highlighted
and clearly marked as Python in the STACK.md content.
- Line 70: The fenced code block ending with ``` is missing a language
identifier; update the opening fence (the matching ```) to include the
appropriate language tag (e.g., ```bash for shell commands or ```text for plain
text) so the block is syntax-highlighted; look for the code fence markers (```)
surrounding the snippet and add the language specifier to the opening fence.
In @.planning/research/SUMMARY.md:
- Around line 22-27: The fenced ASCII diagram in .planning/research/SUMMARY.md
is missing a language identifier; update the opening code fence for the diagram
(the triple-backtick that precedes the diagram block) to include a language tag
such as "text" (i.e., change ``` to ```text) so the block is explicitly marked
as plain text and leave the closing ``` unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8f996ba4-55ef-49bf-a7c0-5abcf11361ed
⛔ Files ignored due to path filters (2)
tests/fixtures/omnivoice_data/omnivoice.dbis excluded by!**/*.dbtests/fixtures/omnivoice_data/voices/test-voice/sample.wavis excluded by!**/*.wav
📒 Files selected for processing (25)
.github/pull_request_template.md.github/workflows/ci.yml.github/workflows/release.yml.gitignore.planning/PROJECT.md.planning/REQUIREMENTS.md.planning/ROADMAP.md.planning/STATE.md.planning/config.json.planning/phases/00-gates/00-PATTERNS.md.planning/phases/00-gates/CONTEXT.md.planning/phases/00-gates/PLAN.md.planning/phases/00-gates/RESEARCH.md.planning/research/ARCHITECTURE.md.planning/research/FEATURES.md.planning/research/PITFALLS.md.planning/research/STACK.md.planning/research/SUMMARY.mdCLAUDE.mdbackend/main.pyscripts/seed-test-fixture.pytests/fixtures/omnivoice_data/README.mdtests/fixtures/omnivoice_data/voices/test-voice/profile.jsontests/smoke/__init__.pytests/smoke/test_boot_smoke.py
| ## Release cadence (read once per RC) | ||
|
|
||
| OmniVoice ships every minor on a **two-RC cadence**: | ||
| - `vX.Y.0-rc1` — cut from `main` once all GATE-* requirements pass; clean-VM exercise on 4 OSes (per `REL-01`) | ||
| - 48-hour soak (no new commits to release branch except fix-forward) | ||
| - `vX.Y.0` — promotion if rc1 is clean | ||
|
|
||
| If your PR touches install / bootstrap / CI, it MUST land before rc1 cut, not between rc1 and the promotion. During a soak, any merge needs explicit OK from the release captain. |
There was a problem hiding this comment.
Clarify OS count discrepancy.
Line 41 mentions "4 OSes" in the clean-VM exercise, but line 35 (and PR objectives) list only 3 platforms: macOS, Windows, and Linux. Either the smoke-matrix should include a fourth OS, or line 41 should say "3 OSes" to match.
Proposed fix
If 3 OSes is correct:
-OmniVoice ships every minor on a **two-RC cadence**:
-- `vX.Y.0-rc1` — cut from `main` once all GATE-* requirements pass; clean-VM exercise on 4 OSes (per `REL-01`)
+OmniVoice ships every minor on a **two-RC cadence**:
+- `vX.Y.0-rc1` — cut from `main` once all GATE-* requirements pass; clean-VM exercise on 3 OSes (per `REL-01`)Or if 4 OSes is correct, update line 35 to list all four platforms.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## Release cadence (read once per RC) | |
| OmniVoice ships every minor on a **two-RC cadence**: | |
| - `vX.Y.0-rc1` — cut from `main` once all GATE-* requirements pass; clean-VM exercise on 4 OSes (per `REL-01`) | |
| - 48-hour soak (no new commits to release branch except fix-forward) | |
| - `vX.Y.0` — promotion if rc1 is clean | |
| If your PR touches install / bootstrap / CI, it MUST land before rc1 cut, not between rc1 and the promotion. During a soak, any merge needs explicit OK from the release captain. | |
| ## Release cadence (read once per RC) | |
| OmniVoice ships every minor on a **two-RC cadence**: | |
| - `vX.Y.0-rc1` — cut from `main` once all GATE-* requirements pass; clean-VM exercise on 3 OSes (per `REL-01`) | |
| - 48-hour soak (no new commits to release branch except fix-forward) | |
| - `vX.Y.0` — promotion if rc1 is clean | |
| If your PR touches install / bootstrap / CI, it MUST land before rc1 cut, not between rc1 and the promotion. During a soak, any merge needs explicit OK from the release captain. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/pull_request_template.md around lines 38 - 45, The PR template has
an OS-count mismatch: the phrase "clean-VM exercise on 4 OSes (per `REL-01`)"
conflicts with the PR objectives/smoke-matrix that list "macOS, Windows, and
Linux"; either change the parenthetical phrase to "clean-VM exercise on 3 OSes"
or add the missing fourth platform to the smoke-matrix/PR objectives so both
places match; update the string "clean-VM exercise on 4 OSes (per `REL-01`)" or
the smoke-matrix list accordingly to keep the template consistent.
| - name: System deps (macOS) | ||
| if: runner.os == 'macOS' | ||
| run: brew install ffmpeg libsndfile || true | ||
|
|
||
| - name: System deps (Windows) | ||
| if: runner.os == 'Windows' | ||
| shell: bash | ||
| run: | | ||
| choco install ffmpeg -y --no-progress | ||
| ffmpeg -version | ||
|
|
||
| - name: System deps (Linux) | ||
| if: runner.os == 'Linux' | ||
| uses: awalsh128/cache-apt-pkgs-action@latest | ||
| with: | ||
| packages: ffmpeg libsndfile1 | ||
| version: 1.0 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if libsndfile is available via chocolatey for Windows
curl -s 'https://community.chocolatey.org/api/v2/Search()?$filter=Id%20eq%20%27libsndfile%27&$orderby=Id' | grep -i libsndfile || echo "Not found in chocolatey"
# Alternative: Check if libsndfile is available via conda-forge or other Windows package managers
echo "Alternative sources for libsndfile on Windows:"
echo "- conda-forge: https://anaconda.org/conda-forge/libsndfile"
echo "- vcpkg: https://github.com/microsoft/vcpkg"Repository: debpalash/OmniVoice-Studio
Length of output: 244
🏁 Script executed:
cd .github/workflows && cat -n ci.yml | sed -n '210,240p'Repository: debpalash/OmniVoice-Studio
Length of output: 1205
Cross-platform dependency parity violation: Windows missing libsndfile.
The macOS and Linux steps install both ffmpeg and libsndfile (lines 222, 235), but the Windows step only installs ffmpeg (line 228). The comment on lines 217-219 explicitly states both are needed for soundfile/audio fixtures. Additionally, libsndfile is not available via Chocolatey, requiring an alternative installation approach (e.g., conda-forge, vcpkg, or prebuilt binaries).
Additionally, line 222 uses || true to silently ignore brew installation failures, which will cause less clear import errors later in tests if dependencies fail to install. Remove the || true to fail fast.
Proposed fix:
- Remove
|| truefrom macOS step (line 222) - Install
libsndfileon Windows via an alternative method (conda-forge, vcpkg, or package manager) since Chocolatey does not provide it
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 220 - 236, The CI workflow has
inconsistent cross-platform deps and a suppressed macOS install failure: the
"System deps (macOS)" step currently appends "|| true" (remove this so brew
failures fail the job) and the "System deps (Windows)" step only installs ffmpeg
via choco but omits libsndfile; update the Windows job "System deps (Windows)"
to install libsndfile using an alternative (e.g., install Miniconda/conda and
conda install -c conda-forge libsndfile, or use vcpkg to install libsndfile) and
verify ffmpeg, ensuring the step uses bash/powershell accordingly and fails on
error.
| MOUNT=$(hdiutil attach -nobrowse -readonly "$DMG" | tail -1 | awk '{print $3}') | ||
| APP=$(find "$MOUNT" -maxdepth 2 -name "*.app" | head -1) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the release.yml file at the specified lines
head -n 375 .github/workflows/release.yml | tail -n 20Repository: debpalash/OmniVoice-Studio
Length of output: 1388
🏁 Script executed:
# Get more context around those lines
head -n 375 .github/workflows/release.yml | tail -n 30Repository: debpalash/OmniVoice-Studio
Length of output: 1869
🏁 Script executed:
# Search for the entire DMG-related section to understand the context
rg -n "hdiutil attach" .github/workflows/release.yml -B 5 -A 10Repository: debpalash/OmniVoice-Studio
Length of output: 1260
🏁 Script executed:
# Check if there are any other mount-related operations in this workflow
rg -n "MOUNT\|DMG\|\.app" .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 52
🌐 Web query:
hdiutil attach output format fields
💡 Result:
The hdiutil attach command provides output designed to be program-readable, consisting of three primary fields separated by tabs [1][2][3]: 1. /dev node: The device identifier (e.g., /dev/disk4) [1][2][3]. 2. Content hint: A description of the partition or content type (if applicable) [1][2][3]. 3. Mount point: The path where the filesystem is mounted (if any filesystems were mounted) [1][2][3]. For more robust, machine-parseable output, it is recommended to use the -plist option [1][2][3]. This returns the information in XML property list format, which can be easily converted to JSON using tools like plutil for reliable parsing in scripts [4]. Note: Users have reported that the standard text output may sometimes contain unexpected whitespace, which can complicate parsing [5]. Using -plist is the preferred method for programmatic access to avoid these issues [4].
Citations:
- 1: https://real-world-systems.com/docs/hdiutil.1.html
- 2: https://ss64.com/mac/hdiutil.html
- 3: https://www.unix.com/man_page/osx/1/hdiutil/
- 4: https://gist.github.com/putnamhill/5aeeeeb273eb57f4ce7b39b49a28a376
- 5: https://discussions.apple.com/thread/256146327
🏁 Script executed:
# Search for any existing handling of mount points or discussions about this in the repo
rg -i "mount\|volume.*space\|hdiutil" .github/workflows/ -B 2 -A 2Repository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Check if there are any comments or documentation about this issue
rg -i "space\|truncate\|awk.*mount\|DMG" .github/workflows/release.yml -B 3 -A 3Repository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Look at the rest of the workflow to understand the full context
wc -l .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 104
🏁 Script executed:
# Create a test to understand awk field behavior with tabs
python3 << 'EOF'
# Simulate hdiutil output with tabs and spaces in mount point
test_output = "/dev/disk4\tApple_partition_scheme\t/Volumes/My Application"
# Test the proposed fix
import subprocess
result = subprocess.run(
['awk', 'END { $1=$2=""; sub(/^ +/, ""); print }'],
input=test_output.encode(),
capture_output=True,
text=True
)
print(f"Input: {repr(test_output)}")
print(f"Output: {repr(result.stdout.strip())}")
# Also test with the original approach for comparison
result2 = subprocess.run(
['awk', '{print $3}'],
input=test_output.encode(),
capture_output=True,
text=True
)
print(f"Original awk $3: {repr(result2.stdout.strip())}")
EOFRepository: debpalash/OmniVoice-Studio
Length of output: 878
🏁 Script executed:
# Test awk field behavior with tabs and spaces in mount point
python3 << 'EOF'
import subprocess
# Simulate hdiutil output with tabs and spaces in mount point
test_output = "/dev/disk4\tApple_partition_scheme\t/Volumes/My Application"
# Test the proposed fix
result = subprocess.run(
['awk', 'END { $1=$2=""; sub(/^ +/, ""); print }'],
input=test_output,
capture_output=True,
text=True
)
print(f"Input: {repr(test_output)}")
print(f"Proposed fix output: {repr(result.stdout.strip())}")
# Also test with the original approach for comparison
result2 = subprocess.run(
['awk', '{print $3}'],
input=test_output,
capture_output=True,
text=True
)
print(f"Original awk $3: {repr(result2.stdout.strip())}")
# Also test what happens when there are actual spaces before field 3
test_output_with_spaces = "/dev/disk4 Apple_partition_scheme /Volumes/My App"
result3 = subprocess.run(
['awk', '{print $3}'],
input=test_output_with_spaces,
capture_output=True,
text=True
)
print(f"\nWith spaces instead of tabs:")
print(f"Input: {repr(test_output_with_spaces)}")
print(f"Original awk $3: {repr(result3.stdout.strip())}")
EOFRepository: debpalash/OmniVoice-Studio
Length of output: 366
Fix macOS mount-point parsing to handle volume names with spaces.
The current awk '{print $3}' truncates mount points containing spaces (e.g., /Volumes/My Application becomes /Volumes/My), causing the subsequent find "$MOUNT" to fail. The issue is verified: awk treats spaces as field delimiters regardless of hdiutil's tab-separated output format.
Proposed fix
- MOUNT=$(hdiutil attach -nobrowse -readonly "$DMG" | tail -1 | awk '{print $3}')
+ MOUNT=$(hdiutil attach -nobrowse -readonly "$DMG" | awk 'END { $1=$2=""; sub(/^ +/, ""); print }')🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 368 - 369, The MOUNT extraction
truncates volume names with spaces because awk's default field separator splits
on whitespace; update the hdiutil parsing that sets MOUNT so it preserves tabs
and captures the full mount path (e.g., change the awk invocation used when
assigning MOUNT to use a tab FS and print the last field, or use a sed/cut
variant that extracts everything after the final tab), ensuring the MOUNT
variable contains the full "/Volumes/..." path before the subsequent find
"$MOUNT" call.
| BIN=$(find squashfs-root -type f -name "OmniVoice Studio" -o -name "omnivoice-studio" 2>/dev/null | head -1) | ||
| if [ -z "$BIN" ]; then |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/release.yml | sed -n '430,440p'Repository: debpalash/OmniVoice-Studio
Length of output: 756
🏁 Script executed:
# Search for context around the find command and any similar patterns
rg "find squashfs-root" -B 2 -A 2Repository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Check if there are any tests or other uses of similar find patterns
rg "find.*-type f.*-o.*-name" -B 1 -A 1Repository: debpalash/OmniVoice-Studio
Length of output: 52
🌐 Web query:
find command operator precedence -type -o -name parentheses
💡 Result:
In the find command, operator precedence determines the order in which expressions are evaluated. Operators are evaluated in the following order, from highest to lowest precedence [1][2][3]: 1. Parentheses: ( expr) [2][3] 2. Negation:! expr or -not expr [2][3] 3. Conjunction: expr1 expr2, expr1 -a expr2, or expr1 -and expr2 [2][3] 4. Alternation: expr1 -o expr2 or expr1 -or expr2 [2][3] 5. List: expr1, expr2 (GNU find only) [2][3] Key points regarding precedence: - Implicit Conjunction: When two expressions are placed next to each other without an explicit operator, find treats them as if they are joined by an implicit -a (AND) operator [4][5][3]. - Higher Precedence of AND: Because the AND operator (explicit or implicit) has higher precedence than the OR (-o) operator, expressions are often grouped differently than one might intuitively expect [4][3]. For example, find. -type f -name apple -o -name orange is evaluated as (find. -type f -name apple) -o (name orange) [4][6]. - Shell Quoting: Parentheses are special characters in most shells. To use them in a find command, you must typically escape them (e.g., ( and )) to prevent the shell from interpreting them before they reach the find command [3][7].
Citations:
- 1: https://stackoverflow.com/questions/24338777/understanding-escaped-parentheses-in-find
- 2: https://www.gnu.org/software/findutils/manual/html_node/find_html/Combining-Primaries-With-Operators.html
- 3: https://man7.org/linux/man-pages/man1/find.1.html
- 4: https://unix.stackexchange.com/questions/276574/operator-precedence-in-a-find-command/276575
- 5: https://www.unix.com/man-page/posix/1p/find?os=posix§ion=1p&query=find
- 6: https://unix.stackexchange.com/questions/276574/operator-precedence-in-a-find-command
- 7: https://linux.die.net/man/1/find
Fix find operator precedence in Linux AppImage binary lookup.
The -o operator has lower precedence than the implicit -a (AND), so the current expression is parsed as (find squashfs-root -type f -name "OmniVoice Studio") -o (-name "omnivoice-studio"). This means the second -name pattern is not constrained by -type f, allowing BIN to resolve to directories or other non-file types.
Proposed fix
- BIN=$(find squashfs-root -type f -name "OmniVoice Studio" -o -name "omnivoice-studio" 2>/dev/null | head -1)
+ BIN=$(find squashfs-root -type f \( -name "OmniVoice Studio" -o -name "omnivoice-studio" \) 2>/dev/null | head -1)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| BIN=$(find squashfs-root -type f -name "OmniVoice Studio" -o -name "omnivoice-studio" 2>/dev/null | head -1) | |
| if [ -z "$BIN" ]; then | |
| BIN=$(find squashfs-root -type f \( -name "OmniVoice Studio" -o -name "omnivoice-studio" \) 2>/dev/null | head -1) | |
| if [ -z "$BIN" ]; then |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 434 - 435, The find invocation
used to set BIN incorrectly applies -type f only to the first -name due to
operator precedence; update the command so -type f applies to both patterns by
grouping the name tests, e.g. change the assignment in BIN to use: find
squashfs-root -type f \( -name "OmniVoice Studio" -o -name "omnivoice-studio" \)
2>/dev/null | head -1 (use escaped parentheses as shown) so both "OmniVoice
Studio" and "omnivoice-studio" are constrained to files.
| - name: Append checksums to release + attach SHA256SUMS file | ||
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | ||
| uses: softprops/action-gh-release@v2 | ||
| with: | ||
| tag_name: ${{ github.ref_name }} | ||
| append_body: true | ||
| body_path: ${{ steps.checksums.outputs.checksums_file }} | ||
| files: ${{ steps.checksums.outputs.checksums_file }} | ||
| fail_on_unmatched_files: true |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the release.yml file
fd -e yml release | head -20Repository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Get the full context around lines 492-500
wc -l .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 104
🏁 Script executed:
# Read the section around the mentioned lines to understand the context
sed -n '450,510p' .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 2216
🏁 Script executed:
# Look for matrix configuration in the workflow
grep -n "matrix:" .github/workflows/release.yml | head -20Repository: debpalash/OmniVoice-Studio
Length of output: 88
🏁 Script executed:
# Search for all occurrences of softprops/action-gh-release in the file
grep -n "softprops/action-gh-release" .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 119
🏁 Script executed:
# View the full matrix configuration
sed -n '100,150p' .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 2115
🏁 Script executed:
# Look for the job definition that contains this step
sed -n '1,120p' .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 4012
🏁 Script executed:
# Find the job name that contains line 494
sed -n '70,120p' .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 1746
🏁 Script executed:
# Check if there are any other steps that might serialize or coordinate the release updates
sed -n '450,500p' .github/workflows/release.yml | grep -i "job\|needs\|step"Repository: debpalash/OmniVoice-Studio
Length of output: 200
🏁 Script executed:
# Look for any other release-related steps or workarounds
grep -n "gh-release\|release\|append" .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 2443
🏁 Script executed:
# Check the full structure around the release step to see if there's any coordination mechanism
sed -n '480,500p' .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 950
🌐 Web query:
softprops/action-gh-release append_body concurrent matrix race condition
💡 Result:
When using softprops/action-gh-release in a concurrent matrix environment, race conditions frequently occur because the action relies on the GitHub API to check for an existing release before creating or updating one [1][2]. Because the GitHub API may not immediately reflect newly created releases, multiple concurrent jobs can simultaneously conclude that no release exists, leading them to attempt to create separate releases for the same tag [1]. This results in errors (such as 422 "already_exists") [2] or the creation of multiple, fragmented releases [1]. Regarding the append_body functionality, the action has historically struggled with consistent behavior when multiple jobs attempt to update the same release body [3][4]. When multiple jobs run in parallel, they may read, modify, and write the release body in a non-atomic way, leading to unexpected overwrites or duplicated content [5][3][4]. To mitigate these race conditions, the following strategies are recommended: 1. Use GitHub Actions Concurrency: Define a concurrency group at the job or workflow level to ensure that only one job interacts with the release at a time [6]. This serializes the release process and prevents concurrent API calls [6]. 2. Two-Step Release Process: Separate the release creation from the asset upload [2]. Create the release (as a draft) in a single, non-matrixed job first [2]. Subsequent matrix jobs can then upload assets to that existing release without needing to create it [3][2]. 3. Conditional Execution: Use an if condition to ensure that only one specific job (e.g., the last one to finish or a designated primary job) handles the finalization or body updates of the release, while others only upload their respective artifacts [3].
Citations:
- 1: Race condition when calling the action multiple times softprops/action-gh-release#705
- 2: Getting 422 "already_exists" error on 2.2.2 softprops/action-gh-release#616
- 3: Cannot disable release body append mechanism when updating an existing release softprops/action-gh-release#57
- 4: Using "body" appends instead of replacing existing body softprops/action-gh-release#150
- 5: The body of a release is overwritten with the content of
body_pathorbodysoftprops/action-gh-release#362 - 6: https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs
Serialize checksum appends to release to prevent concurrent writes.
All three matrix legs run in parallel and call softprops/action-gh-release with append_body: true on the same tag, causing a race condition. The GitHub API does not serialize concurrent updates to the release body atomically, so one leg's appended checksums can be dropped or overwritten by another leg's concurrent update.
Recommended fix: Use GitHub Actions concurrency groups to serialize release body updates, or adopt a two-step process where release creation is separate from asset uploads.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 492 - 500, The release append
step using softprops/action-gh-release with append_body: true and files: ${{
steps.checksums.outputs.checksums_file }} is causing race conditions when
multiple matrix legs run in parallel; fix by serializing those updates—either
add a GitHub Actions concurrency group keyed by the tag (e.g., concurrency:
group: release-${{ github.ref_name }}) on the job that runs
softprops/action-gh-release so only one runner appends at a time, or refactor to
a two-step flow where matrix jobs produce checksums and a single follow-up job
(depends-on all matrix jobs) performs the append/upload to
softprops/action-gh-release. Ensure the job referencing append_body and
steps.checksums.outputs.checksums_file is the only one allowed to run the append
step.
| elapsed = 0 | ||
| while elapsed < TIMEOUT_S: | ||
| try: | ||
| with urllib.request.urlopen(HEALTH_URL, timeout=2) as resp: | ||
| if resp.status == 200: | ||
| print(f"OK — /health responded 200 after {elapsed}s", flush=True) | ||
| sys.exit(0) | ||
| except Exception: | ||
| pass | ||
| time.sleep(INTERVAL_S) | ||
| elapsed += INTERVAL_S |
There was a problem hiding this comment.
Use wall-clock timeout for --health-check instead of interval counting.
elapsed += INTERVAL_S ignores request time spent in urlopen(timeout=2), so this loop can run past the promised 60s gate window.
Proposed fix
- elapsed = 0
- while elapsed < TIMEOUT_S:
+ start = time.monotonic()
+ deadline = start + TIMEOUT_S
+ while time.monotonic() < deadline:
try:
with urllib.request.urlopen(HEALTH_URL, timeout=2) as resp:
if resp.status == 200:
- print(f"OK — /health responded 200 after {elapsed}s", flush=True)
+ elapsed = int(time.monotonic() - start)
+ print(f"OK — /health responded 200 after {elapsed}s", flush=True)
sys.exit(0)
except Exception:
pass
- time.sleep(INTERVAL_S)
- elapsed += INTERVAL_S
+ remaining = deadline - time.monotonic()
+ if remaining > 0:
+ time.sleep(min(INTERVAL_S, remaining))🧰 Tools
🪛 Ruff (0.15.12)
[error] 473-473: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.
(S310)
[error] 477-478: try-except-pass detected, consider logging the exception
(S110)
[warning] 477-477: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` around lines 470 - 480, The loop currently increments
elapsed by INTERVAL_S which ignores time spent in
urllib.request.urlopen(timeout=2) and can exceed TIMEOUT_S; change to use
wall-clock timing by recording a start = time.time() and replace the condition
with while time.time() - start < TIMEOUT_S, compute elapsed = int(time.time() -
start) for messages, and keep the same try/except around
urllib.request.urlopen(HEALTH_URL, timeout=2) and the same sleep of INTERVAL_S
to avoid busy-waiting (refer to symbols: elapsed, TIMEOUT_S, HEALTH_URL,
INTERVAL_S, urllib.request.urlopen).
| | Technology | Version | Purpose | Why Recommended | | ||
| |------------|---------|---------|-----------------| | ||
| | `huggingface_hub` (already pinned transitively by `transformers>=5.3.0`) | `≥1.12.x` (latest 2026) | Auth + cache + token storage | Canonical, used by every HF library already in the stack. `HfFolder` is **superseded** in v1.x by the higher-level `login()` / `auth_list()` / `auth_switch()` API. | | ||
| | `keyring` (Python) | `≥25.x` | Optional OS-keychain backing | Only adopt if a future hardening pass wants Keychain/Credential-Manager/SecretService. **Not recommended for this milestone** — adds a native dep (`dbus`, `pywin32`) per platform with no real security win over `0600` file storage in `HF_HOME`. | | ||
| | Shell | One-liner to persist `HF_TOKEN` | | ||
| |-------|---------------------------------| | ||
| | macOS zsh (default since 10.15) | `echo 'export HF_TOKEN=hf_xxx' >> ~/.zshrc && source ~/.zshrc` | | ||
| | Linux bash | `echo 'export HF_TOKEN=hf_xxx' >> ~/.bashrc && source ~/.bashrc` | | ||
| | Windows PowerShell (user scope) | `[Environment]::SetEnvironmentVariable("HF_TOKEN","hf_xxx","User")` (new shells only) | | ||
| | Windows cmd | `setx HF_TOKEN "hf_xxx"` (user scope, new shells only) | | ||
| - [HF environment variables docs](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables) — HIGH confidence (official, current) |
There was a problem hiding this comment.
Fix table structure corruption.
Two separate tables have been merged without proper separation. Lines 26-29 define a 4-column table for technologies, but lines 30-35 insert a 2-column Shell commands table without closing the first table or adding proper separators.
This breaks markdown rendering and makes the document unreadable.
🔧 Proposed fix
| `huggingface_hub` (already pinned transitively by `transformers>=5.3.0`) | `≥1.12.x` (latest 2026) | Auth + cache + token storage | Canonical, used by every HF library already in the stack. `HfFolder` is **superseded** in v1.x by the higher-level `login()` / `auth_list()` / `auth_switch()` API. |
| `keyring` (Python) | `≥25.x` | Optional OS-keychain backing | Only adopt if a future hardening pass wants Keychain/Credential-Manager/SecretService. **Not recommended for this milestone** — adds a native dep (`dbus`, `pywin32`) per platform with no real security win over `0600` file storage in `HF_HOME`. |
+
+**Shell-specific persistence commands:**
+
| Shell | One-liner to persist `HF_TOKEN` |
|-------|---------------------------------|📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | Technology | Version | Purpose | Why Recommended | | |
| |------------|---------|---------|-----------------| | |
| | `huggingface_hub` (already pinned transitively by `transformers>=5.3.0`) | `≥1.12.x` (latest 2026) | Auth + cache + token storage | Canonical, used by every HF library already in the stack. `HfFolder` is **superseded** in v1.x by the higher-level `login()` / `auth_list()` / `auth_switch()` API. | | |
| | `keyring` (Python) | `≥25.x` | Optional OS-keychain backing | Only adopt if a future hardening pass wants Keychain/Credential-Manager/SecretService. **Not recommended for this milestone** — adds a native dep (`dbus`, `pywin32`) per platform with no real security win over `0600` file storage in `HF_HOME`. | | |
| | Shell | One-liner to persist `HF_TOKEN` | | |
| |-------|---------------------------------| | |
| | macOS zsh (default since 10.15) | `echo 'export HF_TOKEN=hf_xxx' >> ~/.zshrc && source ~/.zshrc` | | |
| | Linux bash | `echo 'export HF_TOKEN=hf_xxx' >> ~/.bashrc && source ~/.bashrc` | | |
| | Windows PowerShell (user scope) | `[Environment]::SetEnvironmentVariable("HF_TOKEN","hf_xxx","User")` (new shells only) | | |
| | Windows cmd | `setx HF_TOKEN "hf_xxx"` (user scope, new shells only) | | |
| - [HF environment variables docs](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables) — HIGH confidence (official, current) | |
| | Technology | Version | Purpose | Why Recommended | | |
| |------------|---------|---------|-----------------| | |
| | `huggingface_hub` (already pinned transitively by `transformers>=5.3.0`) | `≥1.12.x` (latest 2026) | Auth + cache + token storage | Canonical, used by every HF library already in the stack. `HfFolder` is **superseded** in v1.x by the higher-level `login()` / `auth_list()` / `auth_switch()` API. | | |
| | `keyring` (Python) | `≥25.x` | Optional OS-keychain backing | Only adopt if a future hardening pass wants Keychain/Credential-Manager/SecretService. **Not recommended for this milestone** — adds a native dep (`dbus`, `pywin32`) per platform with no real security win over `0600` file storage in `HF_HOME`. | | |
| **Shell-specific persistence commands:** | |
| | Shell | One-liner to persist `HF_TOKEN` | | |
| |-------|---------------------------------| | |
| | macOS zsh (default since 10.15) | `echo 'export HF_TOKEN=hf_xxx' >> ~/.zshrc && source ~/.zshrc` | | |
| | Linux bash | `echo 'export HF_TOKEN=hf_xxx' >> ~/.bashrc && source ~/.bashrc` | | |
| | Windows PowerShell (user scope) | `[Environment]::SetEnvironmentVariable("HF_TOKEN","hf_xxx","User")` (new shells only) | | |
| | Windows cmd | `setx HF_TOKEN "hf_xxx"` (user scope, new shells only) | | |
| - [HF environment variables docs](https://huggingface.co/docs/huggingface_hub/en/package_reference/environment_variables) — HIGH confidence (official, current) |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 26-26: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
[warning] 30-30: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 31-31: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 32-32: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 33-33: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 34-34: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 35-35: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 35-35: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CLAUDE.md` around lines 26 - 36, The markdown tables in CLAUDE.md were merged
incorrectly; split them into two valid Markdown tables by ending the 4-column
"Technology | Version | Purpose | Why Recommended" table with a blank line after
its final row, then create a new 2-column table with its own header row and
separator (e.g., "| Shell | One-liner to persist `HF_TOKEN` |" followed by
"|-------|---------------------------------|") and move the macOS/Linux/Windows
rows under that header; remove the stray merged separator row and ensure each
table has matching header and separator lines so rendering is restored.
| # Pseudocode for the bootstrap | ||
| # Final fallback: don't download Python at all |
There was a problem hiding this comment.
Remove or properly fence orphaned code comments.
Lines 76-77 contain code comments that appear outside any code block context. These should either be removed (if redundant) or included in a properly fenced code example.
🔧 Proposed fix
If these are meant to be documentation prose, rephrase them:
-# Pseudocode for the bootstrap
-# Final fallback: don't download Python at all
+**Mirror fallback strategy:** The bootstrap tries multiple mirror URLs in sequence, falling back to system Python if all mirrors fail.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Pseudocode for the bootstrap | |
| # Final fallback: don't download Python at all | |
| **Mirror fallback strategy:** The bootstrap tries multiple mirror URLs in sequence, falling back to system Python if all mirrors fail. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CLAUDE.md` around lines 76 - 77, The two orphaned comment lines "Pseudocode
for the bootstrap" and "Final fallback: don't download Python at all" are
outside any fenced block in CLAUDE.md; either delete them if redundant, or move
them into a proper fenced code block (triple backticks) or convert to regular
prose/rephrased sentences so they render correctly—look for those exact strings
to locate and update them.
| | Technology | Version | Purpose | Why Recommended | | ||
| |------------|---------|---------|-----------------| | ||
| | Plain Markdown in `docs/` + GitHub-rendered (current state) | n/a | Install tutorial, troubleshooting | Zero new infra. Renders inline on GitHub for issue-replies. No build step to break. | | ||
| | Existing `scripts/smoke-test.sh` + Playwright `tests/` (already in `package.json`) | already pinned | Verify install paths actually work | **This is the real solution to "docs drift."** If smoke-test exercises the install path described in docs, docs that drift will break CI. | | ||
| | **Future** (defer): Astro Starlight | `≥0.30` | Standalone docs site at `docs.omnivoice.studio` | Adopt only when docs exceed ~20 markdown files and need search/versioning. Tauri, the framework OmniVoice already depends on, uses Starlight — well-traveled choice. Material for MkDocs entered maintenance mode in November 2025 per Docsio's 2026 review — **avoid** for new docs. | | ||
| | Project | What they do | | ||
| |---------|--------------| | ||
| | **OBS Studio** | Docs at `obsproject.com/docs` (Sphinx, separate repo). Install paths in README, wiki for community-contributed. CI doesn't gate on docs drift. | | ||
| | **Audacity** | Manual at `manual.audacityteam.org` (MediaWiki). README is minimal. Install path = "use the installer." No automated sync. | | ||
| | **Tauri** | Docs at `v2.tauri.app` (Astro Starlight, separate repo `tauri-apps/tauri-docs`). README is minimal. Heavy reliance on community contributions and PR review. | | ||
| | **VS Code** | Docs at `code.visualstudio.com/docs` (separate repo, Markdown). README is minimal. Manual sync; docs team is staffed. | | ||
| - [Tauri docs (Astro Starlight)](https://github.com/tauri-apps/tauri-docs) — HIGH (reference for "if we ever move off README") |
There was a problem hiding this comment.
Fix table structure corruption.
A 2-column "Project examples" table (lines 109-114) has been incorrectly inserted into the middle of a 4-column technology stack table (lines 104-108). This breaks markdown table rendering.
🔧 Proposed fix
| Existing `scripts/smoke-test.sh` + Playwright `tests/` (already in `package.json`) | already pinned | Verify install paths actually work | **This is the real solution to "docs drift."** If smoke-test exercises the install path described in docs, docs that drift will break CI. |
| **Future** (defer): Astro Starlight | `≥0.30` | Standalone docs site at `docs.omnivoice.studio` | Adopt only when docs exceed ~20 markdown files and need search/versioning. Tauri, the framework OmniVoice already depends on, uses Starlight — well-traveled choice. Material for MkDocs entered maintenance mode in November 2025 per Docsio's 2026 review — **avoid** for new docs. |
+
+**How other OSS desktop apps handle docs:**
+
| Project | What they do |
|---------|--------------|🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 104-104: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
[warning] 109-109: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 110-110: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 111-111: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 112-112: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 113-113: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 114-114: Table column count
Expected: 4; Actual: 2; Too few cells, row will be missing data
(MD056, table-column-count)
[warning] 114-114: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CLAUDE.md` around lines 104 - 115, The 4-column technology table header
"Technology | Version | Purpose | Why Recommended" has been corrupted by an
inserted 2-column "Project | What they do" table; remove or move the "Project"
table so the 4-column table contains only its intended rows and the header
separator (the --- line) has four columns of separators, then place the "Project
| What they do" table after the technology table as its own separate table;
ensure each table uses consistent pipe separators and that the technology table
rows align with the four headers while the Project table remains a distinct
2-column block.
| # counter just on open, and the backend creates runtime subdirs | ||
| # (dub_jobs/, outputs/, preview/) under OMNIVOICE_DATA_DIR — both would | ||
| # show up as dirty in `git status` after every test run. | ||
| _FIXTURE_COPY = Path(tempfile.mkdtemp(prefix="omnivoice-smoke-")) |
There was a problem hiding this comment.
Temporary directory will leak on every test run.
tempfile.mkdtemp() creates a directory that persists after the process exits. Over many test runs, this will accumulate orphaned directories in /tmp (or the system temp location).
Use a session-scoped pytest fixture with cleanup instead:
♻️ Proposed fix using pytest fixture with cleanup
-# Copy the frozen fixture into a per-session temp dir so the smoke test
-# never mutates the checked-in artifact. SQLite touches its file-change
-# counter just on open, and the backend creates runtime subdirs
-# (dub_jobs/, outputs/, preview/) under OMNIVOICE_DATA_DIR — both would
-# show up as dirty in `git status` after every test run.
-_FIXTURE_COPY = Path(tempfile.mkdtemp(prefix="omnivoice-smoke-"))
-shutil.copytree(FIXTURE_SRC, _FIXTURE_COPY, dirs_exist_ok=True)
-
-# Point backend.core.config.get_app_data_dir() at the COPY before any
-# import that pulls in core.config (which caches DB_PATH at module import).
-os.environ.setdefault("OMNIVOICE_DATA_DIR", str(_FIXTURE_COPY))
+@pytest.fixture(scope="session", autouse=True)
+def _fixture_copy_session():
+ """Copy the frozen fixture to a temp dir and clean up on exit."""
+ with tempfile.TemporaryDirectory(prefix="omnivoice-smoke-") as tmpdir:
+ fixture_copy = Path(tmpdir)
+ shutil.copytree(FIXTURE_SRC, fixture_copy, dirs_exist_ok=True)
+ os.environ["OMNIVOICE_DATA_DIR"] = str(fixture_copy)
+ yield fixture_copy🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/smoke/test_boot_smoke.py` at line 43, The global _FIXTURE_COPY created
via tempfile.mkdtemp leaks temp directories; replace it with a session-scoped
pytest fixture (e.g., `@pytest.fixture`(scope="session") def fixture_copy()) that
creates the tempdir (use tempfile.TemporaryDirectory() or tempfile.mkdtemp()),
yields Path(tmpdir) to tests, and performs cleanup (shutil.rmtree or
TemporaryDirectory.__exit__) in teardown; update usages of the global
_FIXTURE_COPY to accept the new fixture parameter in tests (reference symbol:
_FIXTURE_COPY and the new fixture name like fixture_copy) so tempdirs are
removed after the test session.
…DOCS-05) Covers two persistent paths: - Method A — canonical ~/.cache/huggingface/token via huggingface-cli login - Method B — shell env var (~/.zshrc / ~/.bashrc / Windows User scope) Documents the v0.2.7 "session only" in-app behavior + notes that Phase 1 AUTH-03 will make in-app pastes write to the canonical file. Bundled with Phase 0 PR per user request. Strictly DOCS-05 scope — zero code changes, no engine touches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lback (AUTH-01..06) Replaces the env_store.py file-based design with a SQLite-backed app store + cascade resolver that checks app → env var → ~/.cache/huggingface/token in priority order, with automatic fallback to next source on HTTP 401. User-explicit design decision: - App-stored token (SQLite settings table, AES-GCM encrypted) wins - Env var ($HF_TOKEN) second - Global huggingface-cli login file third - All three sources visible in Settings → API Keys with "Active" badge - Save action populates BOTH app store AND canonical HF file (defense in depth) New requirement: - AUTH-06 — on 401, auto-retry next source in cascade before erroring Also: traceability count corrected (62 → 74 — undercount at planning + INST-12 + AUTH-06 added post-planning). All 74 v1 reqs mapped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.planning/REQUIREMENTS.md:
- Around line 204-205: The traceability totals are inconsistent: decide the
authoritative total (e.g., 74 if Phase 4 added) and update the coverage headline
string ("Coverage = 62 / 62 v1 requirements (100%)"), the summary bullets that
currently state "74 total" (or vice-versa if you choose 62), and the phase count
table (the "Total 62" and each per-phase counts) so every reference and the
per-phase sums match the chosen authoritative number; ensure Phase 4 (Adaptive &
Specialty Engines) is included with its correct count and that the sum of all
phase rows equals the single chosen total.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: cae0f590-6e75-420d-a5b3-bfef83ffdb92
📒 Files selected for processing (1)
.planning/REQUIREMENTS.md
| Filled by roadmap on 2026-05-16; updated 2026-05-16 after inserting Phase 4 (Adaptive & Specialty Engines). Coverage = 62 / 62 v1 requirements (100%). No orphans, no duplicates. | ||
|
|
There was a problem hiding this comment.
Traceability totals are internally inconsistent and need one authoritative count.
Line 204 says 62 / 62, but Lines 283-287 say 74 total, and the phase table in Lines 289-299 still reports Total 62 with per-phase counts that don’t match the listed requirements. This makes milestone gating ambiguous.
Please reconcile these values in one pass (coverage line, summary bullets, and phase count table) so they all reflect the same total.
Proposed doc fix (example)
-Filled by roadmap on 2026-05-16; updated 2026-05-16 after inserting Phase 4 (Adaptive & Specialty Engines). Coverage = 62 / 62 v1 requirements (100%). No orphans, no duplicates.
+Filled by roadmap on 2026-05-16; updated 2026-05-16 after inserting Phase 4 (Adaptive & Specialty Engines). Coverage = 74 / 74 v1 requirements (100%). No orphans, no duplicates.
...
-| Phase 1 — Install + Token + Docs + Error UX | 16 |
+| Phase 1 — Install + Token + Docs + Error UX | 18 |
...
-| **Total** | **62** |
+| **Total** | **74** |Also applies to: 283-299
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.planning/REQUIREMENTS.md around lines 204 - 205, The traceability totals
are inconsistent: decide the authoritative total (e.g., 74 if Phase 4 added) and
update the coverage headline string ("Coverage = 62 / 62 v1 requirements
(100%)"), the summary bullets that currently state "74 total" (or vice-versa if
you choose 62), and the phase count table (the "Total 62" and each per-phase
counts) so every reference and the per-phase sums match the chosen authoritative
number; ensure Phase 4 (Adaptive & Specialty Engines) is included with its
correct count and that the sum of all phase rows equals the single chosen total.
…env var Two call sites were only checking $HF_TOKEN env var, missing the canonical ~/.cache/huggingface/token file written by `huggingface-cli login` (or the app's future Save action): - system.py `/system/info` `has_hf_token` flag — UI showed "No HF token" even when `huggingface-cli login` had populated the file. - model_manager.get_diarization_pipeline — pyannote diarization silently returned None when only the canonical file was set. This is the bug behind issue #35 (speaker diarization setup failure). Both fixes use the same pattern: env var > huggingface_hub.get_token() (which reads the canonical file). Adds a local _has_hf_token() helper to system.py with a comment marking it as prelude to the AUTH-01..06 cascade (Phase 1 token_resolver.py will layer SQLite app-store on top). Closes #35 sub-issue (canonical token invisible to diarization). Cross-cuts AUTH-02 + AUTH-06 design for Phase 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…INST-13) The dictation widget infrastructure shipped in PR #40 but was only reachable via the undocumented --pill CLI flag. Adds three discovery paths: 1. Tray menu: "Switch to Dictation Widget" (studio mode) — saves launch_as_widget=true to config, relaunches with --pill, exits current. Mirrors the existing "Open Studio" path in pill-mode tray. 2. Persistent config: AppConfig.launch_as_widget (bool, default false). Read at startup via load_config_pre_app() (uses dirs-next, no AppHandle required). CLI --pill still takes precedence when explicitly passed. 3. Tauri commands: get_launch_as_widget / set_launch_as_widget for the Phase 2 Settings UI to bind a checkbox to. 4. Scripts: bun desktop-prod:pill / desktop-prod:run:pill — forward --pill to the bundled app launch. macOS uses `open -n --args` to spawn fresh instance with the flag. Closes the GUI half of INST-13. Phase 2 closes the Settings UI half. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src-tauri/src/lib.rs`:
- Around line 271-300: The menu handlers for "open_studio" and "switch_to_pill"
currently spawn a new process via std::process::Command and then call
app.exit(0), causing a race with single-instance enforcement; replace that
spawn+exit pattern by calling the Tauri process plugin's relaunch() API (via the
ProcessExt trait) after persisting the config (i.e., keep
crate::config::load_config/save_config and setting cfg.launch_as_widget), remove
the std::process::Command::{new(...).spawn()} calls, and ensure you import and
use ProcessExt::relaunch() so the new instance is reliably started once the
current instance tears down rather than being intercepted by the single-instance
plugin.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b4bbe34a-77ac-4433-9a8e-60c2922a8e5c
📒 Files selected for processing (6)
.planning/REQUIREMENTS.mdfrontend/src-tauri/src/commands.rsfrontend/src-tauri/src/config.rsfrontend/src-tauri/src/lib.rspackage.jsonscripts/desktop-prod.sh
| "open_studio" => { | ||
| // Launch ourselves without --pill to open the full studio | ||
| // Persist the preference (so next launch is studio, not pill) | ||
| // then spawn a new instance without --pill and exit this one. | ||
| let mut cfg = crate::config::load_config(app); | ||
| cfg.launch_as_widget = false; | ||
| crate::config::save_config(app, &cfg); | ||
| if let Ok(exe) = std::env::current_exe() { | ||
| let _ = std::process::Command::new(exe).spawn(); | ||
| } | ||
| app.state::<AppFlags>() | ||
| .quitting | ||
| .store(true, Ordering::SeqCst); | ||
| app.exit(0); | ||
| } | ||
| "switch_to_pill" => { | ||
| // Mirror of "open_studio" but the other direction: | ||
| // persist launch_as_widget=true, relaunch with --pill, | ||
| // and exit the current (studio) instance. | ||
| let mut cfg = crate::config::load_config(app); | ||
| cfg.launch_as_widget = true; | ||
| crate::config::save_config(app, &cfg); | ||
| if let Ok(exe) = std::env::current_exe() { | ||
| let _ = std::process::Command::new(exe) | ||
| .arg("--pill") | ||
| .spawn(); | ||
| } | ||
| app.state::<AppFlags>() | ||
| .quitting | ||
| .store(true, Ordering::SeqCst); | ||
| app.exit(0); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.rs" | head -20Repository: debpalash/OmniVoice-Studio
Length of output: 1041
🏁 Script executed:
git ls-files | grep -E "(lib\.rs|main\.rs|Cargo\.toml)" | head -20Repository: debpalash/OmniVoice-Studio
Length of output: 161
🏁 Script executed:
# Get repository structure
find . -type f \( -name "Cargo.toml" -o -name "lib.rs" -o -name "main.rs" \) | head -30Repository: debpalash/OmniVoice-Studio
Length of output: 167
🏁 Script executed:
# Read the relevant section of lib.rs around lines 271-300
sed -n '260,310p' frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 3050
🏁 Script executed:
# Get more context - read the whole file to understand structure
wc -l frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 104
🏁 Script executed:
# Look at the beginning of lib.rs to understand imports and setup
head -100 frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 3714
🏁 Script executed:
# Check for single-instance configuration in Cargo.toml
grep -A 20 "\[package\]" frontend/src-tauri/Cargo.toml | head -30Repository: debpalash/OmniVoice-Studio
Length of output: 540
🏁 Script executed:
# Check for any setup related to single-instance apps
rg "single.instance|single-instance" frontend/src-tauri/Repository: debpalash/OmniVoice-Studio
Length of output: 373
🏁 Script executed:
# Look for more context around the single-instance setup
sed -n '65,120p' frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 2603
🏁 Script executed:
# Check main.rs for app launch context
cat frontend/src-tauri/src/main.rsRepository: debpalash/OmniVoice-Studio
Length of output: 247
🏁 Script executed:
# Search for any other spawn patterns in the codebase
rg "std::process::Command|spawn\(" frontend/src-tauri/src/ -A 2 -B 2Repository: debpalash/OmniVoice-Studio
Length of output: 5117
🏁 Script executed:
# Check if there's any event or signal handling for process exit
rg "exit|shutdown|quitting" frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 1023
🏁 Script executed:
# Look at the full context of where app.exit is called
sed -n '250,320p' frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 3876
🏁 Script executed:
# Check if there's any window close or cleanup handler
rg "on_window_close|window_close_requested" frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Look for Tauri's event system and if there's a recommended restart/relaunch pattern
rg "Event|event_loop|on_" frontend/src-tauri/src/lib.rs | head -20Repository: debpalash/OmniVoice-Studio
Length of output: 892
🏁 Script executed:
# Check the Tauri version in Cargo.toml
grep -E "^tauri|^tauri-plugin" frontend/src-tauri/Cargo.tomlRepository: debpalash/OmniVoice-Studio
Length of output: 465
🌐 Web query:
Tauri 2.0 single-instance app restart relaunch pattern recommended approach
💡 Result:
In Tauri 2.0, the recommended approach for managing single-instance applications and handling restarts involves using the official tauri-plugin-single-instance [1] and tauri-plugin-process [2] plugins. Single-Instance Management To ensure only one instance of your application runs, use the tauri-plugin-single-instance plugin [1]. When a user attempts to launch a second instance, the plugin intercepts the request and prevents the new process from fully starting [3][1]. You can configure the plugin's init() closure to perform actions in the primary instance, such as focusing the main window [3][1]: // src-tauri/src/lib.rs use tauri::{Manager}; use tauri_plugin_single_instance::init; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run { let mut builder = tauri::Builder::default; #[cfg(desktop)] { builder = builder.plugin(init(|app, _args, _cwd| { if let Some(window) = app.get_webview_window("main") { let _ = window.set_focus; } })); } builder.run(tauri::generate_context!).expect("error while running tauri application"); } Restart/Relaunch Pattern To restart the application, use the relaunch() function provided by the tauri-plugin-process plugin [2][4]. Important Considerations for Stability: 1. Race Conditions: Developers have reported that calling relaunch() or app.restart() can sometimes lead to race conditions where the current process is terminated before the new process successfully spawns [5][6]. 2. Robust Restart Pattern: To mitigate potential instability, some developers implement a manual cleanup and exit strategy [6]. Instead of relying solely on relaunch(), you can listen for the RunEvent::Exit event in your main loop, perform necessary cleanup (e.g., saving state, closing handles), and then trigger the restart [6]. 3. Plugin Ordering: Ensure that the single-instance plugin is registered first in your tauri::Builder configuration to ensure it correctly intercepts new launch attempts before other plugins initialize [7][8]. If you encounter issues where the app fails to relaunch, ensure your tauri.conf.json has the process:allow-restart permission enabled [2]. For complex applications, consider implementing a state-saving mechanism that persists data to disk (e.g., SQLite or a JSON file) before the restart, as memory-resident state will be lost [9][10].
Citations:
- 1: https://v2.tauri.app/plugin/single-instance/
- 2: https://v2.tauri.app/plugin/process/
- 3: https://github.com/tauri-apps/tauri-docs/blob/v2/src/content/docs/plugin/single-instance.mdx
- 4: https://v2.tauri.app/reference/javascript/process/
- 5: Unstable relaunch with single instance plugin tauri-apps/plugins-workspace#1692
- 6: [bug] App::restart does not restart after update.download_and_install in rust tauri-apps/tauri#11392
- 7: https://crates.io/crates/tauri-plugin-single-instance/2.0.2
- 8: https://crates.io/crates/tauri-plugin-single-instance
- 9: https://dev.to/hiyoyok/building-a-menubar-app-with-tauri-v2-what-nobody-tells-you-9a2
- 10: https://dev.to/jorrygo_dev/building-a-jira-time-tracker-with-tauri-how-i-stored-api-tokens-securely-46aj
🌐 Web query:
Tauri App.relaunch() method single-instance plugin interaction
💡 Result:
In Tauri applications, the App.relaunch() method (provided by the @tauri-apps/plugin-process plugin) and the tauri-plugin-single-instance plugin interact in a way that can lead to instability or unexpected behavior if not managed carefully [1]. When relaunch() is called, the current application process exits and attempts to start a new instance [2][3]. If the single-instance plugin is active, the new instance will detect the existing process (or the remnants of the exiting one) and may immediately exit, handing off control to the original instance [4][5]. Key considerations for this interaction include: 1. Potential for Race Conditions: Users have reported that relaunch() is not always guaranteed to succeed when the single-instance plugin is present [1]. The asynchronous nature of the exit and restart process can lead to situations where the application closes without successfully launching the new instance, or where the single-instance lock is not released in time [1]. 2. Window Restoration: A common issue occurs when the app relaunches but fails to restore the main window [6]. Because the single-instance plugin's callback is often used to focus an existing window, if the main window was hidden or destroyed during the relaunch process, the app may appear to be running in the background with no visible interface [6]. Developers should ensure their single-instance callback explicitly checks for and restores/shows the main window if it is missing [4][5][6]. 3. Plugin Registration Order: It is critical that the single-instance plugin is registered first in your tauri::Builder configuration [7][8]. This ensures it correctly intercepts instance-related events before other plugins can interfere [7]. 4. Platform Differences: On macOS, the operating system has built-in single-instance mechanisms that may supersede or conflict with the single-instance plugin [9]. In some cases, handling the RunEvent::Reopen event is a more reliable way to manage app visibility and focus than relying solely on the plugin [9]. To mitigate these issues, ensure your single-instance initialization callback is robust: rust // Example of a robust single-instance callback builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, cwd| { if let Some(window) = app.get_webview_window("main") { window.show().unwrap(); window.set_focus().unwrap(); } })); If you continue to experience instability, consider implementing a custom check to ensure the previous process has fully exited before the new one attempts to take over, or verify that your relaunch logic does not conflict with the OS-level process management [1].
Citations:
- 1: Unstable relaunch with single instance plugin tauri-apps/plugins-workspace#1692
- 2: https://v2.tauri.app/reference/javascript/process
- 3: https://v2.tauri.app/plugin/process/
- 4: https://tauri.app/plugin/single-instance
- 5: https://v2.tauri.app/plugin/single-instance/
- 6: Desktop relaunch can exit without restoring the main window anomalyco/opencode#20222
- 7: https://github.com/tauri-apps/tauri-docs/blob/v2/src/content/docs/plugin/single-instance.mdx
- 8: https://crates.io/crates/tauri-plugin-single-instance/2.2.2
- 9: [single-instance] Callback function does not run if the app is not visible (after calling window.hide()) tauri-apps/plugins-workspace#1613
🏁 Script executed:
# Check if there's any existing use of app.relaunch in the codebase
rg "relaunch|restart" frontend/src-tauri/Repository: debpalash/OmniVoice-Studio
Length of output: 468
🏁 Script executed:
# Check the exact API available in tauri-plugin-process
rg "restart|relaunch" frontend/src-tauri/Cargo.tomlRepository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Look at commands.rs to see if there's already a pattern for restart
sed -n '1,50p' frontend/src-tauri/src/commands.rsRepository: debpalash/OmniVoice-Studio
Length of output: 1406
🏁 Script executed:
# Check what's imported from tauri in lib.rs
sed -n '1,30p' frontend/src-tauri/src/lib.rs | grep "use tauri"Repository: debpalash/OmniVoice-Studio
Length of output: 184
🌐 Web query:
Tauri 2.0 tauri-plugin-process restart relaunch API Rust
💡 Result:
In Tauri 2.0, the functionality to restart or relaunch the application is provided by the tauri-plugin-process plugin [1][2]. To use this functionality, you must first add the plugin to your project and configure the necessary permissions in your src-tauri/capabilities/ configuration files [2][3]. Configuration and Usage: 1. Plugin Setup: Ensure tauri-plugin-process is added to your Cargo.toml and initialized in your main.rs (or lib.rs) [1]. 2. Permissions: You must explicitly allow the restart capability in your capability configuration (e.g., src-tauri/capabilities/main.json) [2][3]: { "permissions": [ "process:allow-restart" ] } 3. JavaScript/TypeScript API: You can trigger a restart from your frontend using the relaunch function [1][4]: import { relaunch } from '@tauri-apps/plugin-process'; await relaunch; 4. Rust API: You can also trigger a restart from the Rust backend using the tauri_plugin_process::process_ext::ProcessExt trait or by calling the restart method on the AppHandle [5]. Note that AppHandle::restart and process::restart are marked as diverging functions [5]. If you encounter an "Operation not permitted" error when calling relaunch, ensure that the process:allow-restart permission is correctly defined in your active capability file [3]. Note that restarting an application can be complex depending on the OS and sandboxing environment, and in some cases, may require additional helper processes or specific OS-level configurations [3].
Citations:
- 1: https://crates.io/crates/tauri-plugin-process/2.0.0-beta.0
- 2: https://v2.tauri.app/plugin/process/
- 3: [question] When calling the relaunch method of the plugin-process, I encounter the error “Operation not permitted (os error 1).” Could you help me find a solution? tauri-apps/plugins-workspace#2273
- 4: https://v2.tauri.app/reference/javascript/process
- 5: https://www.github.com/tauri-apps/tauri/pull/10171
Use Tauri's relaunch() API to safely switch modes with single-instance enforcement.
The current pattern of spawning a child process before app.exit(0) creates a race condition: the child may be intercepted by the single-instance plugin as a duplicate instance and terminated, leaving no app running. Tauri's tauri-plugin-process::relaunch() (already available in your dependencies and with process:allow-restart permission already configured) is designed specifically for this scenario and ensures the new process starts only after the current one fully tears down.
Replace the manual spawn-then-exit pattern in "open_studio" and "switch_to_pill" menu handlers with relaunch() from the process plugin. This requires adding the ProcessExt trait import and will handle correct single-instance semantics across macOS (Intel/Apple Silicon), Windows, and Linux.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src-tauri/src/lib.rs` around lines 271 - 300, The menu handlers for
"open_studio" and "switch_to_pill" currently spawn a new process via
std::process::Command and then call app.exit(0), causing a race with
single-instance enforcement; replace that spawn+exit pattern by calling the
Tauri process plugin's relaunch() API (via the ProcessExt trait) after
persisting the config (i.e., keep crate::config::load_config/save_config and
setting cfg.launch_as_widget), remove the
std::process::Command::{new(...).spawn()} calls, and ensure you import and use
ProcessExt::relaunch() so the new instance is reliably started once the current
instance tears down rather than being intercepted by the single-instance plugin.
…ible Suspense fallback Before: pill mode set up correctly but the widget window stayed hidden until ⌘⇧Space was pressed. New users saw absolutely nothing on launch (no main window, no dock icon, hidden widget) and assumed the app failed. If global-shortcut Accessibility permission wasn't granted, they had no path to discover the widget at all. Two changes: 1. lib.rs: in pill_mode_setup, explicitly show + position + focus the widget window after hiding main. With per-call error logging so we can diagnose failures (and a clear error log if widget window wasn't created at all — points at tauri.conf.json regression). 2. main-app.jsx: Suspense fallback was `null`, which combined with widget's transparent+decorations:false config made any lazy-import delay or failure invisible. Now renders a dark pill saying "Loading dictation…" so even if CaptureWidget lazy-import stalls, the user sees the window exists. Studio mode behavior unchanged — widget stays hidden until hotkey or tray click triggers it (existing show() call in the shortcut/ menu handlers is preserved). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ly dropped config-array creation
Root cause: declaring the widget window in tauri.conf.json's app.windows[]
silently failed in Tauri 2 — get_webview_window("widget") returned None
even though the config was syntactically valid. Probable culprit was the
transparent + decorations:false + visible:false combo, but Tauri offered
no error message either at startup or via webview_windows() enumeration.
Diagnosed by adding webview_windows() enumeration logging at setup start
(only ["main"] ever appeared) and a programmatic WebviewWindowBuilder
fallback that surfaces real Result errors.
Fix:
- tauri.conf.json: widget entry now has `create: false` to make the
config-vs-programmatic handoff explicit.
- lib.rs setup(): call WebviewWindowBuilder::new(app, "widget", ...).build()
with the exact same surface attributes the config used to declare.
- capabilities/default.json: include "widget" in windows array so the new
window inherits the same Tauri permissions as main.
- tauri.conf.json: remove the invalid `"url": "/?window=widget"` field —
WebviewUrl::App takes a path only, query strings aren't supported.
Both windows now load index.html.
- main-app.jsx: replace URL-query-based widget detection with
getCurrentWindow().label === 'widget' via @tauri-apps/api/window. This
is the Tauri 2-recommended pattern for multi-window apps and works
regardless of URL routing.
Closes the immediate UX bug behind the dictation widget being invisible.
Builds cleanly + manually verified: pill widget visible on screen at
top-center after `bun desktop-prod:pill`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(phase-5): research opt-in bug reporting Phase 5 research: prefilled-URL GitHub Issues pattern, default-deny payload, redaction layer, two-step consent UX, rate/dedup/recursion safeguards, aggregation across Python/Rust/React error producers. Builds on Phase 1's links.py + errorDocsMap deeplink infrastructure; uses already-installed @tauri-apps/plugin-opener (^2.5.4). No new packages required. Covers REPORT-01..12 with confidence levels, 8 pitfalls, subprocess-engine error capture handoff to Phase 2, security domain mapped to ASVS, and 3-wave delivery plan (redactor + payload, consent UI, aggregation + pre-submit search). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(phases): research for Phases 2, 3, 4, 6 (Engine + Supertonic + Spikes + Release) * docs(stack): bump supertonic pin 1.2.3 → 1.3.1 (Phase 3 research finding) * docs(phases): plan Phases 2-6 for v0.3.0 fat-milestone release 15 new plan files + 2 ADR decision docs across 5 phases. Combined with Phase 1's 3 plans, the v0.3.0 milestone now has 18 PLAN.md files covering all 7 phases (Phase 0 already complete via PR #71). PHASE 2 (Engine Isolation — 4 plans): - 02-01: SubprocessBackend primitive + echo sidecar POC + graceful is_available wrap (ENGINE-01/05) - 02-02: _safe_torchaudio_save helper + migrate 11 WAV write sites + #48 regression (BUG-01) - 02-03: IndexTTS sidecar entry + venv-probe bootstrap + IndexTTS2Backend rewire (ENGINE-02/03/04/07, closes #42) - 02-04: Engine Compatibility Matrix UI + /engines/{id}/health route (ENGINE-06) PHASE 3 (Supertonic-3 + Mirror — 2 plans): - 03-01: Supertonic-3 engine on SubprocessBackend + SHA pin + license gate (TTS-01..06) - 03-02: bootstrap.rs mirror cascade + UV_DEFAULT_INDEX migration + frozen enforcement + docs (INST-07..11) PHASE 4 (Spike-first Adaptive & Specialty — 2 plans + 2 ADRs): - 04-01: OmniVoice-GGUF hardware-adaptive engine + quant_map + bundled binaries (SPIKE-01, GGUF-01..06) - 04-02: OmniVoice-Singing subclass + dub pipeline singing mode + segment detector (SPIKE-02, SING-01..05) - SPIKE-01-gguf.md + SPIKE-02-singing.md ADRs in .planning/decisions/ PHASE 5 (Opt-in Bug Reporting — 3 plans): - 05-01: Redactor + BugReporter + URL builder + rate/dedup/recursion safeguards + FastAPI router (REPORT-01/02/03/05/06/07/08/10/11) - 05-02: BugReportDialog two-step consent + PrivacyPanel + ErrorBoundary integration + Rust panic hook (chained) (REPORT-01-Rust/04/09/12) - 05-03: Dry-run vs 3 historical issues + cross-platform openUrl smoke + Phase 2 subprocess-errors handoff (REPORT-02 smoke, REPORT-03 expansion, REPORT-09) PHASE 6 (Release + Retro — 4 plans): - 06-01: rc1 prep — version bump across 4 sources + CHANGELOG + retro stub + PR-73-strategy doc (REL-01/03/06) - 06-02: CI guards — workflow-parity actionlint + tag-shaped dry-run (Phase 0 retro options B + C; closes release-engineer gap) - 06-03: PR #73 reimplementation (NOT rebase) — backend-split installer with mirror-cascade integration + pill-mode regression checkpoint - 06-04: Execute the release — pre-tag gates + 4-OS clean-VM + 48h soak + tag + retro + 3 v0.4 deferral tracking issues (REL-01/02/03/04/05/06) Scope decisions locked in plans (council session): - SoniTranslate refactor DEFERRED to v0.4 (Phase 2 ships SubprocessBackend without migrating Soni) - macOS notarization DEFERRED to v0.4 (Phase 6 ships xattr -cr automation per CLAUDE.md Key Decision #7) - supertonic pin 1.2.3 → 1.3.1 (already committed in ba63733) - SPIKE-01 and SPIKE-02 both GO; 13/13 Phase 4 reqs stay in scope - PR #73 reimplemented, not rebased (93 commits behind main) All 18 plans validated via gsd-sdk frontmatter.validate + verify.plan-structure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…92) Phase 0 was verified PASS against `main` (7/7 truths, 9/9 artifacts, 6/6 GATE requirements, 5/5 success criteria; live smoke `tests/smoke/` green in 1.73s). Add the verifier's report and reconcile the REQUIREMENTS tracker — GATE-01..06 and AUTH-01..06 now show Done now that PR #71 (Phase 0) and PR #91 (Phase 1 Wave 1) are both on `main`. AUTH-03 is split: backend endpoints landed in Wave 1, UI ships in Wave 2. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Phase 0 of the v0.3.x stabilization milestone — lays the CI/release runway so all downstream stability fixes ship with macOS/Windows regressions caught before they hit users.
Requirements covered
tests/fixtures/omnivoice_data/checked-in regression fixture (≤200 KB, no LFS) +tests/smoke/test_boot_smoke.pyruns against it.smoke-matrixjob in.github/workflows/ci.ymlruns onmacos-14,windows-2022,ubuntu-22.04for every PR tomain..github/workflows/release.ymlboots the bundled installer per OS and asserts--health-check→/health200 within 60 s..github/pull_request_template.md(lowercase, in place) documents two-RC release cadence + regression-fixture check.release.ymlpublishes SHA-256 checksums inline in the release body AND as per-OSSHA256SUMS-<label>.txtrelease assets.Dogfood check
The new
smoke-matrixruns against THIS PR's diff. If you see it green on all three OSes in the PR checks, the matrix itself is wired correctly.Test plan
smoke-matrix(macOS / Windows / Linux) are green on this PR's checks.uv run pytest tests/smoke/ -qpasses (4 tests, < 30 s on warm cache).du -sh tests/fixtures/omnivoice_data/≤ 200 KB.release.ymlper T0.D.3 confirms installer smoke steps work end-to-end.Not in this PR (deferred to Slice C, post-merge)
smoke-matrixjob applies to its diff (per CONTEXT.md L86 interleave decision).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
CI / Release
Tests
Documentation