Skip to content

chore: docs tests and misc optimizations#752

Merged
JarbasAl merged 26 commits intodevfrom
optimize
Mar 14, 2026
Merged

chore: docs tests and misc optimizations#752
JarbasAl merged 26 commits intodevfrom
optimize

Conversation

@JarbasAl
Copy link
Member

@JarbasAl JarbasAl commented Mar 12, 2026

Summary by CodeRabbit

  • New Features

    • Skill uninstall flow implemented with GitHub validation and optional deferred skill loading.
  • Bug Fixes

    • Multiple race conditions fixed; improved stop/intent coordination and listener cleanup.
  • Performance Improvements

    • Safer concurrent plugin handling, event-driven waits, reusable stop events, and lazy cached transformer sorting.
  • Documentation

    • Added/updated AUDIT, FAQ (CI & performance), MAINTENANCE, SUGGESTIONS, QUICK_FACTS.
  • Localization

    • Extensive updates to stop/global-stop phrase sets across many locales.
  • Tests / CI

    • Large new unit and end-to-end test suites and several CI workflows (coverage, docs, locale, type checks, release previews).

JarbasAl and others added 4 commits March 12, 2026 00:32
- pyproject.toml: Python project configuration (required for build)
- AUDIT.md: Known issues and technical debt documentation
- QUICK_FACTS.md: Machine-readable project reference
- docs/: Architecture and feature documentation
- .env: Environment configuration

These files were merged but not committed. Required for CI builds to succeed.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…overhead

**Priority 1 — Race Conditions (correctness + perf)**

- Add lock to _unload_plugin_skill to prevent concurrent dict mutation (skill_manager.py:585)
- Snapshot plugin_skills dict inside lock for safe iteration in 4 methods:
  send_skill_list, deactivate_skill, activate_skill, deactivate_except
  Prevents RuntimeError: dictionary changed size during iteration
- Replace busy-wait in _collect_fallback_skills with threading.Event signaling
  (fallback_service.py:122-125) — reduces CPU usage on utterances reaching fallback

**Priority 2 — Per-Utterance Work (latency)**

- Replace threading.Event().wait(1) with self._stop_event.wait(1)
  (skill_manager.py:462) — reuses event, correctly respects stop signal
- Move migration_map dict and re.compile regex to module-level constants
  (service.py) — 15 utterances/second → rebuilding these on every pipeline stage
- Guard create_daemon calls with config check before spawning thread
  (service.py:322, 352) — skip thread creation when open_data.intent_urls not configured

**Priority 3 — Minor Overhead**

- Change _logged_skill_warnings from list to set (O(1) lookup vs O(n))
  (skill_manager.py:111)
- Cache sorted plugins in all 3 transformer services (transformers.py)
  Invalidate cache on load_plugins()
- Read blacklist once before plugin scan loop instead of per-skill
  (skill_manager.py:361)

All 65 unit tests pass. Coverage maintained at 60% for ovos_core.skill_manager.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…eport

- FAQ.md: Add comprehensive Performance section documenting all optimizations (thread-safe loading, event signaling, caching, etc.)
- MAINTENANCE_REPORT.md: Add detailed entry for 2026-03-11 performance optimization work
- AUDIT.md: Document fixed race conditions (plugin_skills dict, busy-wait, temporary events)
- SUGGESTIONS.md: Add S-007 marking all performance improvements as ADDRESSED

All changes cross-referenced with code locations and commit SHA.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Documents and fixes for race conditions and performance; refactors StopService to use OVOSAbstractApplication and changes match return types; adds GitHub-backed skill validation and pip uninstall; introduces lazy transformer caching; broad locale vocabulary updates; packaging/CI workflow additions; and extensive unit and end-to-end tests.

Changes

Cohort / File(s) Summary
Concurrency & Skill lifecycle
ovos_core/skill_manager.py
Add self._plugin_skills_lock, switch _logged_skill_warnings to a set, use local blacklist snapshot, guard plugin_skills reads/writes, perform shutdowns outside lock, and improve wait_for_intent_service timing/event usage.
Fallback synchronization
ovos_core/intent_services/fallback_service.py
Introduce a private threading.Event (_fallback_response_event), signal it in ack handler, and replace busy-wait sleeps with event-based waits in fallback collection.
Stop service refactor & intent service tweaks
ovos_core/intent_services/stop_service.py, ovos_core/intent_services/service.py, ovos_core/intent_services/converse_service.py
StopService now subclasses OVOSAbstractApplication and returns IntentHandlerMatch; remove direct voc cache/helpers; add module-level _PIPELINE_MIGRATION_MAP/_PIPELINE_RE; gate metric uploads on config; ensure try/finally cleanup for bus listeners; tighten pong/ack handling and defensive message parsing.
Transformer lazy caching
ovos_core/transformers.py
Add _sorted_plugins cache (None-invalidate on load) and compute sorted plugin list lazily to avoid repeated sorts.
Skill installer & uninstall
ovos_core/skill_installer.py
Implement validate_skill using GitHub contents API (owner/repo check, 3s timeout, packaging file presence, fail-open on network errors) and functional uninstall via pip_uninstall; add container pip warning and improved messaging.
Locale vocabulary updates
ovos_core/intent_services/locale/*/*.{voc}
Large edits across many locales: add/remove/reorder stop/global_stop phrases to broaden and normalize recognized stop utterances.
Packaging & CI workflows
pyproject.toml, .github/workflows/...
Add package-data for locale .voc files; bump and extend test extras; add new reusable CI workflows (ovoscope, docs_check, locale_check, repo_health, sync_translations, type_check, release_preview) and adjust coverage test path.
Docs & maintenance metadata
AUDIT.md, FAQ.md, MAINTENANCE_REPORT.md, SUGGESTIONS.md, QUICK_FACTS.md
Document race fixes, performance notes, deferred loading flag, maintenance actions, CI/ovoscope info, and suggestion statuses with dates and file references.
Tests — End-to-end
test/end2end/test_intent_pipeline.py, test/end2end/test_stop_refactor.py, test/end2end/test_stop.py
Add E2E tests for pipeline routing and StopService refactor; update stop tests to accept stop.openvoiceos.stop.response and cover global/skill stop interactions.
Tests — Unit
test/unittests/test_converse_service.py, test/unittests/test_fallback_service.py, test/unittests/test_intent_service_extended.py, test/unittests/test_stop_service.py, test/unittests/test_skill_installer.py, test/unittests/test_transformers.py
Large new unit test suites covering converse/fallback/intent/stop services, skill installer validation/uninstall, and transformer caching/ordering; extensive mocks and behavior assertions.
Misc tests config
.github/workflows/coverage.yml, test/...
Change coverage workflow test_path to test/unittests; add/adjust test ignores for stop responses in tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Utterance Handler
    participant Intent as IntentService
    participant Transformer as Transformer Service
    participant Plugin as Pipeline Matcher
    participant Session as SessionManager

    Client->>Intent: handle_utterance(message)
    Intent->>Transformer: _handle_transformers(utterances, context)
    Transformer->>Transformer: compute/cache sorted plugins (_sorted_plugins)
    Transformer-->>Intent: updated_message
    Intent->>Intent: get_pipeline_matcher(plugin_id) (uses _PIPELINE_MIGRATION_MAP/_PIPELINE_RE)
    Intent->>Plugin: match(utterances, lang)
    alt Match Found
        Plugin-->>Intent: IntentHandlerMatch
        Intent->>Session: update_state/activate skill
        Intent-->>Client: intent handled / utterance-handled
    else No Match
        Intent-->>Client: complete_intent_failure
    end
Loading
sequenceDiagram
    participant Service as FallbackService
    participant Bus as Message Bus
    participant Skill as Fallback Skill
    participant Event as threading.Event

    Service->>Bus: emit(skill.fallback.ping)
    Bus->>Skill: ping
    Service->>Event: clear()
    loop wait cycle
        Service->>Event: wait(timeout=0.02)
        alt Event set
            Skill->>Bus: emit(skill.fallback.pong, {can_handle: true})
            Bus->>Service: handle_ack(msg)
            Service->>Event: set()
            Service->>Service: record skill
        end
    end
    Service-->>Service: return collected_skills
Loading
sequenceDiagram
    participant Installer as SkillInstaller
    participant GitHub as GitHub API

    Installer->>Installer: validate_skill(url)
    Installer->>GitHub: GET /repos/{owner}/{repo}/contents (timeout=3s)
    alt Network error / timeout
        GitHub-->>Installer: error
        Installer-->>Installer: return True (fail-open)
    else 404
        GitHub-->>Installer: 404
        Installer-->>Installer: return False
    else 200 with contents
        GitHub-->>Installer: contents[]
        Installer->>GitHub: GET pyproject.toml or setup.cfg or setup.py
        alt packaging file references Mycroft/CommonPlay
            Installer-->>Installer: return False
        else acceptable packaging found
            Installer-->>Installer: return True
        end
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped through locks and events, threads now sleep in line,

I checked GitHub nests for pyprojects, pip uninstall is fine,
Transformers cache their order, no more needless resort,
StopService learned new manners, locales grew their report,
Tests parade in many files—now everything’s in rhyme.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title partially captures the changeset but misses major components; it mentions 'docs tests and misc optimizations' but the PR includes substantial code refactoring (StopService, SkillManager), race condition fixes, CI workflows, and locale updates—not adequately summarized as 'misc optimizations'. Consider a more specific title like 'refactor: StopService, optimize core services, add comprehensive tests and CI workflows' to better represent the scope of changes including race condition fixes, service refactoring, and test coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch optimize
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

**S-002: Implement skill uninstall (VALID FIX)**
- Implement handle_uninstall_skill() to call pip_uninstall() for skill packages
- Replace 'not implemented' error with actual uninstall logic
- Convert skill_id to package name (dots → hyphens)
- Validate skill parameter before attempting uninstall

**Minor TODO clarifications**
- Docker detection warning in launch_standalone() for container environments
- Clarified voc_match() TODO: explain why StopService reimplements instead of using ovos_workshop
  (StopService is not a skill; voc_match is service-specific)

**Test updates**
- Updated test_handle_uninstall_skill to expect 'no packages to install' instead of 'not implemented'

**S-006 (DEFERRED)**
- Reverted external skills registry implementation
- These TODOs are architectural limitations, not missing features
- External skills run in separate processes and only communicate via messagebus
- They cannot be listed, activated, or deactivated by ovos-core

All 65 unit tests pass.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Mar 12, 2026

Beep! I'm back with the goodies! 🍭

I've aggregated the results of the automated checks for this PR below.

🔒 Security (pip-audit)

Looking for any weak links in the supply chain. ⛓️

✅ No known vulnerabilities found (104 packages scanned).

🔨 Build Tests

Running the final assembly check. 🔧

✅ All versions pass

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

⚖️ License Check

I've checked for any 'all rights reserved' surprises. 🛑

❌ License violations detected (163 packages) — review required before merging.

Dependency                          License Name                                            License Type         Misc                                    
phoonnx:0.5.4                       Error                                                   Error                                                        

License Type                        Found                                                  
Error                               1

License distribution: 33× Apache Software License, 26× MIT License, 25× Apache-2.0, 20× MIT, 15× BSD License, 12× BSD-3-Clause, 3× apache-2.0, 2× GNU Lesser General Public License v3 or later (LGPLv3+), +22 more

Full breakdown — 163 packages
Package Version License URL
astral 3.2 Apache Software License link
attrs 25.4.0 MIT link
audioop-lts 0.2.2 PSF-2.0 link
beautifulsoup4 4.14.3 MIT License link
bidict 0.23.1 Mozilla Public License 2.0 (MPL 2.0) link
bitstruct 8.22.1 MIT link
blinker 1.9.0 MIT License link
bs4 0.0.2 MIT License
build 1.4.0 MIT link
caldav 1.6.0 Apache Software License; GNU General Public License (GPL) link
casttube 0.2.1 MIT link
certifi 2026.2.25 Mozilla Public License 2.0 (MPL 2.0) link
cffi 2.0.0 MIT link
charset-normalizer 3.4.5 MIT link
click 8.3.1 BSD-3-Clause link
combo_lock 0.3.0 Apache Software License link
dateparser 1.3.0 BSD License link
dbus-next 0.2.3 MIT License link
decorator 5.2.1 BSD License
fann2 1.0.7 GNU Lesser General Public License v2 or later (LGPLv2+) link
feedparser 6.0.12 BSD License link
filelock 3.25.2 MIT link
Flask 3.1.3 BSD-3-Clause link
flatbuffers 25.12.19 Apache Software License link
ftfy 6.3.1 Apache-2.0 link
future 1.0.0 MIT License link
geocoder 1.38.1 Apache Software License link
h3 4.4.2 Apache Software License link
icalendar 5.0.14 BSD License link
icepool 2.2.1 MIT License link
idna 3.11 BSD-3-Clause link
ifaddr 0.2.0 MIT License link
importlib_metadata 8.7.1 Apache-2.0 link
itsdangerous 2.2.0 BSD License link
Jinja2 3.1.6 BSD License link
json-database 0.10.1 MIT link
kthread 0.2.3 MIT License link
langcodes 3.5.1 MIT License link
lxml 6.0.2 BSD-3-Clause link
markdown-it-py 4.0.0 MIT License link
MarkupSafe 3.0.3 BSD-3-Clause link
mdurl 0.1.2 MIT License link
memory-tempfile 2.2.3 MIT License link
more-itertools 8.14.0 MIT License link
mpmath 1.3.0 BSD License link
numpy 2.4.3 BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0 link
oauthlib 3.3.1 BSD-3-Clause link
onnxruntime 1.24.3 MIT License link
ovos-audio 1.2.0 Apache-2.0 link
ovos-audio-plugin-mpv 0.2.1 Apache-2.0 link
ovos-config 1.2.2 Apache-2.0 link
ovos-core 2.1.4a1 Apache-2.0 link
ovos-date-parser 0.6.5 Apache Software License link
ovos-dialog-normalizer-plugin 0.0.1 MIT link
ovos-dinkum-listener 0.5.0 Apache Software License link
ovos-lang-parser 0.0.2 Apache Software License link
ovos-messagebus 0.0.10 Apache Software License link
ovos-number-parser 0.5.1 Apache Software License link
ovos-ocp-files-plugin 0.13.1 MIT link
ovos-ocp-m3u-plugin 0.0.1 Apache-2.0 link
ovos-ocp-news-plugin 0.1.2 Apache-2.0 link
ovos-PHAL-plugin-oauth 0.1.3 Apache Software License link
ovos-plugin-manager 2.2.3a1 Apache-2.0 link
ovos-skill-alerts 0.1.28 BSD-3-Clause link
ovos-skill-count 0.0.2 Apache-2.0 link
ovos-skill-date-time 1.1.5 Apache-2.0 link
ovos-skill-diagnostics 0.0.8 Apache-2.0 link
ovos-skill-fallback-unknown 0.1.9 Apache-2.0 link
ovos-skill-hello-world 0.2.1 Apache-2.0 link
ovos-skill-parrot 0.1.25 Apache-2.0 link
ovos-skill-personal 0.1.19 Apache-2.0 link
ovos-skill-randomness 0.1.2 MIT link
ovos-skill-spelling 0.2.6 Apache-2.0 link
ovos-solver-yes-no-plugin 0.2.8 MIT link
ovos-utils 0.8.5 Apache-2.0 link
ovos-utterance-normalizer 0.2.3 Apache Software License link
ovos-vad-plugin-silero 0.1.0 Apache Software License link
ovos-workshop 7.0.6 apache-2.0 link
ovos_audio_plugin_simple 0.1.3 Apache-2.0 link
ovos_bus_client 1.5.0 Apache Software License link
ovos_gui 1.3.4 Apache-2.0 link
ovos_gui_plugin_shell_companion 1.0.6 Apache Software License link
ovos_media_plugin_chromecast 0.1.3 Apache-2.0 link
ovos_media_plugin_spotify 0.2.7 Apache-2.0 link
ovos_microphone_plugin_sounddevice 0.0.2 Apache Software License link
ovos_ocp_rss_plugin 0.1.2 Apache-2.0 link
ovos_ocp_youtube_plugin 0.0.6 Apache-2.0 link
ovos_padatious 1.4.3 Apache Software License link
ovos_PHAL 0.2.12 apache-2.0 link
ovos_phal_plugin_connectivity_events 0.1.3 Apache Software License link
ovos_phal_plugin_ipgeo 0.1.7 Apache Software License link
ovos_plugin_common_play 1.3.1 Apache Software License link
ovos_stt_plugin_server 0.1.3 Apache Software License link
ovos_tts_plugin_piper 0.2.5 Apache Software License link
ovos_tts_plugin_server 0.0.5 Apache Software License link
ovos_ww_plugin_precise_onnx 0.1.0 Apache Software License link
ovos_ww_plugin_vosk 0.1.10 Apache Software License link
packaging 26.0 Apache-2.0 OR BSD-2-Clause link
padacioso 1.0.0 apache-2.0 link
pexpect 4.9.0 ISC License (ISCL) link
phoonnx 0.5.4 UNKNOWN link
pillow 12.1.1 MIT-CMU link
pprintpp 0.4.0 BSD License link
protobuf 7.34.0 3-Clause BSD License link
psutil 7.2.2 BSD-3-Clause link
ptyprocess 0.7.0 ISC License (ISCL) link
PyChromecast 14.0.10 MIT License link
pycparser 3.0 BSD-3-Clause link
pyee 12.1.1 MIT License link
Pygments 2.19.2 BSD License link
pyproject_hooks 1.2.0 MIT License link
PyStemmer 3.0.0 BSD License; MIT License link
python-dateutil 2.9.0.post0 Apache Software License; BSD License link
python-mpv-jsonipc 1.2.1 Apache Software License link
pytube 15.0.0 The Unlicense (Unlicense) link
pytz 2026.1.post1 MIT License link
PyYAML 6.0.3 MIT License link
qrcode 7.3.1 BSD License link
quebra-frases 0.3.7 Apache Software License link
RapidFuzz 3.14.3 MIT link
ratelim 0.1.6 MIT link
recurring-ical-events 3.3.4 GNU Lesser General Public License v3 or later (LGPLv3+) link
redis 7.3.0 MIT link
regex 2026.2.28 Apache-2.0 AND CNRI-Python link
requests 2.32.5 Apache Software License link
rich 13.9.4 MIT License link
rich-click 1.9.7 MIT License

Copyright (c) 2022 Phil Ewels

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
| link |
| scipy | 1.17.1 | BSD License | link |
| sgmllib3k | 1.0.0 | BSD License | link |
| simplematch | 1.4 | MIT License | link |
| six | 1.17.0 | MIT License | link |
| snowballstemmer | 3.0.1 | BSD License | link |
| sonopy | 0.1.2 | UNKNOWN | link |
| sounddevice | 0.5.5 | MIT | link |
| soupsieve | 2.8.3 | MIT | link |
| SpeechRecognition | 3.15.1 | BSD-3-Clause | link |
| spotipy | 2.26.0 | MIT | link |
| srt | 3.5.3 | MIT License | link |
| standard-aifc | 3.13.0 | Python Software Foundation License | link |
| standard-chunk | 3.13.0 | Python Software Foundation License | link |
| sympy | 1.14.0 | BSD License | link |
| timezonefinder | 8.2.1 | The MIT License (MIT)

Copyright (c) 2016 Jannik Michelfeit

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
| link |
| tornado | 6.5.5 | Apache Software License | link |
| tqdm | 4.67.3 | MPL-2.0 AND MIT | link |
| tutubo | 2.0.2 | Apache | link |
| typing_extensions | 4.15.0 | PSF-2.0 | link |
| tzdata | 2025.3 | Apache-2.0 | link |
| tzlocal | 5.3.1 | MIT License | link |
| unicode-rbnf | 2.4.0 | MIT License | |
| urllib3 | 2.6.3 | MIT | link |
| vobject | 0.9.9 | Apache Software License | |
| vosk | 0.3.45 | Apache Software License | link |
| watchdog | 2.3.1 | Apache Software License | link |
| websocket-client | 1.9.0 | Apache Software License | link |
| websockets | 16.0 | BSD-3-Clause | link |
| Werkzeug | 3.1.6 | BSD-3-Clause | link |
| wrapt | 2.1.2 | BSD-2-Clause | link |
| x-wr-timezone | 1.0.1 | GNU Lesser General Public License v3 or later (LGPLv3+) | link |
| xxhash | 3.6.0 | BSD License | link |
| yt-dlp | 2026.3.13 | Unlicense | link |
| ytmusicapi | 1.11.5 | MIT License | link |
| zeroconf | 0.148.0 | LGPL-2.1-or-later | link |
| zipp | 3.23.0 | MIT | link |

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.

📊 Coverage

Mapping the landscape of your tests. 🗺️

⚠️ 60.6% total coverage

Files below 80% coverage (8 files)
File Coverage Missing lines
ovos_core/__init__.py 0.0% 7
ovos_core/__main__.py 0.0% 26
ovos_core/intent_services/__init__.py 0.0% 1
ovos_core/version.py 0.0% 18
ovos_core/skill_installer.py 42.3% 139
ovos_core/intent_services/service.py 49.2% 165
ovos_core/skill_manager.py 58.8% 173
ovos_core/transformers.py 66.0% 49

Full report: download the coverage-report artifact.

🔌 Skill Tests (ovoscope)

Checking if the skill follows our conversational guidelines. 📏

36/36 passed

TestAdaptIntent — 4/4
TestCancelIntentMidSentence — 1/1
TestConverse — 1/1
TestCountSkills — 4/4
TestDeactivate — 3/3
TestFallback — 1/1
TestGlobalStopVocWithActiveSkill — 1/1
TestGlobalStopVocabulary — 2/2
TestIntentPipelineRouting — 4/4
TestLangDisambiguation — 4/4
TestNoSkills — 2/2
TestPadatiousIntent — 4/4
TestStopNoSkills — 3/3
TestStopServiceAsSkill — 1/1
TestStopSkillCanHandleFalse — 1/1

🚌 Bus Coverage

Measuring the reach of our bus handlers. 📏

🔴 Coverage Summary

Metric Status Coverage
Listeners ██░░░░░░░░ 26.3% 59/224 handlers
Emitters ██████████ 100% 60/60 observed
Assertions █████████░ 98% 59/60 asserted

📊 Per-Skill Breakdown

Skill Listeners Observed Asserted
AdaptPipeline 4/7 (57.1%) 0/0 0/0
ConverseService 3/4 (75.0%) 0/0 0/0
FallbackService 2/2 (100.0%) 0/0 0/0
IntentService 3/4 (75.0%) 0/0 0/0
Model2VecIntentPipeline 3/5 (60.0%) 0/0 0/0
PadaciosoPipeline 2/4 (50.0%) 0/0 0/0
PadatiousPipeline 4/8 (50.0%) 0/0 0/0
SkillManager 0/4 (0.0%) 0/0 0/0
__core__ 7/40 (17.5%) 13/13 13/13
ovos-skill-count.openvoiceos 4/20 (20.0%) 8/8 7/8
ovos-skill-fallback-unknown.openvoiceos 3/21 (14.3%) 4/4 4/4
ovos-skill-hello-world.openvoiceos 3/24 (12.5%) 7/7 7/7
ovos-skill-parrot.openvoiceos 9/33 (27.3%) 10/10 10/10
stop.openvoiceos 3/21 (14.3%) 7/7 7/7
test_activation.openvoiceos 9/27 (33.3%) 11/11 11/11
🔍 Detailed Message Type Breakdown

AdaptPipeline

⚠️ Uncovered Listeners:

  • detach_intent
  • intent.service.adapt.get
  • intent.service.adapt.vocab.manifest.get
    ✅ Covered Listeners:
  • detach_skill (1427x)
  • intent.service.adapt.manifest.get (2650x)
  • register_intent (382x)
  • register_vocab (37908x)

ConverseService

⚠️ Uncovered Listeners:

  • intent.service.active_skills.get
    ✅ Covered Listeners:
  • converse:skill (78x)
  • intent.service.skills.activate (5x)
  • intent.service.skills.deactivate (7x)

FallbackService

✅ Covered Listeners:

  • ovos.skills.fallback.deregister (30x)
  • ovos.skills.fallback.register (16x)

IntentService

⚠️ Uncovered Listeners:

  • intent.service.intent.get (Intent)
    ✅ Covered Listeners:
  • intent.service.pipelines.reload (697x)
  • intent.service.skills.deactivate (7x)
  • recognizer_loop:utterance (931x)

Model2VecIntentPipeline

⚠️ Uncovered Listeners:

  • detach_intent
  • mycroft.ready
    ✅ Covered Listeners:
  • detach_skill (1427x)
  • padatious:register_intent (841x)
  • register_intent (382x)

PadaciosoPipeline

⚠️ Uncovered Listeners:

  • padatious:register_entity (Intent)
  • detach_intent
    ✅ Covered Listeners:
  • detach_skill (1427x)
  • padatious:register_intent (841x)

PadatiousPipeline

⚠️ Uncovered Listeners:

  • padatious:register_entity (Intent)
  • detach_intent
  • intent.service.padatious.entities.manifest.get
  • intent.service.padatious.get
    ✅ Covered Listeners:
  • detach_skill (1427x)
  • intent.service.padatious.manifest.get (2650x)
  • mycroft.skills.train (697x)
  • padatious:register_intent (841x)

SkillManager

⚠️ Uncovered Listeners:

  • skillmanager.activate
  • skillmanager.deactivate
  • skillmanager.keep
  • skillmanager.list

__core__

⚠️ Uncovered Listeners:

  • recognizer_loop:audio_output_end (Intent)
  • recognizer_loop:audio_output_start (Intent)
  • recognizer_loop:record_begin (Intent)
  • recognizer_loop:record_end (Intent)
  • add_context
  • clear_context
  • message
  • mycroft.ovos-skill-count.openvoiceos.all_loaded
  • mycroft.ovos-skill-count.openvoiceos.is_alive
  • mycroft.ovos-skill-count.openvoiceos.is_ready
  • mycroft.ovos-skill-fallback-unknown.openvoiceos.all_loaded
  • mycroft.ovos-skill-fallback-unknown.openvoiceos.is_alive
  • mycroft.ovos-skill-fallback-unknown.openvoiceos.is_ready
  • mycroft.ovos-skill-hello-world.openvoiceos.all_loaded
  • mycroft.ovos-skill-hello-world.openvoiceos.is_alive
  • mycroft.ovos-skill-hello-world.openvoiceos.is_ready
  • mycroft.ovos-skill-parrot.openvoiceos.all_loaded
  • mycroft.ovos-skill-parrot.openvoiceos.is_alive
  • mycroft.ovos-skill-parrot.openvoiceos.is_ready
  • mycroft.test_activation.openvoiceos.all_loaded
  • mycroft.test_activation.openvoiceos.is_alive
  • mycroft.test_activation.openvoiceos.is_ready
  • ovos-skill-count.openvoiceos.set
  • ovos-skill-fallback-unknown.openvoiceos.set
  • ovos-skill-hello-world.openvoiceos.set
  • ovos-skill-parrot.openvoiceos.set
  • ovos.session.sync
  • ovos.skills.fallback.force_timeout
  • ovos.skills.fallback.ovos-skill-fallback-unknown.openvoiceos
  • remove_context
  • skill.converse.get_response.disable
  • skill.converse.get_response.enable
  • test_activation.openvoiceos.set
    ✅ Covered Listeners:
  • ovos-skill-count.openvoiceos.stop.response (148x)
  • ovos.session.update_default (700x)
  • ovos.skills.converse.force_timeout (92x)
  • ovos.skills.fallback.pong (15x)
  • ovos.utterance.handled (834x)
  • skill.converse.pong (112x)
  • skill.stop.pong (91x)

📤 Emitters:

  • complete_intent_failure (Asserted ✅)
  • mycroft.audio.play_sound (Asserted ✅)
  • ovos-skill-count.openvoiceos.stop.ping (Asserted ✅)
  • ovos-skill-parrot.openvoiceos.converse.ping (Asserted ✅)
  • ovos.skills.fallback.ovos-skill-fallback-unknown.openvoiceos.request (Asserted ✅)
  • ovos.skills.fallback.ovos-skill-fallback-unknown.openvoiceos.start (Asserted ✅)
  • ovos.skills.fallback.ping (Asserted ✅)
  • ovos.utterance.cancelled (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • recognizer_loop:utterance (Asserted ✅)
  • test_activate (Asserted ✅)
  • test_activation.openvoiceos.converse.ping (Asserted ✅)
  • test_deactivate (Asserted ✅)

ovos-skill-count.openvoiceos

⚠️ Uncovered Listeners:

  • question:action (Intent)
  • question:action.ovos-skill-count.openvoiceos (Intent)
  • question:query (Intent)
  • homescreen.metadata.get
  • mycroft.ovos-skill-count.openvoiceos.all_loaded
  • mycroft.ovos-skill-count.openvoiceos.is_alive
  • mycroft.ovos-skill-count.openvoiceos.is_ready
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • ovos-skill-count.openvoiceos.converse.get_response
  • ovos-skill-count.openvoiceos.set
  • ovos.common_query.ping
  • ovos.skills.settings_changed
    ✅ Covered Listeners:
  • mycroft.stop (189x)
  • ovos-skill-count.openvoiceos.stop (92x)
  • ovos-skill-count.openvoiceos.stop.ping (92x)
  • ovos-skill-count.openvoiceos:count_to_N.intent (186x)

📤 Emitters:

  • mycroft.skill.handler.complete (Asserted ✅)
  • mycroft.skill.handler.start (Asserted ✅)
  • ovos-skill-count.openvoiceos.activate (Asserted ✅)
  • ovos-skill-count.openvoiceos.stop.response (Asserted ✅)
  • ovos-skill-count.openvoiceos:count_to_N.intent (Asserted ✅)
  • ovos.skills.converse.force_timeout (Not Asserted ⚠️)
  • ovos.utterance.handled (Asserted ✅)
  • skill.stop.pong (Asserted ✅)

ovos-skill-fallback-unknown.openvoiceos

⚠️ Uncovered Listeners:

  • question:action (Intent)
  • question:action.ovos-skill-fallback-unknown.openvoiceos (Intent)
  • question:query (Intent)
  • homescreen.metadata.get
  • mycroft.ovos-skill-fallback-unknown.openvoiceos.all_loaded
  • mycroft.ovos-skill-fallback-unknown.openvoiceos.is_alive
  • mycroft.ovos-skill-fallback-unknown.openvoiceos.is_ready
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • ovos-skill-fallback-unknown.openvoiceos.converse.get_response
  • ovos-skill-fallback-unknown.openvoiceos.set
  • ovos-skill-fallback-unknown.openvoiceos.stop
  • ovos-skill-fallback-unknown.openvoiceos.stop.ping
  • ovos.common_query.ping
  • ovos.skills.settings_changed
    ✅ Covered Listeners:
  • mycroft.stop (112x)
  • ovos.skills.fallback.ovos-skill-fallback-unknown.openvoiceos.request (16x)
  • ovos.skills.fallback.ping (16x)

📤 Emitters:

  • ovos.skills.fallback.ovos-skill-fallback-unknown.openvoiceos.response (Asserted ✅)
  • ovos.skills.fallback.pong (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • speak (Asserted ✅)

ovos-skill-hello-world.openvoiceos

⚠️ Uncovered Listeners:

  • ovos-skill-hello-world.openvoiceos:HowAreYou.intent (Intent)
  • ovos-skill-hello-world.openvoiceos:ThankYouIntent (Intent)
  • question:action (Intent)
  • question:action.ovos-skill-hello-world.openvoiceos (Intent)
  • question:query (Intent)
  • hello.world
  • homescreen.metadata.get
  • mycroft.ovos-skill-hello-world.openvoiceos.all_loaded
  • mycroft.ovos-skill-hello-world.openvoiceos.is_alive
  • mycroft.ovos-skill-hello-world.openvoiceos.is_ready
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • ovos-skill-hello-world.openvoiceos.converse.get_response
  • ovos-skill-hello-world.openvoiceos.set
  • ovos-skill-hello-world.openvoiceos.stop
  • ovos-skill-hello-world.openvoiceos.stop.ping
  • ovos.common_query.ping
  • ovos.skills.settings_changed
    ✅ Covered Listeners:
  • mycroft.stop (181x)
  • ovos-skill-hello-world.openvoiceos:Greetings.intent (29x)
  • ovos-skill-hello-world.openvoiceos:HelloWorldIntent (20x)

📤 Emitters:

  • mycroft.skill.handler.complete (Asserted ✅)
  • mycroft.skill.handler.start (Asserted ✅)
  • ovos-skill-hello-world.openvoiceos.activate (Asserted ✅)
  • ovos-skill-hello-world.openvoiceos:Greetings.intent (Asserted ✅)
  • ovos-skill-hello-world.openvoiceos:HelloWorldIntent (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • speak (Asserted ✅)

ovos-skill-parrot.openvoiceos

⚠️ Uncovered Listeners:

  • ovos-skill-parrot.openvoiceos:did.you.hear.me.intent (Intent)
  • ovos-skill-parrot.openvoiceos:repeat.stt.intent (Intent)
  • ovos-skill-parrot.openvoiceos:repeat.tts.intent (Intent)
  • ovos-skill-parrot.openvoiceos:speak.intent (Intent)
  • ovos-skill-parrot.openvoiceos:stop_parrot.intent (Intent)
  • question:action (Intent)
  • question:action.ovos-skill-parrot.openvoiceos (Intent)
  • question:query (Intent)
  • homescreen.metadata.get
  • mycroft.ovos-skill-parrot.openvoiceos.all_loaded
  • mycroft.ovos-skill-parrot.openvoiceos.is_alive
  • mycroft.ovos-skill-parrot.openvoiceos.is_ready
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • ovos-skill-parrot.openvoiceos.converse.get_response
  • ovos-skill-parrot.openvoiceos.deactivate
  • ovos-skill-parrot.openvoiceos.set
  • ovos-skill-parrot.openvoiceos.stop
  • ovos-skill-parrot.openvoiceos.stop.ping
  • ovos.common_query.ping
  • ovos.skills.settings_changed
    ✅ Covered Listeners:
  • intent.service.skills.activated (5x)
  • intent.service.skills.deactivated (7x)
  • mycroft.stop (189x)
  • ovos-skill-parrot.openvoiceos.activate (111x)
  • ovos-skill-parrot.openvoiceos.converse.ping (111x)
  • ovos-skill-parrot.openvoiceos.converse.request (74x)
  • ovos-skill-parrot.openvoiceos:start_parrot.intent (37x)
  • recognizer_loop:utterance (931x)
  • speak (767x)

📤 Emitters:

  • converse:skill (Asserted ✅)
  • mycroft.skill.handler.complete (Asserted ✅)
  • mycroft.skill.handler.start (Asserted ✅)
  • ovos-skill-parrot.openvoiceos.activate (Asserted ✅)
  • ovos-skill-parrot.openvoiceos.converse.request (Asserted ✅)
  • ovos-skill-parrot.openvoiceos:start_parrot.intent (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • skill.converse.pong (Asserted ✅)
  • skill.converse.response (Asserted ✅)
  • speak (Asserted ✅)

stop.openvoiceos

⚠️ Uncovered Listeners:

  • question:action (Intent)
  • question:action.stop.openvoiceos (Intent)
  • question:query (Intent)
  • homescreen.metadata.get
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • mycroft.stop.openvoiceos.all_loaded
  • mycroft.stop.openvoiceos.is_alive
  • mycroft.stop.openvoiceos.is_ready
  • ovos.common_query.ping
  • ovos.skills.settings_changed
  • stop.openvoiceos.converse.get_response
  • stop.openvoiceos.set
  • stop.openvoiceos.stop
  • stop.openvoiceos.stop.ping
    ✅ Covered Listeners:
  • mycroft.stop (189x)
  • stop:global (189x)
  • stop:skill (92x)

📤 Emitters:

  • mycroft.stop (Asserted ✅)
  • ovos-skill-count.openvoiceos.stop (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • stop.openvoiceos.activate (Asserted ✅)
  • stop.openvoiceos.stop.response (Asserted ✅)
  • stop:global (Asserted ✅)
  • stop:skill (Asserted ✅)

test_activation.openvoiceos

⚠️ Uncovered Listeners:

  • question:action (Intent)
  • question:action.test_activation.openvoiceos (Intent)
  • question:query (Intent)
  • homescreen.metadata.get
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • mycroft.test_activation.openvoiceos.all_loaded
  • mycroft.test_activation.openvoiceos.is_alive
  • mycroft.test_activation.openvoiceos.is_ready
  • ovos.common_query.ping
  • ovos.skills.settings_changed
  • test_activation.openvoiceos.converse.get_response
  • test_activation.openvoiceos.set
  • test_activation.openvoiceos.stop
  • test_activation.openvoiceos.stop.ping
    ✅ Covered Listeners:
  • intent.service.skills.activated (5x)
  • intent.service.skills.deactivated (7x)
  • mycroft.stop (35x)
  • test_activate (5x)
  • test_activation.openvoiceos.activate (9x)
  • test_activation.openvoiceos.converse.ping (4x)
  • test_activation.openvoiceos.converse.request (4x)
  • test_activation.openvoiceos.deactivate (7x)
  • test_deactivate (3x)

📤 Emitters:

  • converse:skill (Asserted ✅)
  • intent.service.skills.activate (Asserted ✅)
  • intent.service.skills.activated (Asserted ✅)
  • intent.service.skills.deactivate (Asserted ✅)
  • intent.service.skills.deactivated (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • skill.converse.pong (Asserted ✅)
  • skill.converse.response (Asserted ✅)
  • test_activation.openvoiceos.activate (Asserted ✅)
  • test_activation.openvoiceos.converse.request (Asserted ✅)
  • test_activation.openvoiceos.deactivate (Asserted ✅)

📋 Repo Health

Scanning for any signs of 'deprecated' acne. 🧴

✅ All required files present.

Latest Version: 2.1.4a1

ovos_core/version.py — Version file
README.md — README
LICENSE — License file
pyproject.toml — pyproject.toml
CHANGELOG.md — Changelog
⚠️ requirements.txt — Requirements
ovos_core/version.py has valid version block markers

🏷️ Release Preview

Ensuring the release process is fully automated. 🤖

Current: 2.1.4a1Next: 2.1.4a2

Signal Value
Label ignore-for-release
PR title chore: docs tests and misc optimizations
Bump alpha

✅ PR title follows conventional commit format.


🚀 Release Channel Compatibility

Predicted next version: 2.1.4a2

Channel Status Note Current Constraint
Stable Too new (must be <1.4.0) ovos-core>=1.3.1,<1.4.0
Testing Compatible ovos-core>=2.1.1,<3.0.0
Alpha Compatible ovos-core>=2.1.4a1

The pulse of the OpenVoiceOS codebase 💓

JarbasAl and others added 2 commits March 12, 2026 00:37
…itation

- SUGGESTIONS.md: Mark S-002 as ADDRESSED with implementation details
- SUGGESTIONS.md: Document S-006 as architectural limitation (external skills run in separate processes)
- SUGGESTIONS.md: Explain correct pattern for external skills (self-advertise + respond to bus messages)
- MAINTENANCE_REPORT.md: Add entry for S-002 implementation with rationale on S-006
- All references include commit SHAs and line numbers

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
ovos_core/intent_services/fallback_service.py (1)

43-43: Consider thread-safety for concurrent utterance processing.

The _fallback_response_event is a single shared instance. If multiple utterances reach fallback processing concurrently, their event signals could interfere. This appears safe if _collect_fallback_skills is called sequentially (one utterance at a time through the pipeline), but could cause subtle issues if the design ever changes to allow parallel utterance processing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/fallback_service.py` at line 43, The single shared
threading.Event instance _fallback_response_event is not thread-safe for
concurrent utterance processing; remove the shared instance created in __init__
and instead create a per-utterance Event inside _collect_fallback_skills (or
maintain a dict self._fallback_events keyed by a unique utterance/message id
protected by a threading.Lock and cleaned up after use) and use that per-call
Event wherever _fallback_response_event is referenced so signals for one
utterance cannot affect another.
ovos_core/skill_manager.py (1)

362-365: Convert the blacklist snapshot to a set.

This path still does linear membership checks for every discovered plugin. Converting once here keeps the optimization local and makes the new warning-dedup path cheaper when many plugins are installed.

Proposed refactor
-        blacklist = self.blacklist
+        blacklist = set(self.blacklist)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/skill_manager.py` around lines 362 - 365, Convert the snapshot of
self.blacklist to a set before the loop to make membership checks O(1): replace
the current assignment to blacklist with a set conversion (e.g., blacklist =
set(self.blacklist)) and then keep the existing loop over plugins.items() and
the check against self._logged_skill_warnings unchanged so the deduplication
path benefits from faster lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ovos_core/intent_services/service.py`:
- Around line 321-326: The guard that decides to call create_daemon currently
checks top-level Configuration for "open_data.intent_urls" while
_upload_match_data() reads open_data from the service instance config
initialized from Configuration().get("intents", {}); change the checks so they
read open_data from the same scope (use self.config.get("open_data", {}) rather
than Configuration().get("open_data", {})) before calling create_daemon for
_upload_match_data (and update the other similar check that schedules uploads
near the _upload_match_data call as well) so uploads are scheduled when the
intent-scoped open_data.intent_urls is set.

In `@ovos_core/skill_manager.py`:
- Around line 592-605: The code currently calls skill_loader.instance.shutdown()
and default_shutdown() while holding _plugin_skills_lock; change the flow to
minimize lock hold by acquiring _plugin_skills_lock, look up and remove (or
mark) the skill_loader from plugin_skills for skill_id, then release the lock
and call skill_loader.instance.shutdown() and
skill_loader.instance.default_shutdown() outside the lock; ensure you still
handle exceptions and use the same skill_loader and skill_id values captured
while under the lock so the shutdowns run on the correct object without holding
_plugin_skills_lock.

In `@ovos-hc.py`:
- Around line 15-17: The SERVICE_NAME variable is given a default so the runtime
check "if not SERVICE_NAME" in ovos-hc.py is dead; either remove the default
assignment (so SERVICE_NAME must come from the environment) or remove the
unreachable check block. Concretely, decide whether SERVICE_NAME should be
required: if required, stop assigning the default "voice" and read from
os.environ only so the existing guard remains meaningful; if the default is
intentional, delete the "if not SERVICE_NAME" print/exit block to eliminate dead
code. Ensure references to SERVICE_NAME are updated accordingly.
- Around line 30-42: The client connection is leaked because sys.exit() is
called before client.close(); wrap the readiness check so client.close() always
runs—either move client.close() to execute before any sys.exit() or (preferably)
put the call to client.wait_for_response(ready_msg) and the subsequent status
check inside a try/finally that calls client.close() in the finally block;
reference the existing client.wait_for_response(ready_msg), the printed
"Healthcheck OK"/"Healthcheck FAILED" branches, and the client.close() call to
locate where to add the try/finally or to relocate client.close().

In `@unnamed.patch`:
- Around line 50-55: The dependency constraint for ovos-plugin-manager currently
allows an alpha pre-release (">=2.1.0a1,<3.0.0"); update the version specifier
for ovos-plugin-manager to exclude pre-releases by changing it to
">=2.1.0,<3.0.0" so only stable 2.1.x+ releases are permitted.

---

Nitpick comments:
In `@ovos_core/intent_services/fallback_service.py`:
- Line 43: The single shared threading.Event instance _fallback_response_event
is not thread-safe for concurrent utterance processing; remove the shared
instance created in __init__ and instead create a per-utterance Event inside
_collect_fallback_skills (or maintain a dict self._fallback_events keyed by a
unique utterance/message id protected by a threading.Lock and cleaned up after
use) and use that per-call Event wherever _fallback_response_event is referenced
so signals for one utterance cannot affect another.

In `@ovos_core/skill_manager.py`:
- Around line 362-365: Convert the snapshot of self.blacklist to a set before
the loop to make membership checks O(1): replace the current assignment to
blacklist with a set conversion (e.g., blacklist = set(self.blacklist)) and then
keep the existing loop over plugins.items() and the check against
self._logged_skill_warnings unchanged so the deduplication path benefits from
faster lookups.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c188ffd3-29e2-4e6f-9935-9f39ffcf1c91

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea3f6e and 84d8be1.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .coverage
  • AUDIT.md
  • FAQ.md
  • MAINTENANCE_REPORT.md
  • SUGGESTIONS.md
  • ovos-hc.py
  • ovos_core/intent_services/fallback_service.py
  • ovos_core/intent_services/service.py
  • ovos_core/skill_manager.py
  • ovos_core/transformers.py
  • unnamed.patch

JarbasAl and others added 10 commits March 12, 2026 00:49
…imeout

- converse_service: wrap bus.on/event.wait/bus.remove in try/finally so
  the skill.converse.pong listener is always removed even if handle_ack
  raises; add skill_id guard against malformed pong; change can_handle
  default True→False (non-responding skill should not converse)
- stop_service: same try/finally + skill_id guard + can_handle default
  True→False for skill.stop.pong listener
- service.py: fix LOG.info string concat crash when cancel_word is None
  (use f-string instead of + operator)
- skill_manager: add configurable max_wait to wait_for_intent_service
  (default 300 s via skills.intent_service_timeout); raises descriptive
  RuntimeError with instructions instead of looping forever

Note: sound config caching was not applied — Configuration() is a live
object in OVOS that reflects runtime changes without restart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
StopService now follows the same pattern as CommonQAService and
OCPPipelineMatcher: double-inheriting ConfidenceMatcherPipeline and
OVOSAbstractApplication so that vocabulary loading and voc_match/voc_list
are provided by the shared ovos-workshop infrastructure instead of a
hand-rolled reimplementation.

Changes:
- Add OVOSAbstractApplication to base classes; call both __init__s with
  skill_id="stop.openvoiceos" and resources_dir=dirname(__file__)
- Remove load_resource_files(), _voc_cache dict, _get_closest_lang(),
  and the custom voc_match() override (~60 lines deleted)
- Replace self._voc_cache[lang]['stop'] in match_low with self.voc_list()
- Replace _get_closest_lang() guards in match_* with voc_list() emptiness
  check (voc_list returns [] for unknown langs — no crash, no None sentinel)
- Rename all locale/*.intent files to *.voc so OVOSSkill resource loading
  finds them via the standard ResourceType("vocab", ".voc", ...) path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
27 tests covering:
- _collect_stop_skills: no active skills, can_handle True/False, timeout
  cleanup (try/finally), exception cleanup, malformed pong guard (no
  skill_id), blacklisted skills excluded from ping
- handle_stop_confirmation: error branch, response-mode abort_question,
  converse force_timeout, TTS stop when speaking, skill_id fallback from
  msg_type
- match_high: no vocab → None, stop+no active skills → global stop,
  stop+active skills → skill stop, global_stop voc → global stop
- match_medium: no voc → None, stop/global_stop voc delegates to match_low
- match_low: empty voc_list → None, below threshold → None, active skill
  confidence boost, above threshold → skill stop
- handle_global_stop / handle_skill_stop bus message forwarding
- get_active_skills session delegation
- shutdown removes both bus listeners

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…refactor

Fix existing stop e2e tests:
- Add 'stop.openvoiceos.stop.response' to all ignore_messages lists in
  test_stop.py — StopService now subclasses OVOSAbstractApplication so it
  responds to the mycroft.stop broadcast like other pipeline-plugin skills
  (common_query, ocp, persona already filtered there)

New test file test_stop_refactor.py — 5 tests across 4 classes:

TestGlobalStopVocabulary (no skills loaded):
  - test_global_stop_voc_no_active_skills: 'stop everything' matches
    global_stop.voc and emits stop:global (regression: .voc rename works)
  - test_stop_voc_exact_still_works: bare 'stop' still matches stop.voc
    (regression: .voc rename did not break the stop vocabulary)

TestGlobalStopVocWithActiveSkill (count skill loaded):
  - test_global_stop_voc_with_active_skill: 'stop everything now' emits
    stop:global even when a skill is in the active list — verifying that
    global_stop.voc takes priority over the stop:skill path

TestStopSkillCanHandleFalse (count skill loaded):
  - test_stop_with_active_skill_ping_pong: full stop ping-pong sequence
    with a running skill — verifies stop.ping → skill.stop.pong(can_handle=True)
    → stop:skill → {skill}.stop → {skill}.stop.response chain

TestStopServiceAsSkill (no skills loaded):
  - test_stop_service_emits_activate_and_stop_response: explicitly asserts
    that stop.openvoiceos.activate and stop.openvoiceos.stop.response appear
    in the message sequence, confirming StopService participates in the
    OVOSSkill stop lifecycle

Also installed missing test dependencies:
  ovos-skill-count, ovos-skill-parrot, ovos-skill-hello-world,
  ovos-skill-fallback-unknown, ovos-padatious-pipeline-plugin (from local
  workspace), ovos-adapt-pipeline-plugin (from local workspace),
  ovos-utterance-plugin-cancel (from local workspace)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Parse owner/repo from URL; call api.github.com/repos/{owner}/{repo}/contents/
- Reject repos that don't exist (HTTP 404)
- Reject bare setup.py-only repos (legacy Mycroft packaging)
- Fetch pyproject.toml/setup.cfg and reject if MycroftSkill or CommonPlaySkill found
- Fail-open on network errors and unexpected API status codes (3 s timeout)
- Fix 3 existing tests that assumed no network call (now mock requests.get/validate_skill)
- Add 10 new unit tests covering all validation branches
- Add test_converse_service.py (43 tests, 81% coverage)
- Update FAQ.md and MAINTENANCE_REPORT.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ervice

- test_fallback_service.py: 34 tests, 93% coverage
  - handle_register/deregister_fallback, _fallback_allowed (ACCEPT_ALL/BLACKLIST/WHITELIST),
    _collect_fallback_skills (ping-pong, timeouts, blacklisted sessions),
    _fallback_range, match_high/medium/low delegation, shutdown
- test_transformers.py: 40 tests, 66% coverage
  - All three transformer services (Utterance/Metadata/Intent)
  - Plugin loading, priority ordering + caching, transform chaining,
    exception swallowing, context merging, session key stripping
- test_intent_service_extended.py: 37 tests, raises service.py from 0% to 49%
  - _handle_transformers, disambiguate_lang, get_pipeline_matcher (migration map),
    get_pipeline, context handlers, send_cancel_event/send_complete_intent_failure,
    _emit_match_message, handle_utterance (cancel/no-match), handle_get_intent, shutdown

Total: 247 unit tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 tests covering basic pipeline routing with ovos-skill-count:
- Padatious intent matched end-to-end (full handler lifecycle)
- High-priority pipeline stage handles before lower-priority stages
- Unrecognized utterance produces complete_intent_failure + error sound
- Blacklisted skill falls through to complete_intent_failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regenerate all stop.voc and global_stop.voc files directly from
translations/{lang}/intents.json to ensure they stay in sync.

Changes:
- Preserve phrase order from translations (previously sorted alphabetically)
- Add missing phrases that existed in translations but not locale
- Remove phrases in locale that were not in translations
- Normalize fa-IR -> fa-ir (locale dir is always lowercase)
- nl-NL and nl-nl both exist in translations; nl-nl (canonical) wins
- nl-be has no global_stop translation — global_stop.voc intentionally absent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Created .github/workflows/ovoscope.yml using gh-automations@dev reusable workflow
- Enables bus coverage tracking for behavioural test metrics
- Posts 🔌 Skill Tests (ovoscope) and 🚌 Bus Coverage sections to PR comments
- Requires Adapt and Padatious pipelines for comprehensive intent testing
- Updated FAQ.md with CI/Testing section explaining bus coverage
- Updated QUICK_FACTS.md with testing workflow reference
- Updated MAINTENANCE_REPORT.md with session log

Bus coverage complements code coverage by showing which bus message types
are exercised during tests, helping identify gaps in skill interaction testing.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ovos_core/intent_services/stop_service.py (1)

244-251: ⚠️ Potential issue | 🟠 Major

Fuzzy global_stop phrases lose their semantics here.

Once match_medium() detects global_stop with exact=False, it hands off to match_low(), which only scores against stop.voc and then prefers stop:skill when active skills exist. A non-exact global-stop phrase can therefore stop only one skill—or fall through entirely—instead of emitting stop:global.

Also applies to: 277-304

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/stop_service.py` around lines 244 - 251, The
current flow lets a fuzzy global_stop detected by voc_match(...) fall through to
match_low(...) which only scores against stop.voc and biases stop:skill when
active skills exist, losing the global_stop semantics; change the logic in the
match_medium/match handler (the block using voc_match, get_active_skills, and
calling match_low) so that when is_global_stop is true you either (A)
short-circuit and return an explicit global-stop intent (e.g., the same
structure match_low would return for stop:global) or (B) call match_low with an
additional flag/param that forces matching against the global_stop vocabulary or
forces preference for stop:global over stop:skill; update match_low
signature/behavior accordingly so fuzzy global_stop phrases always map to
stop:global, not to stop:skill or fall-through.
♻️ Duplicate comments (2)
ovos_core/skill_manager.py (1)

600-613: ⚠️ Potential issue | 🟠 Major

Release _plugin_skills_lock before running skill shutdown hooks.

shutdown() and default_shutdown() execute arbitrary skill code. Holding the manager lock while they run can deadlock re-entrant teardown and stalls every other plugin_skills operation.

🔓 Suggested fix
-        with self._plugin_skills_lock:
-            if skill_id in self.plugin_skills:
-                LOG.info('Unloading plugin skill: ' + skill_id)
-                skill_loader = self.plugin_skills[skill_id]
-                if skill_loader.instance is not None:
-                    try:
-                        skill_loader.instance.shutdown()
-                    except Exception:
-                        LOG.exception('Failed to run skill specific shutdown code: ' + skill_loader.skill_id)
-                    try:
-                        skill_loader.instance.default_shutdown()
-                    except Exception:
-                        LOG.exception('Failed to shutdown skill: ' + skill_loader.skill_id)
-                self.plugin_skills.pop(skill_id)
+        with self._plugin_skills_lock:
+            skill_loader = self.plugin_skills.pop(skill_id, None)
+
+        if skill_loader is not None:
+            LOG.info('Unloading plugin skill: ' + skill_id)
+            if skill_loader.instance is not None:
+                try:
+                    skill_loader.instance.shutdown()
+                except Exception:
+                    LOG.exception('Failed to run skill specific shutdown code: ' + skill_loader.skill_id)
+                try:
+                    skill_loader.instance.default_shutdown()
+                except Exception:
+                    LOG.exception('Failed to shutdown skill: ' + skill_loader.skill_id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/skill_manager.py` around lines 600 - 613, The code currently holds
self._plugin_skills_lock while calling skill_loader.instance.shutdown() and
default_shutdown(), risking deadlocks; change the flow in unload logic so you
acquire the lock only to check and remove the SkillLoader from
self.plugin_skills (e.g. get and pop skill_id into a local variable), then
release the lock and call skill_loader.instance.shutdown() and
skill_loader.instance.default_shutdown() outside the lock, preserving the same
try/except logging behavior around those calls; keep references to
_plugin_skills_lock, plugin_skills, skill_loader, shutdown and default_shutdown
to locate and modify the block.
ovos_core/intent_services/service.py (1)

321-326: ⚠️ Potential issue | 🟠 Major

Gate telemetry uploads from the same config scope used by the uploader.

These branches check self.config.get("open_data", {}), but _upload_match_data() reads Configuration().get("open_data", {}). With that mismatch, uploads silently stop depending on where intent_urls is configured.

🛠️ Suggested fix
-            if self.config.get("open_data", {}).get("intent_urls"):
+            if Configuration().get("open_data", {}).get("intent_urls"):
                 create_daemon(self._upload_match_data, (match.utterance,
                                                         match.match_type,
                                                         lang,
                                                         match.match_data))
@@
-            if self.config.get("open_data", {}).get("intent_urls"):
+            if Configuration().get("open_data", {}).get("intent_urls"):
                 create_daemon(self._upload_match_data, (match.utterance,
                                                         "complete_intent_failure",
                                                         lang,
                                                         match.match_data))

Also applies to: 352-357

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/service.py` around lines 321 - 326, The telemetry
upload is gated using self.config.get("open_data", {}) but _upload_match_data()
reads Configuration().get("open_data", {}), causing a mismatch; fix by making
the gating and uploader use the same config source—either modify
_upload_match_data to read from self.config (preferred) or pass the resolved
open_data into create_daemon when calling _upload_match_data (e.g., call
create_daemon(self._upload_match_data, (open_data, match.utterance, ...))), and
update the _upload_match_data signature accordingly so both checks reference the
same config.
🧹 Nitpick comments (17)
ovos_core/intent_services/locale/uk-ua/global_stop.voc (1)

6-15: Consider adding усі variants for recognition robustness.

Optional: add parallel forms with усі (in addition to всі) to improve matching across speaker preference and ASR normalization behavior.

Suggested additions
+припиніть усі процеси
+припиніть усі операції
+скасуйте усі завдання
+скасуйте усі очікуючі операції
+завершіть усі відкриті завдання
+припиніть усі активні дії
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/uk-ua/global_stop.voc` around lines 6 - 15,
Add parallel Ukrainian variants using "усі" alongside existing "всі" entries in
the global_stop.voc vocabulary to improve ASR and speaker-variant matching;
update the entries currently containing "всі" (e.g., "припиніть всі процеси",
"припиніть всі операції", "скасуйте всі завдання", "завершіть всі дії", etc.) by
adding corresponding lines with "усі" (e.g., "припиніть усі процеси", "припиніть
усі операції", "скасуйте усі завдання", "завершіть усі дії", etc.) so each
original phrase has a direct "усі" counterpart.
ovos_core/intent_services/locale/gl-es/global_stop.voc (1)

8-16: Deduplicate repeated stop utterances in this block.

Line 8 duplicates Line 1, Line 15 duplicates Line 10, and Line 16 duplicates Line 9. Removing exact duplicates keeps the vocab clean and avoids accidental weighting bias.

Proposed cleanup
 parar todo
 rematar todo
 finalizar todo
 cancelar todo
 acabar todo
 interromper todo
 deter todo
-parar todo
 paralo todo
 detelo todo
 finalizalo todo
 cancelalo todo
 acabalo todo
 interrompelo todo
-detelo todo
-paralo todo
 Parar todo agora
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/gl-es/global_stop.voc` around lines 8 - 16,
The block contains exact duplicate stop utterances; remove the repeated lines so
each phrase appears only once (specifically deduplicate "parar todo", "paralo
todo", and "detelo todo" so only a single occurrence of each remains), ensure no
extra leading/trailing whitespace or blank lines, and save the updated
global_stop.voc with the remaining unique stop variants intact.
ovos_core/intent_services/locale/ca-es/stop.voc (1)

13-14: Minor inconsistencies in capitalization and punctuation.

  • Line 13: (Atura|para) has inconsistent capitalization within the alternation group. Other lines use consistent casing like (Atura|Para) (lines 15-16) or (atura|para) (line 10).
  • Line 14: Missing comma after "Si us plau" — other polite forms (lines 8 and 11) include the comma: Si us plau,.

These won't affect voice recognition functionality (typically case-insensitive), but could be cleaned up for consistency.

✏️ Suggested consistency fix
-(Atura|para) la comanda actual
-Si us plau (atura|para) la tasca actual
+(Atura|Para) la comanda actual
+Si us plau, (atura|para) la tasca actual
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/ca-es/stop.voc` around lines 13 - 14,
Normalize the capitalization and punctuation in stop.voc: change the alternation
token `(Atura|para)` to match the project's chosen casing (e.g., `(atura|para)`
or `(Atura|Para)`) so it is consistent with other alternations, and add the
missing comma after the polite phrase "Si us plau" so the line reads "Si us
plau, (atura|para) la tasca actual" (adjust casing to the consistent style you
choose).
ovos_core/intent_services/locale/gl-es/stop.voc (1)

9-15: Remove duplicate vocabulary entry.

Parar o proceso en curso appears twice (Line [9] and Line [15]). Keep one entry to avoid redundant weighting/noise.

♻️ Proposed cleanup
 Parar o proceso en curso
 ...
-Parar o proceso en curso
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/gl-es/stop.voc` around lines 9 - 15, Remove
the duplicated vocabulary entry "Parar o proceso en curso" in the stop.voc list
so it appears only once; locate both occurrences of the exact phrase and delete
the redundant line, leaving the rest of the entries and formatting intact.
ovos_core/intent_services/locale/de-de/global_stop.voc (1)

4-5: Pattern on line 4 is redundant.

Line 5 (breche|brech) (alles) ab makes "alles" optional, which means it already matches everything from line 4 ((breche|brech) alles ab). Line 4 can be removed unless the explicit pattern is intentionally kept for readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/de-de/global_stop.voc` around lines 4 - 5,
The pattern "(breche|brech) alles ab" is redundant because "(breche|brech)
(alles) ab" already matches both forms; remove the explicit "(breche|brech)
alles ab" entry from global_stop.voc (or keep it but add a comment explaining
it's for readability) so only the singular pattern "(breche|brech) (alles) ab"
remains to avoid duplicate matches.
ovos_core/intent_services/locale/it-it/global_stop.voc (1)

29-31: Consider removing duplicate generated phrase at Line 30.

Line 30 expands to ... tutte le attività in esecuzione, which is already present at Line 26. Keeping one source avoids duplicate training/matching weight on the same phrase.

✂️ Optional dedupe tweak
-(Interrompi|termina|stoppa|ferma) tutte le (azioni|attività) in (esecuzione|corso)
+(Interrompi|termina|stoppa|ferma) tutte le azioni in (esecuzione|corso)
+(Interrompi|termina|stoppa|ferma) tutte le attività in corso
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/it-it/global_stop.voc` around lines 29 - 31,
Remove the duplicate generated phrase by deleting or modifying the pattern
"(Interrompi|termina|stoppa|ferma) tutte le (azioni|attività) in
(esecuzione|corso)" so it no longer expands to the same utterance as the
existing "(Interrompi|termina|stoppa|ferma) tutte le attività in esecuzione";
update the remaining patterns in global_stop.voc to ensure each line produces
unique phrases and avoid redundant training/matching weight on identical
sentences.
ovos_core/intent_services/locale/fa-ir/stop.voc (1)

15-16: Remove duplicate phrase to keep the lexicon clean.

فعالیت فعلی رو بی‌خیال شو appears twice in this file (existing + changed). Keeping one copy is enough.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/fa-ir/stop.voc` around lines 15 - 16, The
file contains a duplicated vocabulary entry "فعالیت فعلی رو بی‌خیال شو" in the
stop.voc lexicon; remove the redundant occurrence so that only one instance of
"فعالیت فعلی رو بی‌خیال شو" remains (keep the other unique entry "فعالیت فعلی رو
متوقف کن"), ensuring the stop.voc vocabulary has no duplicate phrases.
ovos_core/intent_services/locale/ca-es/global_stop.voc (1)

6-17: Deduplicate repeated stop phrases and equivalent patterns.

This block repeats entries (e.g., acaba-ho tot, cessa tot) and equivalent regex forms ((-ho|) vs (|-ho)). Keep one canonical form per phrase to reduce lexicon noise.

Proposed cleanup
- acaba-ho tot
...
- cessa tot
...
- avorta(|-ho) tot
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/ca-es/global_stop.voc` around lines 6 - 17,
The list in global_stop.voc contains duplicate stop phrases and inconsistent
optional-suffix patterns; deduplicate entries by keeping one canonical form per
phrase (e.g., keep a single "acaba-ho tot" and a single "cessa tot") and
normalize the optional "-ho" pattern across all lines (pick one consistent
pattern instead of mixing "(-ho|)" and "(|-ho)"); update the block so each
unique stop phrase appears only once and all optional-suffix variants use the
same canonical regex form.
ovos_core/intent_services/locale/eu/global_stop.voc (1)

28-31: Drop newly introduced duplicate entries.

gelditu dena, bukatu dena, and bukatu gauza guztiak are already present earlier in the file. Remove duplicates to keep the vocabulary set minimal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/eu/global_stop.voc` around lines 28 - 31,
Remove the duplicate vocabulary lines introduced—specifically drop the entries
"gelditu dena", "bukatu dena", and "bukatu gauza guztiak" from the block that
also contains "gelditu gauza guztiak" so only the original single occurrences
remain; edit the file's vocabulary list in
ovos_core/intent_services/locale/eu/global_stop.voc to delete those duplicate
literal lines and leave the earlier unique entries intact.
ovos_core/intent_services/locale/pl-pl/stop.voc (1)

8-9: Please deduplicate repeated Polish entries.

Zatrzymaj bieżące działanie and Zakończ bieżące działanie are duplicated in this file. Keeping single instances will simplify maintenance.

Also applies to: 16-17

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/pl-pl/stop.voc` around lines 8 - 9, Remove
duplicate Polish entries so each phrase appears only once: locate all
occurrences of "Zatrzymaj bieżące działanie" and "Zakończ bieżące działanie" and
keep a single instance of each (remove the extras), and likewise deduplicate any
repeated variants such as "Zatrzymaj bieżący proces" so the vocabulary file
contains unique lines only; ensure quoting/spacing matches existing entries so
intent matching is unchanged.
ovos_core/intent_services/locale/da-dk/global_stop.voc (1)

1-16: Deduplicate repeated utterances in global_stop vocabulary.

There are exact duplicates in this segment (for example stop alt and afslutte alt). Keeping only unique lines will reduce noise and simplify future maintenance.

♻️ Suggested cleanup
 stop alt
 afslut alt
 afslut alle
 annuller alle
 afslutte alle
 stands alt
 afbryd alt
 ophør med alt
-stop alt
 afslutte alt
-afslutte alt
 annullere alt
-afslutte alt
 standse alt
 afbryde alt
 ophøre med alt
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/da-dk/global_stop.voc` around lines 1 - 16,
The vocabulary contains exact duplicate utterances (e.g., "stop alt" and
"afslutte alt") which should be deduplicated; edit the global_stop vocabulary
block to remove repeated lines so each utterance appears only once, preserving
one instance of each unique Danish phrase and keeping the file as a
newline-separated list of unique utterances (check occurrences of "stop alt",
"afslutte alt", "annuller alle" etc. and remove duplicates).
ovos_core/intent_services/locale/nl-nl/global_stop.voc (1)

1-13: Deduplicate repeated NL-NL global stop lines.

stop alles is repeated several times in this changed segment. Keeping only unique lines will make the vocabulary cleaner and easier to maintain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/nl-nl/global_stop.voc` around lines 1 - 13,
The NL-NL global stop vocabulary contains repeated lines (notably "stop alles"
and other duplicates); edit global_stop.voc to deduplicate entries so each
utterance appears only once (preserve one occurrence of "stop alles", "beëindig
alles", "alles stoppen", "alles annuleren", "alles afmaken", "alles afbreken",
"alles beëindigen", etc.), resulting in a list of unique stop phrases to keep
the vocabulary clean and maintainable.
ovos_core/intent_services/locale/da-dk/stop.voc (1)

15-16: Remove duplicate phrase entry.

Line 15 and Line 16 are identical (Stop den aktuelle handling). One entry is enough.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/intent_services/locale/da-dk/stop.voc` around lines 15 - 16, Remove
the duplicate phrase entry "Stop den aktuelle handling" from the da-dk stop.voc
vocabulary file so the line appears only once; locate the two identical lines
and delete one, ensuring the file contains unique utterances only.
test/unittests/test_skill_installer.py (1)

293-299: Cover the uninstall happy path too.

This now only exercises the empty-input branch. The new uninstall flow—normalizing skill_id, calling pip_uninstall(), and emitting the complete/failed bus message—isn't asserted anywhere in this file, so the core path can regress unnoticed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_skill_installer.py` around lines 293 - 299, Add a new
assertion block to cover the successful uninstall path in
test_handle_uninstall_skill: call skills_store.handle_uninstall_skill with a
Message containing a valid skill id (ensuring the id will be normalized), mock
skills_store.pip_uninstall to return success, assert pip_uninstall was called
with the normalized skill id, assert no play_error_sound was triggered, and
assert the bus emitted the success message type (e.g.,
"ovos.skills.uninstall.complete") with appropriate success data rather than the
failure/error payload.
test/unittests/test_transformers.py (1)

248-336: Add cache-clear coverage for the other transformer services.

The lazy _sorted_plugins cache was added across utterance, metadata, and intent transformers, but only UtteranceTransformersService.load_plugins() is checked for clearing it. Add the same regression test for MetadataTransformersService and IntentTransformersService so stale ordering bugs in those paths don't slip through.

Also applies to: 352-451

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_transformers.py` around lines 248 - 336, Add tests
mirroring the UtteranceTransformersService cache-clear check to cover the other
services: set a sentinel on the service instance's _sorted_plugins, call
MetadataTransformersService.load_plugins and
IntentTransformersService.load_plugins, and assert the sentinel is cleared; use
the same helper mocks/patches as existing tests (patch
find_metadata_transformer_plugins / find_intent_transformer_plugins and
Configuration) and add two new test methods (e.g.
test_metadata_load_plugins_clears_sorted_cache and
test_intent_load_plugins_clears_sorted_cache) in test_transformers.py that set
svc._sorted_plugins to a non-None value, invoke load_plugins(), and assert
svc._sorted_plugins is reset.
test/end2end/test_intent_pipeline.py (1)

99-107: Avoid coupling these routing tests to ovos-skill-count internals.

These cases are validating pipeline routing, but the mycroft.skill.handler.{start,complete} assertions pin the exact Python method name emitted by an external skill. A harmless refactor inside ovos-skill-count.openvoiceos can fail this suite even when routing still works.

Also applies to: 164-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/end2end/test_intent_pipeline.py` around lines 99 - 107, The tests
currently assert exact external-skill internals by expecting Message events with
data name "CountSkill.handle_how_are_you_intent" which couples the routing tests
to ovos-skill-count implementation; change those assertions to check the generic
routing/event identity instead (e.g., assert the "mycroft.skill.handler.start"
and "mycroft.skill.handler.complete" messages are emitted and verify a
non-implementation-specific identifier such as the intent name or skill_id in
data/context rather than the Python method string), updating the Message checks
in test_intent_pipeline.py (the blocks referencing
"mycroft.skill.handler.start"/"mycroft.skill.handler.complete" and
"CountSkill.handle_how_are_you_intent") to validate routing without the exact
method name.
test/end2end/test_stop_refactor.py (1)

196-205: Replace the fixed sleep with an event-based wait.

If the background "count to infinity" handler has not actually reached its running state before the stop utterance is sent, this test exercises the wrong path. time.sleep(2) is both flaky and slower than necessary; wait for a concrete bus event from the skill instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/end2end/test_stop_refactor.py` around lines 196 - 205, The test
currently uses time.sleep(2) after create_daemon(make_it_count), which is flaky;
instead register a temporary listener on self.minicroft.bus that waits for the
concrete bus event emitted by the skill when the background "count to infinity"
handler reaches running (e.g., the skill-specific running/start event), use a
threading.Event (or similar) that the listener sets, call
create_daemon(make_it_count) then wait on that event with a short timeout, and
only proceed to send the stop utterance once the event is set; reference
make_it_count, create_daemon, self.minicroft.bus.emit, session and self.skill_id
when locating where to add the listener and event handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ovos_core/intent_services/locale/de-de/global_stop.voc`:
- Around line 3-16: Remove duplicate vocabulary entries in this vocabulary file
by keeping a single instance of each unique phrase/pattern and deleting repeated
lines; specifically ensure only one occurrence remains for "beende alles",
"(breche|brech) alles ab", "(breche|brech) (alles) ab", "alles anhalten", "alles
abbrechen", "alles aufgeben", "alles stoppen" (preserve the regex variants as
written) so the file contains one canonical line per phrase/pattern and no
duplicate entries.

In `@ovos_core/intent_services/locale/es-es/global_stop.voc`:
- Line 2: The exact-match entry in global_stop.voc currently has a doubled space
("finalizar  todo") which prevents matching when stop_service.py uses
exact=True; update the token to a single space ("finalizar todo") so the intent
matches correctly in the exact lookup used by stop_service.py.

In `@ovos_core/intent_services/locale/it-it/global_stop.voc`:
- Around line 3-25: This file global_stop.voc contains 23 literal `[UNUSED]`
entries which are interpreted by the ovos_workshop vocabulary loader and
expand_template() as optional tokens, producing both "unused" and an empty
utterance—remove or disable these lines by replacing each `[UNUSED]` with a real
stop phrase or prefixing the line with `#` to comment it out so the loader skips
them; ensure no remaining square-bracket placeholders remain in global_stop.voc
to avoid generating unintended stop triggers.

In `@ovos_core/intent_services/locale/it-it/stop.voc`:
- Around line 12-17: Remove all placeholder "[UNUSED]" tokens from the
production vocabulary file and replace them with real Italian stop utterances or
delete the lines entirely so the .voc contains only valid user utterances;
specifically search for the literal token "[UNUSED]" in stop.voc and remove
those entries, preserving file encoding/format and ensuring there are no blank
placeholder lines left behind.

In `@ovos_core/intent_services/locale/pt-br/global_stop.voc`:
- Around line 2-3: Remove the duplicate vocabulary entries in the pt-br
global_stop vocabulary: delete the repeated lines containing "termine
(todos|todas)" and the repeated lines containing "termine tudo" so each phrase
appears only once in ovos_core/intent_services/locale/pt-br/global_stop.voc;
ensure no other identical duplicate tokens remain in that file by scanning for
repeated exact lines and keeping a single instance of each unique phrase.

In `@ovos_core/intent_services/locale/pt-pt/global_stop.voc`:
- Line 21: Fix the typo in the voice pattern by replacing the incorrect token
"taredas" with "tarefas" in the pattern string "(termina|terminar|completar)
todas as taredas" so the locale entry matches "todas as tarefas"; update the
corresponding line in the pt-pt global_stop vocabulary file (the pattern shown)
to "(termina|terminar|completar) todas as tarefas".

In `@ovos_core/intent_services/locale/pt-pt/stop.voc`:
- Line 12: The stop phrase in stop.voc uses "(Pára|pare)o que estás a fazer"
which lacks the required space and, because stop.voc is matched with exact=True,
only matches the concatenated form; update the phrase to "(Pára|pare) o que
estás a fazer" (insert a space after the group) so the file matches the spoken
phrase correctly.

In `@ovos_core/intent_services/stop_service.py`:
- Around line 24-30: Resolve the bus once and pass the same instance to both
base class initializers: compute a single resolved_bus = bus or FakeBus() before
calling OVOSAbstractApplication.__init__ and ConfidenceMatcherPipeline.__init__,
then pass resolved_bus as the bus argument to both so self.bus is consistent and
listeners registered later (the ones attached after initialization) attach to
the same bus instance.

In `@ovos_core/skill_installer.py`:
- Around line 286-306: The current validate_skill block only fetches packaging
files (pyproject.toml/setup.cfg) and looks for legacy_class names, which misses
legacy references in actual Python modules; update validate_skill to follow the
package entry point or scan the repository's Python source instead: determine
the declared entry point from pyproject.toml/setup.cfg (or default package
layout), fetch the referenced module(s) or glob the repo for .py files, then
search those files' contents for "MycroftSkill" and "CommonPlaySkill" (the
legacy_class identifiers) and return False if found; keep the existing
network/error handling and timeouts used for manifest requests but apply them to
fetching the Python files or entry-point target instead.
- Around line 358-364: The handler currently lets exceptions from pip_uninstall
propagate, so wrap the call to pip_uninstall([pkg_name]) in a try/except that
catches subprocess errors (and general exceptions), and on exception call
LOG.error with the exception and emit the failure reply via
self.bus.emit(message.reply("ovos.skills.uninstall.failed", {"error":
InstallError.PIP_ERROR.value})); if pip_uninstall returns False still emit the
same failure reply, and only emit "ovos.skills.uninstall.complete" when
pip_uninstall returns truthy. Use the existing symbols pip_uninstall,
InstallError, self.bus.emit and message.reply to locate and update the logic.

In `@ovos_core/skill_manager.py`:
- Around line 456-471: In wait_for_intent_service(), elapsed is only incremented
by 1 even though each loop may block up to 5s in bus.wait_for_response and 1s in
_stop_event.wait, causing the max_wait check to be incorrect; measure real
elapsed time using time.monotonic() (capture start = time.monotonic() before the
loop and compute elapsed = time.monotonic() - start each iteration) or update
elapsed by the actual durations returned from wait_for_response and _stop_event,
then use that real elapsed to compare against max_wait so the timeout reflects
real wall-clock time (refer to wait_for_intent_service, self._stop_event,
self.bus.wait_for_response, max_wait, elapsed).

In `@test/unittests/test_converse_service.py`:
- Around line 53-160: Tests use time.sleep and a timed t.join to wait for
ack_handler and thread completion, causing flakes; replace that with a
threading.Event: in capture_on call event.set() when installing ack_handler,
then in the test wait on event.wait(timeout) before calling ack_handler, and
after invoking ack_handler use t.join() (no timeout) and assert not t.is_alive()
to ensure the worker finished; update all three tests
(test_skill_responds_can_handle_true_is_included,
test_skill_responds_can_handle_false_excluded,
test_malformed_pong_no_skill_id_is_ignored) that call
svc._collect_converse_skills and use ack_handler to synchronize using this Event
instead of time.sleep and timed join.

In `@test/unittests/test_fallback_service.py`:
- Around line 195-265: The tests test_skill_in_range_receives_ping_and_responds
and test_skill_responds_can_handle_false_excluded are flaky due to time.sleep
and join(timeout); replace the sleep-based synchronization with a
threading.Event: create an Event (e.g., handler_ready) that capture_on sets when
it assigns ack_handler, wait handler_ready.wait(timeout) before invoking
ack_handler, and have the worker thread set another Event (e.g., done) or simply
call t.join() (no timeout) after signaling to ensure completion; remove
time.sleep and the join(timeout=1) usage and assert the thread has finished
before reading result_holder so svc._collect_fallback_skills (and ack_handler)
are deterministically synchronized.

In `@test/unittests/test_intent_service_extended.py`:
- Around line 30-53: The test helper _make_service creates an IntentService
without running IntentService.__init__, so the FakeBus never gets the same
registered handlers that shutdown() expects; update _make_service to either call
IntentService.__init__ (with stubs) or explicitly register the same listener
names the real init creates on the FakeBus (the handlers removed in
IntentService.shutdown), and add the missing 'intent.service.intent.get'
listener to the test's assertions so the shutdown harness matches the real
listener set; reference _make_service, IntentService.__init__, shutdown(),
FakeBus and the 'intent.service.intent.get' listener when making the changes.

In `@test/unittests/test_skill_installer.py`:
- Around line 199-210: The current behavior returns True (fail open) on
ConnectionError or non-404 GitHub responses which bypasses validate_skill;
change validate_skill to treat network exceptions and non-404 API errors as
validation failures (return False or raise so pip_install sees a retryable
install failure) instead of True. Update the tests
(test_validate_skill_network_error_fail_open and
test_validate_skill_unexpected_api_error_fail_open) to expect False (or an
appropriate exception) and ensure validate_skill's implementation catches
requests exceptions and non-404 responses and returns False (or re-raises a
specific install error) so pip_install will not accept unvalidated repos.

In `@test/unittests/test_stop_service.py`:
- Around line 169-210: The test currently sends a malformed pong so handle_ack()
returns early; instead simulate the exception path by invoking the ack handler
with a well-formed Message that passes the guard (include skill_id) and force
the handler to raise (e.g., patch or monkeypatch StopService.handle_ack or wrap
ack_handler so it raises when called) so the exception path inside
svc._collect_stop_skills is exercised; ensure you still capture the ack_handler
from svc.bus.on, call it with Message("skill.stop.pong", {"skill_id":
"bad_skill"}) and have the handler raise an Exception, then assert
svc.bus.remove.assert_called_once() as before.
- Around line 510-519: The test test_returns_skill_ids_in_order only asserts
membership but must assert ordering per documented contract: after activating
"skill_b" then "skill_a" the returned list from
StopService.get_active_skills(Message("test")) should have "skill_a" before
"skill_b". Update the assertions to check the exact order (e.g., compare result
to the expected ordered list or assert result.index("skill_a") <
result.index("skill_b")) by locating the test_returns_skill_ids_in_order
function, the Session.activate_skill calls, and the
StopService.get_active_skills invocation.

---

Outside diff comments:
In `@ovos_core/intent_services/stop_service.py`:
- Around line 244-251: The current flow lets a fuzzy global_stop detected by
voc_match(...) fall through to match_low(...) which only scores against stop.voc
and biases stop:skill when active skills exist, losing the global_stop
semantics; change the logic in the match_medium/match handler (the block using
voc_match, get_active_skills, and calling match_low) so that when is_global_stop
is true you either (A) short-circuit and return an explicit global-stop intent
(e.g., the same structure match_low would return for stop:global) or (B) call
match_low with an additional flag/param that forces matching against the
global_stop vocabulary or forces preference for stop:global over stop:skill;
update match_low signature/behavior accordingly so fuzzy global_stop phrases
always map to stop:global, not to stop:skill or fall-through.

---

Duplicate comments:
In `@ovos_core/intent_services/service.py`:
- Around line 321-326: The telemetry upload is gated using
self.config.get("open_data", {}) but _upload_match_data() reads
Configuration().get("open_data", {}), causing a mismatch; fix by making the
gating and uploader use the same config source—either modify _upload_match_data
to read from self.config (preferred) or pass the resolved open_data into
create_daemon when calling _upload_match_data (e.g., call
create_daemon(self._upload_match_data, (open_data, match.utterance, ...))), and
update the _upload_match_data signature accordingly so both checks reference the
same config.

In `@ovos_core/skill_manager.py`:
- Around line 600-613: The code currently holds self._plugin_skills_lock while
calling skill_loader.instance.shutdown() and default_shutdown(), risking
deadlocks; change the flow in unload logic so you acquire the lock only to check
and remove the SkillLoader from self.plugin_skills (e.g. get and pop skill_id
into a local variable), then release the lock and call
skill_loader.instance.shutdown() and skill_loader.instance.default_shutdown()
outside the lock, preserving the same try/except logging behavior around those
calls; keep references to _plugin_skills_lock, plugin_skills, skill_loader,
shutdown and default_shutdown to locate and modify the block.

---

Nitpick comments:
In `@ovos_core/intent_services/locale/ca-es/global_stop.voc`:
- Around line 6-17: The list in global_stop.voc contains duplicate stop phrases
and inconsistent optional-suffix patterns; deduplicate entries by keeping one
canonical form per phrase (e.g., keep a single "acaba-ho tot" and a single
"cessa tot") and normalize the optional "-ho" pattern across all lines (pick one
consistent pattern instead of mixing "(-ho|)" and "(|-ho)"); update the block so
each unique stop phrase appears only once and all optional-suffix variants use
the same canonical regex form.

In `@ovos_core/intent_services/locale/ca-es/stop.voc`:
- Around line 13-14: Normalize the capitalization and punctuation in stop.voc:
change the alternation token `(Atura|para)` to match the project's chosen casing
(e.g., `(atura|para)` or `(Atura|Para)`) so it is consistent with other
alternations, and add the missing comma after the polite phrase "Si us plau" so
the line reads "Si us plau, (atura|para) la tasca actual" (adjust casing to the
consistent style you choose).

In `@ovos_core/intent_services/locale/da-dk/global_stop.voc`:
- Around line 1-16: The vocabulary contains exact duplicate utterances (e.g.,
"stop alt" and "afslutte alt") which should be deduplicated; edit the
global_stop vocabulary block to remove repeated lines so each utterance appears
only once, preserving one instance of each unique Danish phrase and keeping the
file as a newline-separated list of unique utterances (check occurrences of
"stop alt", "afslutte alt", "annuller alle" etc. and remove duplicates).

In `@ovos_core/intent_services/locale/da-dk/stop.voc`:
- Around line 15-16: Remove the duplicate phrase entry "Stop den aktuelle
handling" from the da-dk stop.voc vocabulary file so the line appears only once;
locate the two identical lines and delete one, ensuring the file contains unique
utterances only.

In `@ovos_core/intent_services/locale/de-de/global_stop.voc`:
- Around line 4-5: The pattern "(breche|brech) alles ab" is redundant because
"(breche|brech) (alles) ab" already matches both forms; remove the explicit
"(breche|brech) alles ab" entry from global_stop.voc (or keep it but add a
comment explaining it's for readability) so only the singular pattern
"(breche|brech) (alles) ab" remains to avoid duplicate matches.

In `@ovos_core/intent_services/locale/eu/global_stop.voc`:
- Around line 28-31: Remove the duplicate vocabulary lines
introduced—specifically drop the entries "gelditu dena", "bukatu dena", and
"bukatu gauza guztiak" from the block that also contains "gelditu gauza guztiak"
so only the original single occurrences remain; edit the file's vocabulary list
in ovos_core/intent_services/locale/eu/global_stop.voc to delete those duplicate
literal lines and leave the earlier unique entries intact.

In `@ovos_core/intent_services/locale/fa-ir/stop.voc`:
- Around line 15-16: The file contains a duplicated vocabulary entry "فعالیت
فعلی رو بی‌خیال شو" in the stop.voc lexicon; remove the redundant occurrence so
that only one instance of "فعالیت فعلی رو بی‌خیال شو" remains (keep the other
unique entry "فعالیت فعلی رو متوقف کن"), ensuring the stop.voc vocabulary has no
duplicate phrases.

In `@ovos_core/intent_services/locale/gl-es/global_stop.voc`:
- Around line 8-16: The block contains exact duplicate stop utterances; remove
the repeated lines so each phrase appears only once (specifically deduplicate
"parar todo", "paralo todo", and "detelo todo" so only a single occurrence of
each remains), ensure no extra leading/trailing whitespace or blank lines, and
save the updated global_stop.voc with the remaining unique stop variants intact.

In `@ovos_core/intent_services/locale/gl-es/stop.voc`:
- Around line 9-15: Remove the duplicated vocabulary entry "Parar o proceso en
curso" in the stop.voc list so it appears only once; locate both occurrences of
the exact phrase and delete the redundant line, leaving the rest of the entries
and formatting intact.

In `@ovos_core/intent_services/locale/it-it/global_stop.voc`:
- Around line 29-31: Remove the duplicate generated phrase by deleting or
modifying the pattern "(Interrompi|termina|stoppa|ferma) tutte le
(azioni|attività) in (esecuzione|corso)" so it no longer expands to the same
utterance as the existing "(Interrompi|termina|stoppa|ferma) tutte le attività
in esecuzione"; update the remaining patterns in global_stop.voc to ensure each
line produces unique phrases and avoid redundant training/matching weight on
identical sentences.

In `@ovos_core/intent_services/locale/nl-nl/global_stop.voc`:
- Around line 1-13: The NL-NL global stop vocabulary contains repeated lines
(notably "stop alles" and other duplicates); edit global_stop.voc to deduplicate
entries so each utterance appears only once (preserve one occurrence of "stop
alles", "beëindig alles", "alles stoppen", "alles annuleren", "alles afmaken",
"alles afbreken", "alles beëindigen", etc.), resulting in a list of unique stop
phrases to keep the vocabulary clean and maintainable.

In `@ovos_core/intent_services/locale/pl-pl/stop.voc`:
- Around line 8-9: Remove duplicate Polish entries so each phrase appears only
once: locate all occurrences of "Zatrzymaj bieżące działanie" and "Zakończ
bieżące działanie" and keep a single instance of each (remove the extras), and
likewise deduplicate any repeated variants such as "Zatrzymaj bieżący proces" so
the vocabulary file contains unique lines only; ensure quoting/spacing matches
existing entries so intent matching is unchanged.

In `@ovos_core/intent_services/locale/uk-ua/global_stop.voc`:
- Around line 6-15: Add parallel Ukrainian variants using "усі" alongside
existing "всі" entries in the global_stop.voc vocabulary to improve ASR and
speaker-variant matching; update the entries currently containing "всі" (e.g.,
"припиніть всі процеси", "припиніть всі операції", "скасуйте всі завдання",
"завершіть всі дії", etc.) by adding corresponding lines with "усі" (e.g.,
"припиніть усі процеси", "припиніть усі операції", "скасуйте усі завдання",
"завершіть усі дії", etc.) so each original phrase has a direct "усі"
counterpart.

In `@test/end2end/test_intent_pipeline.py`:
- Around line 99-107: The tests currently assert exact external-skill internals
by expecting Message events with data name
"CountSkill.handle_how_are_you_intent" which couples the routing tests to
ovos-skill-count implementation; change those assertions to check the generic
routing/event identity instead (e.g., assert the "mycroft.skill.handler.start"
and "mycroft.skill.handler.complete" messages are emitted and verify a
non-implementation-specific identifier such as the intent name or skill_id in
data/context rather than the Python method string), updating the Message checks
in test_intent_pipeline.py (the blocks referencing
"mycroft.skill.handler.start"/"mycroft.skill.handler.complete" and
"CountSkill.handle_how_are_you_intent") to validate routing without the exact
method name.

In `@test/end2end/test_stop_refactor.py`:
- Around line 196-205: The test currently uses time.sleep(2) after
create_daemon(make_it_count), which is flaky; instead register a temporary
listener on self.minicroft.bus that waits for the concrete bus event emitted by
the skill when the background "count to infinity" handler reaches running (e.g.,
the skill-specific running/start event), use a threading.Event (or similar) that
the listener sets, call create_daemon(make_it_count) then wait on that event
with a short timeout, and only proceed to send the stop utterance once the event
is set; reference make_it_count, create_daemon, self.minicroft.bus.emit, session
and self.skill_id when locating where to add the listener and event handling.

In `@test/unittests/test_skill_installer.py`:
- Around line 293-299: Add a new assertion block to cover the successful
uninstall path in test_handle_uninstall_skill: call
skills_store.handle_uninstall_skill with a Message containing a valid skill id
(ensuring the id will be normalized), mock skills_store.pip_uninstall to return
success, assert pip_uninstall was called with the normalized skill id, assert no
play_error_sound was triggered, and assert the bus emitted the success message
type (e.g., "ovos.skills.uninstall.complete") with appropriate success data
rather than the failure/error payload.

In `@test/unittests/test_transformers.py`:
- Around line 248-336: Add tests mirroring the UtteranceTransformersService
cache-clear check to cover the other services: set a sentinel on the service
instance's _sorted_plugins, call MetadataTransformersService.load_plugins and
IntentTransformersService.load_plugins, and assert the sentinel is cleared; use
the same helper mocks/patches as existing tests (patch
find_metadata_transformer_plugins / find_intent_transformer_plugins and
Configuration) and add two new test methods (e.g.
test_metadata_load_plugins_clears_sorted_cache and
test_intent_load_plugins_clears_sorted_cache) in test_transformers.py that set
svc._sorted_plugins to a non-None value, invoke load_plugins(), and assert
svc._sorted_plugins is reset.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97ea9b2f-c4a6-464b-9d83-dc6fd6a6ba6b

📥 Commits

Reviewing files that changed from the base of the PR and between 84d8be1 and 999f8ed.

📒 Files selected for processing (49)
  • FAQ.md
  • MAINTENANCE_REPORT.md
  • SUGGESTIONS.md
  • ovos_core/intent_services/converse_service.py
  • ovos_core/intent_services/locale/ca-es/global_stop.voc
  • ovos_core/intent_services/locale/ca-es/stop.voc
  • ovos_core/intent_services/locale/da-dk/global_stop.voc
  • ovos_core/intent_services/locale/da-dk/stop.voc
  • ovos_core/intent_services/locale/de-de/global_stop.voc
  • ovos_core/intent_services/locale/de-de/stop.voc
  • ovos_core/intent_services/locale/en-us/global_stop.voc
  • ovos_core/intent_services/locale/en-us/stop.voc
  • ovos_core/intent_services/locale/es-es/global_stop.voc
  • ovos_core/intent_services/locale/es-es/stop.voc
  • ovos_core/intent_services/locale/eu/global_stop.voc
  • ovos_core/intent_services/locale/eu/stop.voc
  • ovos_core/intent_services/locale/fa-ir/global_stop.voc
  • ovos_core/intent_services/locale/fa-ir/stop.voc
  • ovos_core/intent_services/locale/fr-fr/global_stop.voc
  • ovos_core/intent_services/locale/fr-fr/stop.voc
  • ovos_core/intent_services/locale/gl-es/global_stop.voc
  • ovos_core/intent_services/locale/gl-es/stop.voc
  • ovos_core/intent_services/locale/it-it/global_stop.voc
  • ovos_core/intent_services/locale/it-it/stop.voc
  • ovos_core/intent_services/locale/nl-be/stop.voc
  • ovos_core/intent_services/locale/nl-nl/global_stop.voc
  • ovos_core/intent_services/locale/nl-nl/stop.voc
  • ovos_core/intent_services/locale/pl-pl/global_stop.voc
  • ovos_core/intent_services/locale/pl-pl/stop.voc
  • ovos_core/intent_services/locale/pt-br/global_stop.voc
  • ovos_core/intent_services/locale/pt-br/stop.voc
  • ovos_core/intent_services/locale/pt-pt/global_stop.voc
  • ovos_core/intent_services/locale/pt-pt/stop.voc
  • ovos_core/intent_services/locale/uk-ua/global_stop.voc
  • ovos_core/intent_services/locale/uk-ua/stop.voc
  • ovos_core/intent_services/service.py
  • ovos_core/intent_services/stop_service.py
  • ovos_core/skill_installer.py
  • ovos_core/skill_manager.py
  • pyproject.toml
  • test/end2end/test_intent_pipeline.py
  • test/end2end/test_stop.py
  • test/end2end/test_stop_refactor.py
  • test/unittests/test_converse_service.py
  • test/unittests/test_fallback_service.py
  • test/unittests/test_intent_service_extended.py
  • test/unittests/test_skill_installer.py
  • test/unittests/test_stop_service.py
  • test/unittests/test_transformers.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
ovos_core/skill_installer.py (1)

336-342: ⚠️ Potential issue | 🟠 Major

Uncaught exception from pip_uninstall prevents failure reply.

pip_uninstall() raises RuntimeError on subprocess failures (line 219). This exception escapes the handler, so clients never receive ovos.skills.uninstall.failed. This was flagged in a previous review and remains unaddressed.

🐛 Proposed fix to catch exceptions
         # Treat skill_id as a package name (e.g., 'skill-name.author' -> 'skill-name-author')
         # or accept directly as package name
         pkg_name = skill.replace(".", "-") if "." in skill else skill

-        if self.pip_uninstall([pkg_name]):
+        try:
+            success = self.pip_uninstall([pkg_name])
+        except Exception:
+            LOG.exception(f"Failed to uninstall skill: {skill}")
+            self.bus.emit(message.reply("ovos.skills.uninstall.failed",
+                                        {"error": InstallError.PIP_ERROR.value}))
+            return
+
+        if success:
             LOG.info(f"Successfully uninstalled skill: {skill}")
             self.bus.emit(message.reply("ovos.skills.uninstall.complete"))
         else:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ovos_core/skill_installer.py` around lines 336 - 342, Wrap the call to
pip_uninstall in a try/except so RuntimeError from pip_uninstall doesn't escape;
in the uninstall handler (the method containing the shown snippet) catch
RuntimeError (or Exception) around self.pip_uninstall([pkg_name]), log the
exception via LOG.exception or LOG.error including the error details, and in the
except block emit the failure reply using
self.bus.emit(message.reply("ovos.skills.uninstall.failed", {"error":
InstallError.PIP_ERROR.value})); keep the existing success branch unchanged.
🧹 Nitpick comments (2)
test/unittests/test_skill_installer.py (2)

9-9: Use explicit None union for optional parameter.

PEP 484 prohibits implicit Optional. The type hint should explicitly indicate None is allowed.

♻️ Proposed fix
-def _make_github_response(status_code: int = 200, file_names: list = None,
+def _make_github_response(status_code: int = 200, file_names: list | None = None,
                           ok: bool = True) -> MagicMock:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_skill_installer.py` at line 9, The helper function
_make_github_response currently types file_names as "list" with a default of
None; update its signature to explicitly allow None (e.g., use
Optional[List[str]] or list[str] | None) and add the required typing imports
(Optional and List if using typing) so the parameter type follows PEP 484 and
clearly indicates the optional nature of file_names.

267-271: Error message says "install" in an uninstall context.

The test asserts "no packages to install" for an uninstall failure, which is confusing. The InstallError.NO_PKGS enum is reused for both install and uninstall, but its value is install-specific. Consider whether a separate error value or message would improve clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unittests/test_skill_installer.py` around lines 267 - 271, The test
asserts an uninstall failure but the error text uses an install-specific
message; update the handling to produce an uninstall-appropriate message: either
change the InstallError enum to add a distinct value (e.g.,
InstallError.NO_PKGS_UNINSTALL) or adjust handle_uninstall_skill to map
InstallError.NO_PKGS to "no packages to uninstall" (or otherwise translate the
enum) so skills_store.handle_uninstall_skill returns/dispatches "no packages to
uninstall" instead of "no packages to install"; reference InstallError.NO_PKGS
and the handle_uninstall_skill function to locate the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ovos_core/skill_installer.py`:
- Around line 229-250: The docstring for the GitHub validation routine in
ovos_core/skill_installer.py is inaccurate: it claims the function scans
pyproject.toml/setup.cfg for legacy class names but the implementation only
checks file presence. Either update the docstring to remove/clarify the
legacy-class scanning claim, or implement the scanning behavior in the
validation function (validate_github_repo) by fetching the contents of
pyproject.toml, setup.cfg or setup.py via the GitHub contents API (respecting
the existing 3s timeout/fallback behavior) and searching for the identifiers
"MycroftSkill" and "CommonPlaySkill", returning False if found; keep existing
repo-existence and packaging-file presence checks intact.

In `@test/unittests/test_skill_installer.py`:
- Around line 185-207: The tests test_validate_skill_setup_cfg_valid and
test_validate_skill_dot_git_suffix_stripped mock two HTTP responses but the
current validate_skill implementation only performs the GitHub contents request,
so remove the unused second mock response from mock_get.side_effect in both
tests (delete the _make_manifest_response(...) entries) and adjust assertions if
needed; locate these changes in the test functions
test_validate_skill_setup_cfg_valid and
test_validate_skill_dot_git_suffix_stripped and update only the
mock_get.side_effect arrays so they match the actual single requests made by
validate_skill in ovos_core.skill_installer.
- Around line 155-162: The test test_validate_skill_valid_ovos_skill currently
supplies two mocked responses for requests.get but validate_skill no longer
fetches file contents a second time; update the test to match the current
validate_skill behavior by removing the unused manifest mock and only mocking
the single repository contents API call (adjust mock_get.side_effect to a single
response produced by _make_github_response with
file_names=["pyproject.toml","README.md"]) so the test exercises validate_skill
as implemented; keep the existing assert that
skills_store.validate_skill("https://github.com/openvoiceos/skill-foo") is True
and reference the test function name and the validate_skill method when making
the change.

---

Duplicate comments:
In `@ovos_core/skill_installer.py`:
- Around line 336-342: Wrap the call to pip_uninstall in a try/except so
RuntimeError from pip_uninstall doesn't escape; in the uninstall handler (the
method containing the shown snippet) catch RuntimeError (or Exception) around
self.pip_uninstall([pkg_name]), log the exception via LOG.exception or LOG.error
including the error details, and in the except block emit the failure reply
using self.bus.emit(message.reply("ovos.skills.uninstall.failed", {"error":
InstallError.PIP_ERROR.value})); keep the existing success branch unchanged.

---

Nitpick comments:
In `@test/unittests/test_skill_installer.py`:
- Line 9: The helper function _make_github_response currently types file_names
as "list" with a default of None; update its signature to explicitly allow None
(e.g., use Optional[List[str]] or list[str] | None) and add the required typing
imports (Optional and List if using typing) so the parameter type follows PEP
484 and clearly indicates the optional nature of file_names.
- Around line 267-271: The test asserts an uninstall failure but the error text
uses an install-specific message; update the handling to produce an
uninstall-appropriate message: either change the InstallError enum to add a
distinct value (e.g., InstallError.NO_PKGS_UNINSTALL) or adjust
handle_uninstall_skill to map InstallError.NO_PKGS to "no packages to uninstall"
(or otherwise translate the enum) so skills_store.handle_uninstall_skill
returns/dispatches "no packages to uninstall" instead of "no packages to
install"; reference InstallError.NO_PKGS and the handle_uninstall_skill function
to locate the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be0f0b98-6148-47eb-9bd8-5b6436baefaf

📥 Commits

Reviewing files that changed from the base of the PR and between 999f8ed and ae50c9b.

📒 Files selected for processing (8)
  • .github/workflows/coverage.yml
  • .github/workflows/ovoscope.yml
  • FAQ.md
  • MAINTENANCE_REPORT.md
  • QUICK_FACTS.md
  • ovos_core/skill_installer.py
  • pyproject.toml
  • test/unittests/test_skill_installer.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/docs_check.yml:
- Around line 10-11: Replace the mutable branch reference in the reusable
workflow invocation by pinning the `uses:
OpenVoiceOS/gh-automations/.github/workflows/docs-check.yml@dev` to a full
commit SHA (e.g., change `@dev` to `@<commit-sha>`) and keep `secrets: inherit`
only if you trust that fixed SHA; update the same pattern in other workflows
that reference OpenVoiceOS/gh-automations (type_check.yml, repo_health.yml,
sync_translations.yml, release_workflow.yml, release_preview.yml,
publish_stable.yml, pipaudit.yml, ovoscope.yml, locale_check.yml) so all `uses:`
lines point to immutable commit SHAs instead of `@dev`.

In @.github/workflows/locale_check.yml:
- Around line 10-11: Replace the mutable ref "@dev" used in the reusable
workflow call with an immutable commit SHA: update the uses line (uses:
OpenVoiceOS/gh-automations/.github/workflows/locale-check.yml@dev) to reference
the full commit SHA of the target repo, keep secrets: inherit as-is, and verify
the chosen SHA corresponds to the intended workflow commit in
OpenVoiceOS/gh-automations to ensure the workflow is pinned immutably.

In @.github/workflows/repo_health.yml:
- Around line 10-11: Replace the mutable branch ref in the reusable workflow
invocation by pinning the `uses:
OpenVoiceOS/gh-automations/.github/workflows/repo-health.yml@dev` entry to the
specific commit SHA of the external workflow, and remove `secrets:
inherit`—instead explicitly pass only the required secrets (e.g. build/repo
tokens needed by this workflow) by name; update the `uses:` line to the commit
SHA and add a `secrets:` mapping that lists each required secret rather than
inheriting all secrets.

In @.github/workflows/sync_translations.yml:
- Around line 13-14: The workflow currently references a mutable branch in the
reusable workflow via the line starting with "uses:
OpenVoiceOS/gh-automations/.github/workflows/sync-translations.yml@dev" which is
unsafe when combined with "secrets: inherit"; replace the branch ref "@dev" with
the corresponding immutable full commit SHA for that reusable workflow so the
"uses" entry pins to a specific commit, keeping the "secrets: inherit" behavior
unchanged.

In @.github/workflows/type_check.yml:
- Around line 10-11: Replace the mutable branch reference in the reusable
workflow invocation "uses:
OpenVoiceOS/gh-automations/.github/workflows/type-check.yml@dev" with an
immutable commit SHA (e.g. @<commit-sha>) so the workflow is pinned; update the
line that currently reads "uses:
OpenVoiceOS/gh-automations/.github/workflows/type-check.yml@dev" to "uses:
OpenVoiceOS/gh-automations/.github/workflows/type-check.yml@<commit-sha>" and
keep "secrets: inherit" as-is; apply the same SHA-pinning pattern to the other
occurrences of the same reusable workflow references across the repo.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52a7ec4b-a4d1-4938-9049-24b03a047fe8

📥 Commits

Reviewing files that changed from the base of the PR and between ae50c9b and f121f37.

📒 Files selected for processing (6)
  • .github/workflows/docs_check.yml
  • .github/workflows/locale_check.yml
  • .github/workflows/release_preview.yml
  • .github/workflows/repo_health.yml
  • .github/workflows/sync_translations.yml
  • .github/workflows/type_check.yml
✅ Files skipped from review due to trivial changes (1)
  • .github/workflows/release_preview.yml

@JarbasAl JarbasAl changed the title Optimize chore: docs tests and misc optimizations Mar 14, 2026
@JarbasAl JarbasAl merged commit 6d87f1d into dev Mar 14, 2026
12 of 14 checks passed
@JarbasAl JarbasAl deleted the optimize branch March 14, 2026 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant