legionforge-convobox 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. legionforge_convobox-0.4.0/.gitattributes +15 -0
  2. legionforge_convobox-0.4.0/.githooks/pre-push +29 -0
  3. legionforge_convobox-0.4.0/.github/CODEOWNERS +6 -0
  4. legionforge_convobox-0.4.0/.github/dependabot.yml +15 -0
  5. legionforge_convobox-0.4.0/.github/pull_request_template.md +35 -0
  6. legionforge_convobox-0.4.0/.github/workflows/ai-attribution.yml +89 -0
  7. legionforge_convobox-0.4.0/.github/workflows/ci.yml +81 -0
  8. legionforge_convobox-0.4.0/.github/workflows/publish.yml +81 -0
  9. legionforge_convobox-0.4.0/.gitignore +72 -0
  10. legionforge_convobox-0.4.0/.gitmessage.txt +14 -0
  11. legionforge_convobox-0.4.0/.tours/01-architecture-and-data-flow.tour +139 -0
  12. legionforge_convobox-0.4.0/.tours/02-review-findings-security-performance.tour +69 -0
  13. legionforge_convobox-0.4.0/.tours/03-extension-points-modularity.tour +75 -0
  14. legionforge_convobox-0.4.0/.vscode/extensions.json +5 -0
  15. legionforge_convobox-0.4.0/AGENTS.md +140 -0
  16. legionforge_convobox-0.4.0/CHANGELOG.md +761 -0
  17. legionforge_convobox-0.4.0/CLAUDE.md +17 -0
  18. legionforge_convobox-0.4.0/CONTRIBUTING.md +66 -0
  19. legionforge_convobox-0.4.0/CREDITS.md +96 -0
  20. legionforge_convobox-0.4.0/DEPENDENCY_LICENSE_AUDIT.md +171 -0
  21. legionforge_convobox-0.4.0/LICENSE +21 -0
  22. legionforge_convobox-0.4.0/OPENCODE_API_NOTES.md +347 -0
  23. legionforge_convobox-0.4.0/PKG-INFO +360 -0
  24. legionforge_convobox-0.4.0/README.md +304 -0
  25. legionforge_convobox-0.4.0/TESTING.md +538 -0
  26. legionforge_convobox-0.4.0/UAT-WORKSPACE-NOTES.md +24 -0
  27. legionforge_convobox-0.4.0/convobox.example.yaml +233 -0
  28. legionforge_convobox-0.4.0/docs/ADVANCED-SETTINGS-SCOPE.md +182 -0
  29. legionforge_convobox-0.4.0/docs/AI-ATTRIBUTION.md +133 -0
  30. legionforge_convobox-0.4.0/docs/ARCHITECTURE.md +176 -0
  31. legionforge_convobox-0.4.0/docs/ARTIFACT-PANE-SCOPE.md +607 -0
  32. legionforge_convobox-0.4.0/docs/CONVERSATION-DESIGN-REFERENCES.md +698 -0
  33. legionforge_convobox-0.4.0/docs/DESIGN-0.3.0-interaction-and-safety.md +394 -0
  34. legionforge_convobox-0.4.0/docs/DESIGN-backend-questions.md +85 -0
  35. legionforge_convobox-0.4.0/docs/DESIGN-backend-sandboxing.md +177 -0
  36. legionforge_convobox-0.4.0/docs/DESIGN-barge-in.md +347 -0
  37. legionforge_convobox-0.4.0/docs/DESIGN-echo-and-barge-in.md +360 -0
  38. legionforge_convobox-0.4.0/docs/DESIGN-guided-setup-for-non-technical-users.md +152 -0
  39. legionforge_convobox-0.4.0/docs/DESIGN-mic-mute-vs-pause-listening.md +166 -0
  40. legionforge_convobox-0.4.0/docs/DESIGN-opencode-artifact-pane-wiring.md +224 -0
  41. legionforge_convobox-0.4.0/docs/KNOWN-ISSUES.md +2224 -0
  42. legionforge_convobox-0.4.0/docs/LESSONS-FROM-VOICE-OPENCODE.md +42 -0
  43. legionforge_convobox-0.4.0/docs/PERMISSION-MODEL.md +157 -0
  44. legionforge_convobox-0.4.0/docs/PRIOR-ART.md +125 -0
  45. legionforge_convobox-0.4.0/docs/QUICKSTART.md +152 -0
  46. legionforge_convobox-0.4.0/docs/ROADMAP.md +469 -0
  47. legionforge_convobox-0.4.0/docs/SECURITY.md +281 -0
  48. legionforge_convobox-0.4.0/docs/SETTINGS-TUI-SCOPE.md +169 -0
  49. legionforge_convobox-0.4.0/docs/STATUS.md +702 -0
  50. legionforge_convobox-0.4.0/docs/TROUBLESHOOTING.md +143 -0
  51. legionforge_convobox-0.4.0/docs/UAT-L3-contrast.md +97 -0
  52. legionforge_convobox-0.4.0/docs/UAT-checklist.md +1448 -0
  53. legionforge_convobox-0.4.0/docs/UAT-claude-code-smoke.md +83 -0
  54. legionforge_convobox-0.4.0/docs/UAT-settings-tui.md +136 -0
  55. legionforge_convobox-0.4.0/docs/WEB-UI-ARCHITECTURE.md +364 -0
  56. legionforge_convobox-0.4.0/docs/WEB-UI-DEV.md +211 -0
  57. legionforge_convobox-0.4.0/docs/WEB-UI-USAGE.md +173 -0
  58. legionforge_convobox-0.4.0/docs/field-notes/2026-07-18-interactive-prompt-deadlock.md +87 -0
  59. legionforge_convobox-0.4.0/docs/field-notes/2026-07-18-safeword-vad-coupling.md +78 -0
  60. legionforge_convobox-0.4.0/docs/field-notes/2026-07-19-corrections-glossary-domain-improbable-sources.md +87 -0
  61. legionforge_convobox-0.4.0/docs/field-notes/2026-07-20-aec-delay-hint-was-a-red-herring.md +140 -0
  62. legionforge_convobox-0.4.0/docs/field-notes/2026-07-20-self-barge-in-was-backchannel-not-echo.md +116 -0
  63. legionforge_convobox-0.4.0/docs/field-notes/2026-07-21-codex-approval-decision-schema-bug.md +97 -0
  64. legionforge_convobox-0.4.0/docs/field-notes/2026-07-21-sherpa-onnx-dedicated-keyword-spotting-candidate.md +127 -0
  65. legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-cublas-dll-loading-needs-path-not-just-add-dll-directory.md +117 -0
  66. legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-mcp-tool-calls-have-a-separate-permission-gate.md +115 -0
  67. legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-native-allocator-leak-also-surfaces-as-numpy-memoryerror.md +124 -0
  68. legionforge_convobox-0.4.0/docs/field-notes/2026-07-22-shared-venv-editable-install-cross-contamination.md +110 -0
  69. legionforge_convobox-0.4.0/docs/field-notes/2026-07-23-approval-explain-clarify-help-outcome.md +104 -0
  70. legionforge_convobox-0.4.0/docs/field-notes/2026-07-25-player-is-playing-races-ahead-of-first-audio.md +129 -0
  71. legionforge_convobox-0.4.0/docs/field-notes/2026-07-25-timing-coincidence-is-not-echo-correlation.md +224 -0
  72. legionforge_convobox-0.4.0/docs/field-notes/2026-07-26-reference-capture-is-time-compressed-not-wall-clock.md +202 -0
  73. legionforge_convobox-0.4.0/docs/field-notes/2026-07-26-under-cancelled-echo-is-sometimes-transcribable.md +157 -0
  74. legionforge_convobox-0.4.0/docs/field-notes/2026-07-27-headphone-choice-does-not-eliminate-under-cancelled-echo.md +198 -0
  75. legionforge_convobox-0.4.0/docs/field-notes/2026-07-28-other-claude-code-web-uis-dont-transfer-much.md +142 -0
  76. legionforge_convobox-0.4.0/docs/field-notes/2026-07-29-openlive-comparison-and-acp-direction.md +139 -0
  77. legionforge_convobox-0.4.0/docs/field-notes/2026-07-31-acp-scoping-only-opencode-native.md +145 -0
  78. legionforge_convobox-0.4.0/docs/field-notes/2026-08-03-stt-config-swap-session-self-barge-in-drift.md +243 -0
  79. legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-stt-hotwords-athena-resume-inconclusive.md +202 -0
  80. legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-vad-segmenter-silent-unbounded-lockup.md +207 -0
  81. legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-web-resume-desyncs-tui-display.md +200 -0
  82. legionforge_convobox-0.4.0/docs/field-notes/2026-08-05-web-transcript-forwarding-parity.md +118 -0
  83. legionforge_convobox-0.4.0/docs/field-notes/2026-08-06-resume-word-hallucination-and-runaway-repetition.md +396 -0
  84. legionforge_convobox-0.4.0/docs/field-notes/2026-08-06-settings-tui-cannot-open-invalid-config.md +129 -0
  85. legionforge_convobox-0.4.0/docs/field-notes/2026-08-07-codex-artifact-pane-wiring.md +117 -0
  86. legionforge_convobox-0.4.0/docs/field-notes/2026-08-07-opencode-artifact-pane-file-edited-is-a-global-event.md +148 -0
  87. legionforge_convobox-0.4.0/docs/field-notes/2026-08-07-stt-engine-continued-investment-research.md +379 -0
  88. legionforge_convobox-0.4.0/docs/field-notes/2026-08-09-aec-spectral-suppression-vs-vad-rejection-diverge-on-laptop-internal-array.md +203 -0
  89. legionforge_convobox-0.4.0/docs/field-notes/2026-08-09-hard-stop-does-not-cancel-an-in-flight-tool-call.md +209 -0
  90. legionforge_convobox-0.4.0/docs/field-notes/2026-08-10-macos-kokoro-and-tts-stt-roundtrip.md +103 -0
  91. legionforge_convobox-0.4.0/docs/field-notes/2026-08-10-macos-live-hardware-first-pass.md +173 -0
  92. legionforge_convobox-0.4.0/docs/field-notes/2026-08-10-macos-live-mic-loop-and-safeword-first-confirmation.md +126 -0
  93. legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-artifact-editor-uri-stale-fetch-race-live-reproduced-and-fixed.md +147 -0
  94. legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-full-volume-sweep-raw-data-and-room-rt60.md +380 -0
  95. legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-macos-live-human-demo-safeword-bargein-and-self-echo-loop.md +155 -0
  96. legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-permission-model-validation-claude-codex-opencode.md +336 -0
  97. legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-self-barge-in-combined-mitigation-and-hardware-notes.md +150 -0
  98. legionforge_convobox-0.4.0/docs/field-notes/2026-08-11-self-barge-in-mitigation-at-demo-volume.md +126 -0
  99. legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-safeword-and-pause-phrase-are-mutually-exclusive-within-one-utterance.md +109 -0
  100. legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-stt-hallucination-bypasses-the-language-probability-gate-near-miss-on-approval-phrase.md +114 -0
  101. legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-vad-freeze-exhaustive-batch-after-fixing-windows-enhancements-confound.md +153 -0
  102. legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-vad-freeze-harness-catches-short-stalls-and-a-12-minute-unrecoverable-one.md +220 -0
  103. legionforge_convobox-0.4.0/docs/field-notes/2026-08-12-vad-freeze-live-reproduced-three-times-pr269-did-not-fix-it.md +165 -0
  104. legionforge_convobox-0.4.0/docs/field-notes/2026-08-14-force-kill-reliability-across-all-three-backends.md +177 -0
  105. legionforge_convobox-0.4.0/docs/field-notes/2026-08-14-mic-pipeline-silence-freeze-isolated-from-backend-via-typed-text-then-a-41-minute-compound-freeze.md +221 -0
  106. legionforge_convobox-0.4.0/docs/field-notes/2026-08-14-vad-freeze-harness-live-catches-two-more-readline-stalls-with-real-telemetry.md +166 -0
  107. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-claude-code-vad-freeze-re-confirmed-clean-at-good-volume.md +75 -0
  108. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-force-kill-does-not-reach-real-tool-call-children-on-macos.md +161 -0
  109. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-force-kill-macos-fix-attempts-killpg-and-processid-both-fail.md +189 -0
  110. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-force-kill-macos-pgrep-fallback-implemented-and-validated-15-of-15.md +200 -0
  111. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-kill-phrase-live-verified-during-a-genuine-freeze-resume-word-stt-unreliable.md +171 -0
  112. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-deterministic-audio-free-repro-orchestrator-only.md +167 -0
  113. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-fix-validated-retry-cancel-turns-indefinite-hang-into-3s-bound.md +143 -0
  114. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-genuine-event-loop-hang-not-a-volume-confound-live-repro.md +158 -0
  115. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-minimal-repro-attempt-inconclusive-not-a-single-hard-stop.md +111 -0
  116. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-repeated-cancel-mitigates-mechanism-and-workaround-candidate.md +152 -0
  117. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-reproduced-again-narrows-to-overlapping-hard-stops-not-a-single-one.md +133 -0
  118. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-freeze-root-cause-pinpointed-cancel-does-not-unstick-aiter-sse.md +162 -0
  119. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-opencode-retry-cancel-fix-holds-under-automated-hardstop-storm-on-windows.md +234 -0
  120. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-pause-phrase-fix-live-verified-hard-stop-and-pause-both-register.md +120 -0
  121. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-safety-phrase-reliability-battery-halt-and-bare-athena-unreliable.md +201 -0
  122. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-both-repro-conditions-clean-at-confirmed-good-volume.md +119 -0
  123. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-claude-code-backend-more-resilient-than-codex-on-macos.md +96 -0
  124. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-harness-confound-check-pure-text-still-stalls-severe-variant-audio-coupled.md +112 -0
  125. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-confirmed-no-active-stress-needed.md +97 -0
  126. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-does-not-reproduce-on-claude-code.md +91 -0
  127. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-does-not-reproduce-on-opencode.md +88 -0
  128. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-idle-trigger-re-run-with-busy-diagnostic-readline-was-harmless-mic-layer-freeze-real.md +127 -0
  129. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-mic-layer-repro-was-a-test-harness-confound-system-output-volume-at-25pct.md +137 -0
  130. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-new-variant-mic-layer-only-6-minutes-self-resolved.md +115 -0
  131. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-re-run-with-busy-diagnostic-zero-genuine-hangs-in-fresh-10-cycle-batch.md +105 -0
  132. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-readline-stalls-often-just-idle-time-not-a-hang-busy-state-fix.md +145 -0
  133. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-root-cause-codex-blocked-on-own-stdin-not-convobox-event-loop.md +151 -0
  134. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-freeze-second-severe-instance-plus-a-self-resolving-66s-stall.md +110 -0
  135. legionforge_convobox-0.4.0/docs/field-notes/2026-08-15-vad-mic-freeze-live-reproduced-on-macos.md +217 -0
  136. legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-artifact-active-race-fix-live-reverified-post-restart.md +115 -0
  137. legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-bare-athena-stt-unreliable-real-voice-windows.md +94 -0
  138. legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-brake-safeword-stt-hallucination-false-positive-hard-stop.md +170 -0
  139. legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-kill-phrase-live-voice-test-finds-two-real-gaps.md +148 -0
  140. legionforge_convobox-0.4.0/docs/field-notes/2026-08-18-macos-force-kill-pgrep-fallback-reconciled-and-shipped.md +107 -0
  141. legionforge_convobox-0.4.0/docs/field-notes/2026-08-19-kill-phrase-windows-orphaned-descendant-survives-force-kill.md +673 -0
  142. legionforge_convobox-0.4.0/docs/field-notes/2026-08-20-windows-amplitude-sweep-audio-enhancements-mixed-effect.md +223 -0
  143. legionforge_convobox-0.4.0/docs/field-notes/LICENSE +15 -0
  144. legionforge_convobox-0.4.0/docs/field-notes/README.md +50 -0
  145. legionforge_convobox-0.4.0/docs/field-notes/TEMPLATE.md +37 -0
  146. legionforge_convobox-0.4.0/docs/media/architecture.svg +116 -0
  147. legionforge_convobox-0.4.0/docs/media/backends.svg +56 -0
  148. legionforge_convobox-0.4.0/docs/media/use-cases.svg +78 -0
  149. legionforge_convobox-0.4.0/docs/media/webui-chat.png +0 -0
  150. legionforge_convobox-0.4.0/docs/media/webui-settings.png +0 -0
  151. legionforge_convobox-0.4.0/llms.txt +109 -0
  152. legionforge_convobox-0.4.0/pyproject.toml +262 -0
  153. legionforge_convobox-0.4.0/scripts/__init__.py +0 -0
  154. legionforge_convobox-0.4.0/scripts/_console.py +26 -0
  155. legionforge_convobox-0.4.0/scripts/acoustic_calibration.py +809 -0
  156. legionforge_convobox-0.4.0/scripts/analyze_incident.py +333 -0
  157. legionforge_convobox-0.4.0/scripts/audio_devices.py +843 -0
  158. legionforge_convobox-0.4.0/scripts/bootstrap_windows.ps1 +106 -0
  159. legionforge_convobox-0.4.0/scripts/check_venv_identity.py +58 -0
  160. legionforge_convobox-0.4.0/scripts/ci_local.py +39 -0
  161. legionforge_convobox-0.4.0/scripts/playback_smoketest.py +69 -0
  162. legionforge_convobox-0.4.0/scripts/roundtrip_smoketest.py +133 -0
  163. legionforge_convobox-0.4.0/scripts/run_convobox.py +3298 -0
  164. legionforge_convobox-0.4.0/scripts/settings_tui.py +2752 -0
  165. legionforge_convobox-0.4.0/scripts/spike.py +95 -0
  166. legionforge_convobox-0.4.0/scripts/spike_smoketest.py +126 -0
  167. legionforge_convobox-0.4.0/scripts/uat-l3-contrast.ps1 +86 -0
  168. legionforge_convobox-0.4.0/scripts/voice_picker.py +467 -0
  169. legionforge_convobox-0.4.0/scripts/voice_picker_tui.py +470 -0
  170. legionforge_convobox-0.4.0/scripts/voice_tui.py +425 -0
  171. legionforge_convobox-0.4.0/src/convobox/__init__.py +0 -0
  172. legionforge_convobox-0.4.0/src/convobox/adapters/__init__.py +61 -0
  173. legionforge_convobox-0.4.0/src/convobox/adapters/base.py +382 -0
  174. legionforge_convobox-0.4.0/src/convobox/adapters/claude_code.py +1034 -0
  175. legionforge_convobox-0.4.0/src/convobox/adapters/codex.py +940 -0
  176. legionforge_convobox-0.4.0/src/convobox/adapters/opencode.py +387 -0
  177. legionforge_convobox-0.4.0/src/convobox/approval/__init__.py +7 -0
  178. legionforge_convobox-0.4.0/src/convobox/approval/detector.py +166 -0
  179. legionforge_convobox-0.4.0/src/convobox/approval/hook_script.py +159 -0
  180. legionforge_convobox-0.4.0/src/convobox/audio/__init__.py +4 -0
  181. legionforge_convobox-0.4.0/src/convobox/audio/_sounddevice.py +21 -0
  182. legionforge_convobox-0.4.0/src/convobox/audio/ack_tones.py +61 -0
  183. legionforge_convobox-0.4.0/src/convobox/audio/aec.py +300 -0
  184. legionforge_convobox-0.4.0/src/convobox/audio/capture.py +229 -0
  185. legionforge_convobox-0.4.0/src/convobox/audio/correlation.py +85 -0
  186. legionforge_convobox-0.4.0/src/convobox/audio/incident_capture.py +151 -0
  187. legionforge_convobox-0.4.0/src/convobox/audio/playback.py +298 -0
  188. legionforge_convobox-0.4.0/src/convobox/config.py +901 -0
  189. legionforge_convobox-0.4.0/src/convobox/confirmword/__init__.py +3 -0
  190. legionforge_convobox-0.4.0/src/convobox/confirmword/detector.py +137 -0
  191. legionforge_convobox-0.4.0/src/convobox/interrupt_presets.py +104 -0
  192. legionforge_convobox-0.4.0/src/convobox/listening_pause/__init__.py +6 -0
  193. legionforge_convobox-0.4.0/src/convobox/listening_pause/detector.py +65 -0
  194. legionforge_convobox-0.4.0/src/convobox/orchestrator/__init__.py +5 -0
  195. legionforge_convobox-0.4.0/src/convobox/orchestrator/orchestrator.py +724 -0
  196. legionforge_convobox-0.4.0/src/convobox/response_tiering/__init__.py +14 -0
  197. legionforge_convobox-0.4.0/src/convobox/response_tiering/detector.py +123 -0
  198. legionforge_convobox-0.4.0/src/convobox/response_tiering/tiering.py +63 -0
  199. legionforge_convobox-0.4.0/src/convobox/resumeword/__init__.py +7 -0
  200. legionforge_convobox-0.4.0/src/convobox/resumeword/detector.py +145 -0
  201. legionforge_convobox-0.4.0/src/convobox/safeword/__init__.py +3 -0
  202. legionforge_convobox-0.4.0/src/convobox/safeword/detector.py +45 -0
  203. legionforge_convobox-0.4.0/src/convobox/stt/__init__.py +14 -0
  204. legionforge_convobox-0.4.0/src/convobox/stt/base.py +62 -0
  205. legionforge_convobox-0.4.0/src/convobox/stt/corrections.py +77 -0
  206. legionforge_convobox-0.4.0/src/convobox/stt/factory.py +26 -0
  207. legionforge_convobox-0.4.0/src/convobox/stt/language_tracker.py +44 -0
  208. legionforge_convobox-0.4.0/src/convobox/stt/transcriber.py +519 -0
  209. legionforge_convobox-0.4.0/src/convobox/tts/__init__.py +14 -0
  210. legionforge_convobox-0.4.0/src/convobox/tts/base.py +54 -0
  211. legionforge_convobox-0.4.0/src/convobox/tts/factory.py +216 -0
  212. legionforge_convobox-0.4.0/src/convobox/tts/kokoro.py +106 -0
  213. legionforge_convobox-0.4.0/src/convobox/tts/piper.py +193 -0
  214. legionforge_convobox-0.4.0/src/convobox/tui/__init__.py +4 -0
  215. legionforge_convobox-0.4.0/src/convobox/tui/render.py +352 -0
  216. legionforge_convobox-0.4.0/src/convobox/tui/state.py +154 -0
  217. legionforge_convobox-0.4.0/src/convobox/vad/__init__.py +3 -0
  218. legionforge_convobox-0.4.0/src/convobox/vad/segmenter.py +418 -0
  219. legionforge_convobox-0.4.0/src/convobox/web/__init__.py +3 -0
  220. legionforge_convobox-0.4.0/src/convobox/web/app.py +415 -0
  221. legionforge_convobox-0.4.0/src/convobox/web/artifacts.py +270 -0
  222. legionforge_convobox-0.4.0/src/convobox/web/bridge.py +607 -0
  223. legionforge_convobox-0.4.0/src/convobox/web/history.py +204 -0
  224. legionforge_convobox-0.4.0/src/convobox/web/mcp_server.py +198 -0
  225. legionforge_convobox-0.4.0/src/convobox/web/settings_api.py +211 -0
  226. legionforge_convobox-0.4.0/src/convobox/web/static/img/legionforge-logo.svg +74 -0
  227. legionforge_convobox-0.4.0/src/convobox/web/static/index.html +2381 -0
  228. legionforge_convobox-0.4.0/src/convobox/web/static/manifest.json +18 -0
  229. legionforge_convobox-0.4.0/src/convobox/web/static/sw.js +56 -0
  230. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/LICENSE +29 -0
  231. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/NOTICE.md +22 -0
  232. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/highlight.min.js +1244 -0
  233. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/bash.min.js +21 -0
  234. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/c.min.js +41 -0
  235. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/cpp.min.js +47 -0
  236. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/csharp.min.js +49 -0
  237. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/css.min.js +31 -0
  238. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/dart.min.js +25 -0
  239. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/go.min.js +20 -0
  240. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/graphql.min.js +12 -0
  241. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/java.min.js +38 -0
  242. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/javascript.min.js +81 -0
  243. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/json.min.js +8 -0
  244. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/kotlin.min.js +46 -0
  245. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/lua.min.js +15 -0
  246. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/markdown.min.js +32 -0
  247. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/php.min.js +59 -0
  248. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/powershell.min.js +39 -0
  249. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/python.min.js +42 -0
  250. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/ruby.min.js +54 -0
  251. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/rust.min.js +27 -0
  252. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/scala.min.js +28 -0
  253. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/sql.min.js +21 -0
  254. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/swift.min.js +68 -0
  255. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/typescript.min.js +99 -0
  256. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/xml.min.js +29 -0
  257. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/languages/yaml.min.js +28 -0
  258. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/styles/github-dark.min.css +10 -0
  259. legionforge_convobox-0.4.0/src/convobox/web/static/vendor/highlightjs/styles/github.min.css +10 -0
  260. legionforge_convobox-0.4.0/src/convobox/web/stream.py +136 -0
  261. legionforge_convobox-0.4.0/src/convobox/web/uploads.py +109 -0
  262. legionforge_convobox-0.4.0/tests/__init__.py +0 -0
  263. legionforge_convobox-0.4.0/tests/fake_claude_cli.py +136 -0
  264. legionforge_convobox-0.4.0/tests/fake_codex_appserver.py +316 -0
  265. legionforge_convobox-0.4.0/tests/test_ack_tones.py +78 -0
  266. legionforge_convobox-0.4.0/tests/test_adapters_base.py +109 -0
  267. legionforge_convobox-0.4.0/tests/test_aec.py +335 -0
  268. legionforge_convobox-0.4.0/tests/test_approval.py +147 -0
  269. legionforge_convobox-0.4.0/tests/test_approval_explanation_rendering.py +100 -0
  270. legionforge_convobox-0.4.0/tests/test_approval_prompt_gate.py +324 -0
  271. legionforge_convobox-0.4.0/tests/test_audio.py +883 -0
  272. legionforge_convobox-0.4.0/tests/test_audio_correlation.py +117 -0
  273. legionforge_convobox-0.4.0/tests/test_audio_devices.py +356 -0
  274. legionforge_convobox-0.4.0/tests/test_backend_working_dir.py +61 -0
  275. legionforge_convobox-0.4.0/tests/test_barge_in.py +446 -0
  276. legionforge_convobox-0.4.0/tests/test_claude_approval_hook_script.py +256 -0
  277. legionforge_convobox-0.4.0/tests/test_claude_code_adapter.py +666 -0
  278. legionforge_convobox-0.4.0/tests/test_claude_code_approval.py +646 -0
  279. legionforge_convobox-0.4.0/tests/test_codex_adapter.py +1063 -0
  280. legionforge_convobox-0.4.0/tests/test_config.py +404 -0
  281. legionforge_convobox-0.4.0/tests/test_confirmword.py +138 -0
  282. legionforge_convobox-0.4.0/tests/test_continue_prompt_gate.py +82 -0
  283. legionforge_convobox-0.4.0/tests/test_conversation_tui.py +489 -0
  284. legionforge_convobox-0.4.0/tests/test_conversation_tui_keys.py +158 -0
  285. legionforge_convobox-0.4.0/tests/test_example_config.py +48 -0
  286. legionforge_convobox-0.4.0/tests/test_factories.py +35 -0
  287. legionforge_convobox-0.4.0/tests/test_incident_capture.py +181 -0
  288. legionforge_convobox-0.4.0/tests/test_interrupt_presets.py +77 -0
  289. legionforge_convobox-0.4.0/tests/test_language_tracker.py +62 -0
  290. legionforge_convobox-0.4.0/tests/test_listening_pause.py +72 -0
  291. legionforge_convobox-0.4.0/tests/test_opencode_adapter.py +815 -0
  292. legionforge_convobox-0.4.0/tests/test_orchestrator.py +1569 -0
  293. legionforge_convobox-0.4.0/tests/test_permission_mode.py +214 -0
  294. legionforge_convobox-0.4.0/tests/test_recognition_error_ladder.py +58 -0
  295. legionforge_convobox-0.4.0/tests/test_response_tiering.py +100 -0
  296. legionforge_convobox-0.4.0/tests/test_response_tiering_state.py +90 -0
  297. legionforge_convobox-0.4.0/tests/test_resumeword.py +88 -0
  298. legionforge_convobox-0.4.0/tests/test_roundtrip_smoketest_cli.py +32 -0
  299. legionforge_convobox-0.4.0/tests/test_run_convobox_echo.py +601 -0
  300. legionforge_convobox-0.4.0/tests/test_run_convobox_quit.py +192 -0
  301. legionforge_convobox-0.4.0/tests/test_safeword.py +49 -0
  302. legionforge_convobox-0.4.0/tests/test_settings_tui.py +2569 -0
  303. legionforge_convobox-0.4.0/tests/test_spike_cli.py +27 -0
  304. legionforge_convobox-0.4.0/tests/test_startup_announcement.py +23 -0
  305. legionforge_convobox-0.4.0/tests/test_stt_factory.py +65 -0
  306. legionforge_convobox-0.4.0/tests/test_transcribe_with_timeout.py +120 -0
  307. legionforge_convobox-0.4.0/tests/test_transcriber.py +848 -0
  308. legionforge_convobox-0.4.0/tests/test_transcript_corrections.py +59 -0
  309. legionforge_convobox-0.4.0/tests/test_tts.py +341 -0
  310. legionforge_convobox-0.4.0/tests/test_tts_factory.py +450 -0
  311. legionforge_convobox-0.4.0/tests/test_tts_kokoro.py +227 -0
  312. legionforge_convobox-0.4.0/tests/test_vad_segmenter.py +777 -0
  313. legionforge_convobox-0.4.0/tests/test_voice_picker_cli.py +201 -0
  314. legionforge_convobox-0.4.0/tests/test_voice_picker_tui.py +153 -0
  315. legionforge_convobox-0.4.0/tests/test_web_app.py +916 -0
  316. legionforge_convobox-0.4.0/tests/test_web_artifacts.py +491 -0
  317. legionforge_convobox-0.4.0/tests/test_web_bridge.py +1115 -0
  318. legionforge_convobox-0.4.0/tests/test_web_history.py +292 -0
  319. legionforge_convobox-0.4.0/tests/test_web_mcp_server.py +121 -0
  320. legionforge_convobox-0.4.0/tests/test_web_settings_api.py +324 -0
  321. legionforge_convobox-0.4.0/tests/test_web_uploads.py +129 -0
  322. legionforge_convobox-0.4.0/tests/test_working_watchdog.py +499 -0
  323. legionforge_convobox-0.4.0/uv.lock +2695 -0
@@ -0,0 +1,15 @@
1
+ * text=auto eol=lf
2
+
3
+ *.py text eol=lf
4
+ *.md text eol=lf
5
+ *.toml text eol=lf
6
+ *.yaml text eol=lf
7
+ *.yml text eol=lf
8
+ *.tour text eol=lf
9
+ *.json text eol=lf
10
+ *.lock text eol=lf
11
+ *.ps1 text eol=crlf
12
+ *.sh text eol=lf
13
+
14
+ *.wav binary
15
+ *.onnx binary
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+ # Pre-push privacy scrub -- scans every commit about to be uploaded, not
3
+ # just the working tree (a clean tree is not a clean push: git push
4
+ # uploads all ancestor commits). Reference: LegionForge/jeli's hook and
5
+ # the 2026-07-02 stale-branch leak it was written after.
6
+ # Enable once per clone: git config core.hooksPath .githooks
7
+ PATTERN='192\.168\.|10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|ssh[^ ]*@[a-zA-Z0-9]|@[a-zA-Z0-9-]+\.local|id_ed25519|id_rsa|BEGIN [A-Z ]*PRIVATE KEY'
8
+ fail=0
9
+ while read -r _local_ref local_sha _remote_ref remote_sha; do
10
+ [ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
11
+ if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
12
+ range=$(git rev-list "$local_sha" --not --remotes=origin 2>/dev/null)
13
+ else
14
+ range=$(git rev-list "$remote_sha..$local_sha" 2>/dev/null)
15
+ fi
16
+ [ -z "$range" ] && continue
17
+ # uv.lock excluded: package version strings match the IP regex
18
+ # (nvidia_curand 10.4.0.35 -- verified false positive 2026-07-19).
19
+ # This hook excludes itself: it necessarily contains the patterns it
20
+ # scans for (it blocked its own first push -- working as designed).
21
+ hits=$(git grep -l -E "$PATTERN" $range -- ':!uv.lock' ':!.githooks/pre-push' 2>/dev/null)
22
+ if [ -n "$hits" ]; then
23
+ echo "pre-push BLOCKED: possible private data in outgoing commits:" >&2
24
+ echo "$hits" >&2
25
+ echo "Inspect with: git grep -n -E '<pattern>' <sha> -- <file>" >&2
26
+ fail=1
27
+ fi
28
+ done
29
+ exit $fail
@@ -0,0 +1,6 @@
1
+ # Every change requires JP's review -- the mechanical enforcement of
2
+ # "agents propose, JP approves" (AGENTS.md rule 10 era). GitHub
3
+ # auto-requests review from owners on every PR touching matched paths.
4
+ * @jp-cruz
5
+ # Workflow changes doubly so: they run with repo permissions.
6
+ .github/workflows/ @jp-cruz
@@ -0,0 +1,15 @@
1
+ version: 2
2
+ updates:
3
+ # The SHA-pinned dev-rig reusables and actions never update themselves
4
+ # -- that is the trade-off of pinning; this is the standard answer.
5
+ - package-ecosystem: github-actions
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ groups:
10
+ actions:
11
+ patterns: ["*"]
12
+ - package-ecosystem: pip
13
+ directory: /
14
+ schedule:
15
+ interval: weekly
@@ -0,0 +1,35 @@
1
+ ## Summary
2
+
3
+ Describe the change briefly.
4
+
5
+ ## Attribution
6
+
7
+ - [ ] Human-only change
8
+ - [ ] AI-assisted change
9
+
10
+ If you check `AI-assisted change`, the CI check will require the attribution
11
+ block below or an `AI-Attribution:` commit trailer.
12
+
13
+ If AI-assisted, fill in:
14
+
15
+ - Product:
16
+ - Provider:
17
+ - Model:
18
+ - Scope:
19
+ - Date:
20
+
21
+ Example:
22
+
23
+ ```text
24
+ Attribution: OpenAI Codex
25
+ Provider: OpenAI
26
+ Model: GPT-5
27
+ Scope: src/convobox/adapters/codex.py
28
+ Date: 2026-07-15
29
+ ```
30
+
31
+ ## Validation
32
+
33
+ - [ ] Tests run
34
+ - [ ] Docs updated
35
+ - [ ] Changelog updated, if user-visible
@@ -0,0 +1,89 @@
1
+ name: AI Attribution Check
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, edited, synchronize, reopened, ready_for_review]
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: read
10
+
11
+ jobs:
12
+ check:
13
+ name: Require attribution when AI-assisted
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Validate PR attribution
17
+ uses: actions/github-script@v9
18
+ with:
19
+ script: |
20
+ const pull = context.payload.pull_request;
21
+ const body = pull.body || "";
22
+
23
+ const aiAssistedMarked = /-\s*\[[xX]\]\s*AI-assisted change/.test(body);
24
+ if (!aiAssistedMarked) {
25
+ core.info("PR is not marked as AI-assisted; skipping attribution enforcement.");
26
+ return;
27
+ }
28
+
29
+ // Check the PR body FIRST -- this needs no API call, so a
30
+ // correctly-attributed PR can never be blocked by a GitHub API
31
+ // outage (which is exactly what once spuriously failed a
32
+ // correct PR: the old code called listCommits unconditionally
33
+ // at the top and threw on a transient 503).
34
+ const bodyHasAttribution =
35
+ /Attribution:\s*/i.test(body) &&
36
+ /Provider:\s*/i.test(body) &&
37
+ /Model:\s*/i.test(body) &&
38
+ /Scope:\s*/i.test(body) &&
39
+ /Date:\s*/i.test(body);
40
+ if (bodyHasAttribution) {
41
+ core.info("AI attribution present in PR body.");
42
+ return;
43
+ }
44
+
45
+ // The body lacks the block -> fall back to commit trailers. This
46
+ // is the ONLY path that needs the API. A transient API error here
47
+ // must NOT hard-fail the PR on infrastructure grounds: attribution
48
+ // is a governance requirement, and blocking correct work during a
49
+ // GitHub outage is worse than the rare case of one un-attributed
50
+ // PR slipping through while the API is down (easily caught later).
51
+ let commitMessages = "";
52
+ try {
53
+ const commits = await github.paginate(
54
+ github.rest.pulls.listCommits,
55
+ {
56
+ owner: context.repo.owner,
57
+ repo: context.repo.repo,
58
+ pull_number: pull.number,
59
+ per_page: 100,
60
+ },
61
+ );
62
+ commitMessages = commits.map((commit) => commit.commit.message).join("\n\n");
63
+ } catch (error) {
64
+ core.warning(
65
+ "Could not fetch commits to check for an AI-Attribution trailer (" +
66
+ (error && error.message ? error.message : String(error)) +
67
+ "). Skipping the commit-trailer check rather than failing the PR on " +
68
+ "an infrastructure error -- ensure attribution is present.",
69
+ );
70
+ return;
71
+ }
72
+
73
+ if (/AI-Attribution:/i.test(commitMessages)) {
74
+ core.info("AI attribution present in a commit trailer.");
75
+ return;
76
+ }
77
+
78
+ core.setFailed(
79
+ [
80
+ "This PR is marked AI-assisted but is missing the required attribution.",
81
+ "",
82
+ "Add either:",
83
+ "- the standard attribution block to the PR body, or",
84
+ "- an `AI-Attribution:` commit trailer on one or more commits.",
85
+ "",
86
+ "Required fields for the PR body block:",
87
+ "Attribution, Provider, Model, Scope, Date.",
88
+ ].join("\n"),
89
+ );
@@ -0,0 +1,81 @@
1
+ # LegionForge dev-rig CI: test/lint/SAST/dependency-audit/SBOM/secrets.
2
+ #
3
+ # Consumes LegionForge/dev-rig's shared reusable workflows (shared Python CI
4
+ # + security rig used across LegionForge projects) rather than duplicating
5
+ # tool config here. See TESTING.md -> "CI / LegionForge dev-rig" for what
6
+ # each job checks, why it's scoped the way it is, and how to run the same
7
+ # checks locally before pushing.
8
+ #
9
+ # Pinned to a commit SHA, not a tag: LegionForge/dev-rig has cut no tagged
10
+ # release as of this writing (confirmed via `gh api repos/LegionForge/dev-rig/tags`
11
+ # returning empty) -- dev-rig's own README examples currently show `@main`,
12
+ # a mutable ref this project deliberately does not consume (see
13
+ # LegionForge/dev-rig's pipeline-hardening handoff doc: never consume
14
+ # `@main`, use a pinned tag once one exists). SHA-pin is the correct
15
+ # available-today choice; re-pin to a released tag once dev-rig cuts one.
16
+ #
17
+ # DEV_RIG_REF: 468d5a7c30f6540a472a32d8b3bea1ac381f2017 (main, 2026-07-10)
18
+
19
+ name: CI
20
+
21
+ on:
22
+ pull_request:
23
+ push:
24
+ branches: [main]
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ test:
31
+ name: Tests & Coverage
32
+ uses: LegionForge/dev-rig/.github/workflows/test.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
33
+ with:
34
+ coverage-source: "convobox"
35
+ python-version: "3.12"
36
+
37
+ lint:
38
+ name: Lint & Type Check
39
+ uses: LegionForge/dev-rig/.github/workflows/lint.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
40
+ with:
41
+ source-dirs: "src/convobox"
42
+ python-version: "3.12"
43
+ # types-PyYAML is the default already; explicit for clarity. mypy's
44
+ # remaining untyped third-party deps (faster-whisper, sounddevice,
45
+ # silero-vad -- no stub packages exist for these) are scoped out via
46
+ # [[tool.mypy.overrides]] in pyproject.toml, not a blanket CLI flag,
47
+ # so this stays real type-checking rather than a broad opt-out.
48
+ extra-mypy-deps: "types-PyYAML"
49
+
50
+ sast:
51
+ name: SAST
52
+ uses: LegionForge/dev-rig/.github/workflows/sast.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
53
+ permissions:
54
+ contents: read
55
+ security-events: write # required for the CodeQL upload-sarif step inside sast.yml
56
+ with:
57
+ source-dirs: "src/convobox"
58
+ # Restored p/fastapi 2026-08-17: ConvoBox now has a real FastAPI web
59
+ # UI (src/convobox/web/) plus a mounted MCP HTTP server with a CSRF
60
+ # exemption for its own routes -- exactly the trust-boundary code
61
+ # p/fastapi's ruleset targets. dev-rig's default is "p/python
62
+ # p/fastapi"; kept p/security-audit alongside it rather than
63
+ # dropping back to the narrower default.
64
+ semgrep-configs: "p/python p/fastapi p/security-audit"
65
+ python-version: "3.12"
66
+
67
+ audit:
68
+ name: Dependency Audit
69
+ uses: LegionForge/dev-rig/.github/workflows/audit.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
70
+ with:
71
+ python-version: "3.12"
72
+
73
+ sbom:
74
+ name: SBOM
75
+ uses: LegionForge/dev-rig/.github/workflows/sbom.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
76
+ with:
77
+ python-version: "3.12"
78
+
79
+ secrets:
80
+ name: Secret Scanning
81
+ uses: LegionForge/dev-rig/.github/workflows/secrets.yml@468d5a7c30f6540a472a32d8b3bea1ac381f2017
@@ -0,0 +1,81 @@
1
+ name: Publish to PyPI
2
+
3
+ # Triggered by pushing a version tag (e.g. git tag v0.4.0 && git push --tags).
4
+ # Uses PyPI Trusted Publishing (OIDC) — no API token stored in secrets.
5
+ # Configure the trusted publisher at https://pypi.org/manage/project/legionforge-convobox/settings/
6
+ # before pushing the first tag:
7
+ # Publisher: GitHub Actions
8
+ # Repository: LegionForge/convobox
9
+ # Workflow: publish.yml
10
+ # Environment: pypi
11
+ #
12
+ # PyPI distribution name is legionforge-convobox (org convention: PyPI is a
13
+ # flat global namespace, so every LegionForge package is prefixed to avoid
14
+ # squatting/ambiguity -- see LegionForge/convobox's own DEPENDENCY_LICENSE_AUDIT.md
15
+ # and pyproject.toml `[project] name`). The repo name, import path, and CLI
16
+ # commands (convobox/convobox-settings/convobox-spike) stay unprefixed.
17
+ #
18
+ # Template: LegionForge/llm-valet/.github/workflows/publish.yml, adapted to
19
+ # this repo's actual Python floor (3.12, matching pyproject.toml's
20
+ # requires-python and ci.yml's python-version -- not copied blindly from
21
+ # llm-valet's 3.11).
22
+ #
23
+ # Every action below is pinned to a commit SHA, not a floating tag --
24
+ # same discipline ci.yml already applies to dev-rig (see that file's own
25
+ # comment on why a moved tag is a real supply-chain vector), just not
26
+ # consistently carried over here when this file was first written.
27
+ # pypa/gh-action-pypi-publish's `release/v1` ref is a BRANCH, not a tag --
28
+ # more mutable than a normal version tag, not less, so pinning matters
29
+ # even more for it. Resolved 2026-08-06:
30
+ # actions/checkout@v7 -> 3d3c42e5aac5ba805825da76410c181273ba90b1
31
+ # actions/setup-python@v6 -> ece7cb06caefa5fff74198d8649806c4678c61a1
32
+ # actions/upload-artifact@v7 -> 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
33
+ # actions/download-artifact@v8 -> 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
34
+ # pypa/gh-action-pypi-publish@release/v1 -> dc37677b2e1c63e2034f94d8a5b11f265b73ba33
35
+
36
+ on:
37
+ push:
38
+ tags:
39
+ - "v[0-9]+.[0-9]+.[0-9]+"
40
+
41
+ jobs:
42
+ build:
43
+ name: Build distribution
44
+ runs-on: ubuntu-latest
45
+
46
+ steps:
47
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
48
+
49
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
50
+ with:
51
+ python-version: "3.12"
52
+
53
+ - name: Install build tools
54
+ run: pip install hatchling build
55
+
56
+ - name: Build wheel and sdist
57
+ run: python -m build
58
+
59
+ - name: Upload dist artifacts
60
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
61
+ with:
62
+ name: dist
63
+ path: dist/
64
+
65
+ publish:
66
+ name: Publish to PyPI
67
+ needs: build
68
+ runs-on: ubuntu-latest
69
+ environment: pypi
70
+ permissions:
71
+ id-token: write # required for OIDC trusted publishing
72
+
73
+ steps:
74
+ - name: Download dist artifacts
75
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
76
+ with:
77
+ name: dist
78
+ path: dist/
79
+
80
+ - name: Publish to PyPI
81
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
@@ -0,0 +1,72 @@
1
+ .venv/
2
+ .models/
3
+ # scripts/run_convobox.py --aec-dump output (reference/mic WAV captures
4
+ # for offline AEC analysis) -- real audio, never committed. *.wav below
5
+ # already covers the files; this also catches the directory itself so an
6
+ # empty/future-dated run doesn't show up as an untracked-but-visible dir.
7
+ .aec-dumps/
8
+ .incident-captures/
9
+ __pycache__/
10
+ *.pyc
11
+ .pytest_cache/
12
+ .coverage
13
+ coverage.xml
14
+ *.egg-info/
15
+ dist/
16
+ build/
17
+ .env
18
+ *.wav
19
+ *.log
20
+ # Manually rotated/renamed logs kept around for a diff (e.g.
21
+ # convobox-tui.log.old.20260726) -- *.log above only matches the
22
+ # unrotated name.
23
+ *.log.old*
24
+ uat-acoustic-calibration/
25
+ .DS_Store
26
+
27
+ # Per-machine local config and its variants (copy convobox.example.yaml to
28
+ # create one) -- these carry real device names and other per-machine
29
+ # specifics, same rationale as convobox.yaml itself. convobox.example.yaml
30
+ # is the one deliberately-tracked exception.
31
+ convobox*.yaml
32
+ !convobox.example.yaml
33
+ # Config backups now live in a subdirectory (settings_tui.py's
34
+ # backup_config/list_config_backups, GitHub issue #235 finding D4) rather
35
+ # than scattered as convobox.yaml.backup-* directly in the repo root --
36
+ # the old flat pattern is kept too so a pre-existing checkout with old-style
37
+ # backups still has them ignored until cleaned up.
38
+ .convobox-backups/
39
+ convobox.yaml.backup-*
40
+
41
+ # run_convobox.py's AEC delay auto-tune diagnostic sidecar (config.py's
42
+ # write_aec_estimate/read_aec_estimate) -- runtime-measured, not config.
43
+ *.aec-estimate.json
44
+
45
+ # Personal working notes kept out of the public repo -- philosophy.md is
46
+ # JP's running voice-dialogue log (archived separately in the vault);
47
+ # _diag.py/_tone.py were throwaway audio probes superseded by
48
+ # scripts/audio_devices.py. Structural protection against an accidental
49
+ # `git add -A` sweeping them in.
50
+ philosophy.md
51
+ scripts/_diag.py
52
+ scripts/_tone.py
53
+
54
+ # Leading-underscore convention for repo-root scratch/throwaway files
55
+ # (message-log dumps, one-off test artifacts) -- generalizes the same
56
+ # pattern _diag.py/_tone.py already used above.
57
+ /_*
58
+
59
+ # Web UI and persistent history -- contains user transcripts, approvals, commands.
60
+ # KEEP THIS unless you explicitly want to track history in a PRIVATE repo.
61
+ # See docs/SECURITY.md for opt-in flow.
62
+ .convobox-history/
63
+ .convobox.db
64
+
65
+ # Web server logs (may contain event data, tool inputs, responses)
66
+ .convobox-web.log
67
+ convobox-web-*.log
68
+ .convobox-cache/
69
+
70
+ # Private/scratch directories
71
+ .scratch/
72
+ .private/
@@ -0,0 +1,14 @@
1
+ # AI-assisted commit template
2
+ #
3
+ # Use this template for commits that include AI-assisted work.
4
+ # Keep the attribution block intact and replace the placeholders.
5
+
6
+ Summary:
7
+
8
+ Attribution: <product>
9
+ Provider: <provider>
10
+ Model: <model>
11
+ Scope: <files, feature, or commit>
12
+ Date: <YYYY-MM-DD>
13
+
14
+ AI-Attribution: <product>; provider=<provider>; model=<model>; scope=<files, feature, or commit>
@@ -0,0 +1,139 @@
1
+ {
2
+ "$schema": "https://aka.ms/codetour-schema",
3
+ "title": "1. Architecture & Data Flow",
4
+ "description": "Full pipeline walkthrough, in call order: mic -> VAD -> STT -> safeword -> orchestrator -> backend adapter -> TTS -> playback. Each step calls out the data handoff (shape/dtype crossing the boundary) and any performance-relevant design decision at that point.",
5
+ "steps": [
6
+ {
7
+ "title": "Start here",
8
+ "description": "**ConvoBox pipeline, top to bottom.**\n\n```\n mic -> VAD -> local STT -> safeword check -> orchestrator -> backend adapter\n | |\n (deterministic, OpenCode /\n no LLM) Claude Code / ...\n |\n local streaming TTS\n```\n\nThis tour follows one utterance through every stage in the order it actually happens at runtime, not file/directory order. Watch the **data handoff** called out at each step — the shape/dtype changes as audio becomes numbers becomes text becomes routing decisions becomes speech again.\n\nSee `README.md` → Architecture for the original design diagram this pipeline implements.\n\nEach step below is anchored by both a line number and a text pattern — if the code has moved since this tour was written, CodeTour re-locates the step by searching for the pattern rather than trusting a stale line number."
9
+ },
10
+ {
11
+ "file": "scripts/spike.py",
12
+ "line": 38,
13
+ "pattern": "with MicrophoneStream\\(sample_rate=config\\.audio\\.sample_rate",
14
+ "description": "**The whole chain in one line.** This is the actual measurement spike — mic → VAD → STT → logged transcript — with no backend/TTS wiring, useful as an orientation map before diving into each stage:\n\n```python\nwith MicrophoneStream(...) as mic:\n async for utterance in segmenter.segment(mic.stream()):\n result = transcriber.transcribe(utterance)\n```\n\n`mic.stream()` is an async generator of raw chunks → `segmenter.segment()` re-yields only complete utterances → `transcriber.transcribe()` turns one utterance into text. Every step below zooms into one of these three calls."
15
+ },
16
+ {
17
+ "file": "src/convobox/audio/capture.py",
18
+ "line": 21,
19
+ "pattern": "class MicrophoneStream:",
20
+ "description": "**Stage 1: audio capture.** `MicrophoneStream` wraps a single `sounddevice.InputStream` — continuous streaming, not record-then-stop, because segmentation into utterances is VAD's job downstream, not a fixed capture window.\n\nDeliberately **not** shelling out to platform tools (sox/arecord/powershell) — see docs/LESSONS-FROM-VOICE-OPENCODE.md for why that was a real command-injection bug in an earlier related project."
21
+ },
22
+ {
23
+ "file": "src/convobox/audio/capture.py",
24
+ "line": 42,
25
+ "pattern": "def _callback\\(",
26
+ "description": "**The real-time boundary.** `_callback` runs on PortAudio's own audio thread, not the asyncio event loop — it must be fast and non-blocking. It just copies the incoming buffer and pushes it onto a plain `queue.Queue`.\n\n**Data handoff:** raw device audio → `float32` numpy array, shape `(blocksize,)`, values in `[-1, 1]`. float32 (not int16) because both Silero VAD and faster-whisper consume float32 directly — int16 would force a conversion on every single chunk (see the module-level comment at the top of this file)."
27
+ },
28
+ {
29
+ "file": "src/convobox/audio/capture.py",
30
+ "line": 70,
31
+ "pattern": "async def stream\\(self\\) -> AsyncIterator",
32
+ "description": "**Bridging the sync callback thread to async consumers.** `stream()` is an async generator that pulls off the queue via `asyncio.to_thread(self._queue.get)` — blocking the queue-get in a worker thread so it never stalls the event loop.\n\nThe `_CLOSE_SENTINEL` pattern (defined near the top of this file) is what lets `close()` wake up a consumer parked in a blocking `get()` — this exact bridge pattern gets reused later for TTS streaming too (see step on `piper.py`)."
33
+ },
34
+ {
35
+ "file": "src/convobox/vad/segmenter.py",
36
+ "line": 23,
37
+ "pattern": "class UtteranceSegmenter:",
38
+ "description": "**Stage 2: voice activity detection.** `UtteranceSegmenter` turns a stream of float32 chunks into discrete utterances using Silero VAD.\n\nTwo deliberate design choices worth noting up front (both in the docstring): a speech run only ends after `min_silence_ms` of *confirmed* silence (tolerant of pauses/disfluencies), and emitted utterances **include** that trailing silence on purpose — it helps STT avoid clipping the last phoneme."
39
+ },
40
+ {
41
+ "file": "src/convobox/vad/segmenter.py",
42
+ "line": 51,
43
+ "pattern": "def feed\\(self, chunk: np\\.ndarray\\)",
44
+ "description": "**Data handoff: arbitrary-length chunks → fixed 512-sample windows.** Silero's ONNX model only accepts exactly 512 samples (32ms) at 16kHz. Incoming capture chunks can be any length (and in `feed()`'s own tests, deliberately unaligned to 512 to prove no samples are dropped/duplicated), so they're buffered in `self._carry` and consumed in exact windows here.\n\nReturns a **list** of utterances (not zero-or-one) because a single large chunk can span the end of one utterance and the start of another."
45
+ },
46
+ {
47
+ "file": "src/convobox/vad/segmenter.py",
48
+ "line": 87,
49
+ "pattern": "def _process_window\\(self, window: np\\.ndarray\\)",
50
+ "description": "**The core state machine — and where a real hang bug lived.** Every window gets a `prob` from the model (this call happens unconditionally, every ~32ms, even during silence — see the Performance tour for why that's not actually wasted work).\n\nA few lines down, `_EXIT_HYSTERESIS` creates three zones: confidently speech, confidently silent, and an **ambiguous band** in between. The `elif is_speech:` branch just below (not a plain `else:`) is the fix for a bug where ambiguous-band frames were treated as speech and reset the silence timer on every frame — a speaker trailing off gradually could keep an utterance open forever. See the code comment right below it for the full story, and the Review Findings tour for the verification trail."
51
+ },
52
+ {
53
+ "file": "src/convobox/vad/segmenter.py",
54
+ "line": 131,
55
+ "pattern": "async def segment\\(",
56
+ "description": "**Data handoff: windows → one concatenated utterance array.** `segment()` is the async-generator wrapper `spike.py` actually calls — consumes chunks, yields complete utterances via `feed()`, and on stream close calls `flush()` to emit anything still in progress.\n\nOutput: a single float32 numpy array (all the 512-sample windows for that utterance concatenated together) — this is exactly what `LocalTranscriber.transcribe()` expects next."
57
+ },
58
+ {
59
+ "file": "src/convobox/stt/transcriber.py",
60
+ "line": 24,
61
+ "pattern": "class LocalTranscriber:",
62
+ "description": "**Stage 3: local speech-to-text.** `LocalTranscriber` loads the faster-whisper `WhisperModel` **once**, at construction — model loading is expensive, so this class is meant to be built once and reused across every utterance for the life of the process."
63
+ },
64
+ {
65
+ "file": "src/convobox/stt/transcriber.py",
66
+ "line": 33,
67
+ "pattern": "def transcribe\\(self, audio: np\\.ndarray\\)",
68
+ "description": "**Data handoff: utterance array → `TranscriptResult`.** Input is the float32 mono 16kHz array from the segmenter (defensively re-contiguous'd a couple lines down, since faster-whisper needs a contiguous buffer).\n\nOutput bundles the transcript text **with timing as a first-class field** (`latency_ms`, `duration_s`) — not just logged internally — because this project's whole first milestone was proving real accuracy/latency numbers before committing to backend/adapter design. Real measured numbers on this hardware: **STT ~140–200ms, RTF 0.05–0.13** (8–20x faster than real-time)."
69
+ },
70
+ {
71
+ "file": "src/convobox/safeword/detector.py",
72
+ "line": 5,
73
+ "pattern": "deliberately a plain normalized-substring match",
74
+ "description": "**Stage 4: the safeword check — deliberately dumb.** This comment says it outright: a plain normalized-substring match, **no ML, no fuzzy matching, no LLM**, on purpose. A hard stop is a safety-critical abort; a human reading this file must be able to predict exactly when it fires. That auditability guarantee is worth more here than the recall a fuzzy matcher would buy.\n\nThis runs on every transcript, in the orchestrator, **before** any busy/idle routing decision — see the next step."
75
+ },
76
+ {
77
+ "file": "src/convobox/orchestrator/orchestrator.py",
78
+ "line": 40,
79
+ "pattern": "async def handle_transcript\\(self, transcript: str\\)",
80
+ "description": "**Stage 5: the routing decision.** `handle_transcript` is where a finalized transcript becomes one of three concrete actions:\n\n1. **Hard stop** — checked first, unconditionally, and wins over everything else. It also stops any in-progress TTS/playback — if the user says stop, ConvoBox shouldn't keep talking.\n2. **Interject** — backend is busy, this is a side-comment, not a fresh task.\n3. **Fresh command** — backend is idle.\n\nThe `start_event_loop()` call near the top of this method is itself a fix: see the Review Findings tour for why `is_busy()` needs that loop running to stay accurate."
81
+ },
82
+ {
83
+ "file": "src/convobox/adapters/base.py",
84
+ "line": 32,
85
+ "pattern": "class BackendAdapter\\(ABC\\):",
86
+ "description": "**The backend adapter contract.** Three implementations exist and are each verified against a live instance: `OpenCodeAdapter` (HTTP+SSE), `ClaudeCodeAdapter` (bidirectional stream-json subprocess, `src/convobox/adapters/claude_code.py`), `CodexAdapter` (app-server JSON-RPC over stdio, `src/convobox/adapters/codex.py`). The docstring draws a hard line: `send_interject` must never derail a running task, `send_hard_stop` must abort it — these are never allowed to collapse into one \"interrupt\" action.\n\nA paragraph further down is an honesty clause added after review: a hard stop's real-world guarantee is bounded by what the backend actually exposes (see the next step for the concrete case)."
87
+ },
88
+ {
89
+ "file": "src/convobox/adapters/opencode.py",
90
+ "line": 82,
91
+ "pattern": "async def events\\(self\\) -> AsyncGenerator",
92
+ "description": "**The OpenCode backend adapter: HTTP + SSE.** `events()` opens a streaming GET and yields typed `BackendEvent`s parsed from each SSE frame in real time — not buffered until the connection closes.\n\n**Data handoff:** raw SSE `data:` frames (JSON text) → typed `BackendEvent` objects (`text | tool_call | tool_result | error | done`). The `finally` block a few lines down clears `self._busy = False` on **any** exit from this generator, not just an observed DONE — a dropped connection or exception used to leave `is_busy()` stuck `True` forever. See Review Findings for the full story."
93
+ },
94
+ {
95
+ "file": "src/convobox/orchestrator/orchestrator.py",
96
+ "line": 86,
97
+ "pattern": "def _on_event\\(self, event: BackendEvent\\)",
98
+ "description": "**Backend response → speech.** `_on_event` is where a `TEXT` event from the backend gets turned into spoken audio: strip code blocks (`strip_code_for_speech`, defined at the top of this file) so raw diffs aren't read aloud, then fire synthesis as a **background task**, not awaited inline.\n\nWhy fire-and-forget: synthesis can take noticeably longer than draining the *next* backend event (e.g. a DONE right behind this TEXT), and keeping `is_busy()` fresh matters more than serializing speech with event consumption. `AudioPlayer.play()` (next stage) is itself non-blocking, so this task's only real work is the `synthesize()` await."
99
+ },
100
+ {
101
+ "file": "src/convobox/tts/base.py",
102
+ "line": 14,
103
+ "pattern": "def sanitize_text\\(text: str\\)",
104
+ "description": "**The security boundary before any synthesis happens.** `sanitize_text` strips control characters and caps length on text that originates from **untrusted LLM output**. Every concrete `TTSEngine` must call this before synthesizing — this exists directly because of a real command-injection bug in an earlier related project (`voice-opencode`), where response text was interpolated straight into a shell command. See docs/LESSONS-FROM-VOICE-OPENCODE.md."
105
+ },
106
+ {
107
+ "file": "src/convobox/tts/base.py",
108
+ "line": 42,
109
+ "pattern": "def synthesize_stream\\(self, text: str\\) -> AsyncIterator",
110
+ "description": "**Streaming is the primary interface; `synthesize()` is the convenience wrapper.** `synthesize_stream` is the abstract method concrete engines implement. `synthesize()` just below it concatenates the stream — prefer `synthesize_stream` directly when time-to-first-audio matters, since it lets a caller start playback on the first chunk instead of waiting for the whole utterance."
111
+ },
112
+ {
113
+ "file": "src/convobox/tts/piper.py",
114
+ "line": 51,
115
+ "pattern": "async def synthesize_stream\\(self, text: str\\)",
116
+ "description": "**Stage 6: local TTS, genuinely streaming.** Piper's own `synthesize()` is a **blocking sync generator**. This bridges it to async the same way `MicrophoneStream.stream()` does — a background thread pushes converted float32 chunks onto a queue, and `synthesize_stream` yields each one via `asyncio.to_thread(chunk_queue.get)` as soon as it's ready.\n\n**Data handoff:** sanitized text → a stream of float32 PCM chunks. This used to buffer the *entire* response into one list before returning anything — measured improvement from switching to real streaming: **first chunk at ~143ms vs. ~1574ms total** on a 20-sentence passage (~11x faster time-to-first-audio)."
117
+ },
118
+ {
119
+ "file": "src/convobox/orchestrator/orchestrator.py",
120
+ "line": 225,
121
+ "pattern": "async def _speak\\(self, text: str\\)",
122
+ "description": "**Final handoff: synthesized audio → speakers, genuinely streamed.** `_speak` calls `self._player.play_stream(self._tts.synthesize_stream(text), ...)` — chunks flow from Piper to the speakers as they're produced, not buffered into one array first. (This step used to describe a whole-utterance `synthesize()`/`play()` call as the current state with streaming as a \"known next step\" — that step has since landed; see the '2. Review Findings' tour for the measured before/after.)\n\nAlso worth knowing from here: `Orchestrator.__init__` takes an optional `on_event` callback (any caller can observe the raw backend event stream, e.g. the `--tui` conversation view) and an optional `tier_responses` flag (speaks only the first paragraph of a multi-paragraph response, with `has_more_to_reveal()`/`speak_more()` exposing the rest for a \"tell me more\" follow-up) — both off by default, zero behavior change unless a caller opts in."
123
+ },
124
+ {
125
+ "file": "src/convobox/audio/playback.py",
126
+ "line": 9,
127
+ "pattern": "class AudioPlayer:",
128
+ "description": "**Stage 7: playback, with barge-in.** `AudioPlayer.stop()` is what makes barge-in possible — a response can be cut off the instant the user starts speaking again, rather than waiting for playback to finish. `Orchestrator.handle_transcript`'s hard-stop path (step 12 in this tour) calls exactly this.\n\n`play()` opens a fresh `sd.OutputStream` per call rather than reusing one — a real but modest overhead (tens of ms of device-open latency per response), deliberately left as-is for now since fixing it would mean reworking a test suite that deliberately asserts today's open/close-per-call behavior. See README → Status for the full reasoning."
129
+ },
130
+ {
131
+ "file": "src/convobox/web/app.py",
132
+ "description": "**A parallel subsystem, now fully wired, live, and viewable: the web UI.** `convobox/web/` (HistoryDB in `history.py`, the FastAPI app in `app.py`, the SSE fan-out in `stream.py`, `WebEventForwarder` in `bridge.py`, and a plain HTML/JS frontend in `static/index.html`) is a SQLite-history + live-event-stream server with a real page to view it, built per `docs/WEB-UI-ARCHITECTURE.md`. `scripts/run_convobox.py`'s `run()` chains a `WebEventForwarder` into `Orchestrator`'s existing `on_event` hook (no change to `Orchestrator` itself needed -- it already had this extension point) and starts a real `uvicorn.Server` as a background asyncio task whenever `web.enabled`/`--web` is set, lazily importing fastapi/uvicorn (the optional \"web\" extra) so CLI/TUI-only users never pay for it. `create_app()` mounts `static/index.html` as a Starlette `StaticFiles` catch-all at `\"/\"` -- registered LAST, after every `/api/*` route, so registration order (not special-casing) is what keeps the frontend from shadowing the API; a dedicated test asserts exactly that. `EventBroadcaster` carries plain dicts, not `BackendEvent` objects -- found while building a demo of this feature (2026-07-26) that the original wiring only ever forwarded backend responses/tool calls, never the user's OWN recognized speech that prompted them (no `BackendEvent` exists for that), so a captured session showed replies with no visible prompt. `WebEventForwarder.forward_transcript()` closes that gap -- wired into all three of run_convobox.py's `orchestrator.handle_transcript()` call sites (the mic loop, --text mode, and queued-interjection delivery). Live-verified end to end each step of the way, most recently: a real multi-turn session (transcript -> response -> tool_call -> tool_result -> response) through the actual ClaudeCodeAdapter + Orchestrator + HistoryDB, served back out over a real running uvicorn server's real HTTP API. `web.enabled` defaults off (zero behavior change for everyone else). The control plane has since grown well past view-only: `app.py`'s `/approve` endpoint (`action: Literal[\"approve\", \"deny\", \"explain\"]`) resolves a pending tool-call approval from the browser through the same `approval_bridge` a spoken approval phrase uses, plus a live `convobox.yaml` editor, stop/resume listening, and quit-the-session -- each added and individually live-verified, not built in one pass. See `docs/WEB-UI-ARCHITECTURE.md` for the full account and `docs/WEB-UI-USAGE.md` for what a user actually sees."
133
+ },
134
+ {
135
+ "title": "End of tour",
136
+ "description": "That's the full `src/convobox/` loop: **mic → VAD → STT → safeword → orchestrator → adapter → TTS → playback**, with a hard-stop escape hatch that can short-circuit at the orchestrator regardless of where the rest of the pipeline is.\n\n**Not covered by this tour, since it lives in `scripts/run_convobox.py`'s main loop rather than `src/convobox/`**: several more pure state-machine gates sit between the safeword check and the orchestrator call in the REAL running loop -- `ListeningGate` (pause/resume: \"stop listening\" hard-stops and pauses, only the wake word resumes), `BargeInMonitor` + `QueuedInterjection` (the two-axis interrupt-preset grid: what happens to the current turn vs. the user's new words), and `ContinuePromptGate` (the response-tiering silence-timeout gate: \"continue\"/\"go on\" reveals the rest of a tiered response, silence implies done). All follow the exact same pure-state-machine shape as the classes this tour walked through, unit-tested independent of real audio/clocks -- see `docs/DESIGN-barge-in.md` and `docs/DESIGN-0.3.0-interaction-and-safety.md` for the design reasoning, not re-derived here.\n\nTwo follow-up tours: **\"2. Review Findings: Security & Performance\"** for the concrete bugs a review pass found and fixed in this exact code, and **\"3. Extension Points: Modularity & Pluggability\"** for exactly where a collaborator can plug in a new backend adapter or TTS engine (and where they deliberately can't). Command Palette → CodeTour: Start Tour."
137
+ }
138
+ ]
139
+ }