Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 okqixiaobao727-design
Copyright (c) 2026 Simon-AI-coding

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
100 changes: 17 additions & 83 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@
![Apple Silicon](https://img.shields.io/badge/Apple%20Silicon-required-111111)
![Licence](https://img.shields.io/badge/licence-MIT-111111)

<!-- PENDING ASSET: the demo video. Drag scratch/demo-recorder/out/take.tvuVMJ/demo.mp4
into any GitHub comment box, then replace this whole comment with the
https://github.com/user-attachments/... URL it returns, on a line of its own with
no Markdown around it, followed by:
https://github.com/user-attachments/assets/a0ea108c-8c5b-45a1-b966-877a05650c65

*Recorded unattended; the narration and the spoken lines are synthesized speech.*
-->
*Recorded unattended; the narration and the spoken lines are synthesized speech.*

One voice call covers every Claude Code and Codex session on your Mac: it tells you which one
stopped and what it stopped on, and carries your spoken answer back into it. The call rides
Expand All @@ -30,17 +26,18 @@ stopped and what it stopped on, and carries your spoken answer back into it. The

## Screenshots

| | |
|---|---|
| ![The Duty Lamp](docs/images/duty-lamp.png)<br>The Duty Lamp sits above everything and counts what is waiting on you. Hovering it offers Hang up, Control and Settings, and shows the session that moved last. | ![The Control Panel](docs/images/control-panel.png)<br>The Control Panel during a call: the three switches, the Call Agent's model and context use, the counts, and every live session. |
| ![Settings](docs/images/settings.png)<br>Settings. Nine voices to pick the Live Call's from, and the realtime model behind it. | ![Telegram](docs/images/telegram.png)<br>Telegram. A stop notice you can reply to, `/sessions` drawn as buttons, and the bot's own command menu. |
![The Duty Lamp, Settings, the Control Panel and Telegram](docs/images/showcase.png)

The Duty Lamp floats above everything and counts what is waiting on you, with Settings below it.
In the middle is the Control Panel during a call. On the right is Telegram, where you reply to a
stop notice and `/sessions` comes back as buttons.

## Install

On an Apple Silicon Mac:

```sh
curl -fsSL https://raw.githubusercontent.com/okqixiaobao727-design/GPT-VoiceCoding/main/scripts/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/Simon-AI-coding/GPT-VoiceCoding/main/scripts/install.sh | sh
```

It builds an ad-hoc-signed app from source, puts it in `/Applications` and opens it. There is no
Expand Down Expand Up @@ -68,80 +65,17 @@ already binds. Codex is read and written through the shared `codex app-server` t
starts and your own terminals join, which is the same process the Live Call's realtime route
rides.

```mermaid
flowchart LR
subgraph terminals["Your own terminals"]
cc["Claude Code Session"]
cx["Codex Session"]
end

appserver["codex app-server<br/>started by this product,<br/>joined by your Sessions"]

subgraph engine["The engine"]
core["Bridge Core<br/>every policy, one source of truth"]
agents["Agent adapters"]
callad["Call adapter"]
chan["Companion Channel adapter"]
plane["Control plane<br/>JSON over a Unix socket"]
core --- agents
core --- callad
core --- chan
core --- plane
end

live["Live Call<br/>Voice speaks, Call Agent acts"]
tg["Telegram"]
shell["Menu-bar app<br/>Duty Lamp, Control Panel"]
ctl["bridgectl"]

cc <-->|"hooks, roster, inbox socket"| agents
cx --- appserver
appserver --- agents
appserver -->|"realtime route"| callad
callad --- live
chan --- tg
plane --- shell
plane --- ctl
```

What happens when a session stops:

```mermaid
sequenceDiagram
participant S as Your Session
participant C as Bridge Core
participant T as Telegram
participant K as Call Keeper
participant V as Voice
participant A as Call Agent
participant U as You

S->>C: stops, waiting on a decision
C->>C: writes the reading to the roster
C->>T: Stop Notice, as an Anchor you can reply to
C->>K: wake
K->>V: dials, briefed from the roster as it stands
V->>U: speaks the Session Brief
U->>V: "use email login, and give me the steps"
V->>A: hands the job over
A->>C: relay
C->>S: Answer Relay
```

Or, without the call:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/architecture-dark.svg">
<img alt="Your terminals, codex app-server, the engine with Bridge Core at its centre, and the Live Call, Telegram, menu-bar app and bridgectl it reaches" src="docs/images/architecture-light.svg">
</picture>

```mermaid
sequenceDiagram
participant T as Telegram
participant U as You
participant C as Bridge Core
participant S as Your Session
What happens when a session stops, whether you answer on the call or on Telegram:

T->>U: Stop Notice
U->>T: replies to it
T->>C: inbound text, naming the Anchor's Session
C->>S: Answer Relay
```
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/session-stops-dark.svg">
<img alt="Sequence: a session stops, Bridge Core sends a Stop Notice and wakes the Call Keeper, and your answer comes back as an Answer Relay either through the call or through a Telegram reply" src="docs/images/session-stops-light.svg">
</picture>

The Voice has no tools. It speaks from what the engine hands it and passes anything that reads
as a job to the Call Agent, which is the only half on the call that can run a control-plane verb.
Expand Down
8 changes: 4 additions & 4 deletions docs/adr/0001-hub-and-spoke-bridge-core-with-seams.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# 1. Bridge Core is a hub; everything else is a deep module behind a seam

Date: 2026-08-20 · Status: Accepted · Source: [module map and seams](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/issues/18)
Date: 2026-08-20 · Status: Accepted · Source: [module map and seams](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/issues/18)

The reference implementation let policy leak into mechanism: Stop Notice delivery was hard-wired to one GUI socket ([#15](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/issues/15)), and nothing owned "exactly one call is up" ([#16](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/issues/16)).
The reference implementation let policy leak into mechanism: Stop Notice delivery was hard-wired to one GUI socket ([#15](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/issues/15)), and nothing owned "exactly one call is up" ([#16](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/issues/16)).

## Decision

**`core` — Bridge Core — owns all policy**: Stop Notice escalation, Relay queueing against the Reply Window, the one-call invariant, switch adjudication. (The Approval budget and its fallback were on this list until [#191](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/191) withdrew both — a held hook's life is the wire's, per ADR 0015 as amended — and the split itself is unchanged by that.) Everything else is a deep module reached through a seam:
**`core` — Bridge Core — owns all policy**: Stop Notice escalation, Relay queueing against the Reply Window, the one-call invariant, switch adjudication. (The Approval budget and its fallback were on this list until [#191](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/191) withdrew both — a held hook's life is the wire's, per ADR 0015 as amended — and the split itself is unchanged by that.) Everything else is a deep module reached through a seam:

| Seam | Adapters |
| --- | --- |
Expand All @@ -27,7 +27,7 @@ Splitting principles:

The engine is one Python asyncio process, a direct child of the menu-bar shell (ADR 0005). Bridge Core memory is the single source of truth; the durable subset is written by one internal storage component and read by nothing else.

Seam responsibilities the verbs encode: Reply-Window queueing is Bridge Core policy — adapters deliver, never queue. Classifying inbound Companion Channel text (control command / Answer Relay / delegation) is Bridge Core's job. The Reply Window level is *asked for* synchronously at registration and only its changes are reported afterwards, because an event raised before the roster holds the Session is dropped and never repeated ([#27](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/27)). Seam verbs extend only through a ruling with a use case behind it.
Seam responsibilities the verbs encode: Reply-Window queueing is Bridge Core policy — adapters deliver, never queue. Classifying inbound Companion Channel text (control command / Answer Relay / delegation) is Bridge Core's job. The Reply Window level is *asked for* synchronously at registration and only its changes are reported afterwards, because an event raised before the roster holds the Session is dropped and never repeated ([#27](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/27)). Seam verbs extend only through a ruling with a use case behind it.

## Consequences

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 2. The control plane is never gated by switches

Date: 2026-08-19 · Status: Accepted · Source: [legacy ADR 0006](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/blob/main/docs/adr/0006-the-control-plane-is-never-gated-by-switches.md), re-affirmed in [#18](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/issues/18)
Date: 2026-08-19 · Status: Accepted · Source: [legacy ADR 0006](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/blob/main/docs/adr/0006-the-control-plane-is-never-gated-by-switches.md), re-affirmed in [#18](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/issues/18)

A user who switched Duty off from the Companion Channel could never switch it back on remotely — locked out by the switch meant to protect them. Per-switch exemption lists were rejected as special-casing that rots.

Expand Down
4 changes: 2 additions & 2 deletions docs/adr/0003-the-engine-reports-what-it-loaded.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 3. The engine reports what it loaded, and liveness checks read that answer

Date: 2026-08-19 · Status: Accepted · Source: [legacy ADR 0003](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/blob/main/docs/adr/0003-companion-channel-liveness-is-verifiable.md) (full failure account)
Date: 2026-08-19 · Status: Accepted · Source: [legacy ADR 0003](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/blob/main/docs/adr/0003-companion-channel-liveness-is-verifiable.md) (full failure account)

The Companion Channel was dead for a day while every guard said nothing: `status` echoed the config file the client had just read, and a reachable daemon proved only that some daemon answered.

Expand All @@ -14,4 +14,4 @@ The check is level-triggered with three outcomes: **pass** (configured and loade

This is the rule for every seam with a pluggable adapter; `verify` is a seam verb for this reason.

**Amended by [#159](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/159): `MANUAL` is structurally unavailable to an adapter that reports a real loaded implementation.** A far side that is uninstalled rather than unconfigured therefore reports `FAIL`: the adapter is configured and loaded, and the machine does not have what it needs to reach that far side.
**Amended by [#159](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/159): `MANUAL` is structurally unavailable to an adapter that reports a real loaded implementation.** A far side that is uninstalled rather than unconfigured therefore reports `FAIL`: the adapter is configured and loaded, and the machine does not have what it needs to reach that far side.
2 changes: 1 addition & 1 deletion docs/adr/0004-the-engine-owns-its-log.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 4. The engine owns its log, so rotation can rename rather than truncate

Date: 2026-08-18 · Status: Accepted · Source: [legacy ADR 0004](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/blob/main/docs/adr/0004-bounded-log-files.md) (measurement), amended in [#4](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/4) and [#33](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/33)
Date: 2026-08-18 · Status: Accepted · Source: [legacy ADR 0004](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/blob/main/docs/adr/0004-bounded-log-files.md) (measurement), amended in [#4](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/4) and [#33](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/33)

The legacy log grew ~1 GB/month, 98% one inherited-environment `libmalloc` line, and could not be rotated because a shell redirect owned the descriptor. Copy-truncate loses lines written between the copy and the truncate.

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0005-the-engine-lives-inside-the-app-bundle.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 5. The engine lives inside the app bundle, because that is what earns the microphone grant

Date: 2026-08-20 · Status: Accepted · Source: [packaging research](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/issues/14), [TCC attribution probe](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/issues/24)
Date: 2026-08-20 · Status: Accepted · Source: [packaging research](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/issues/14), [TCC attribution probe](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/issues/24)

macOS TCC attaches the microphone grant to whoever owns the process. The legacy LaunchAgent daemon put it on the bare `python3.12` path. The probe showed the grant lands on the bundle id whenever the executable is *inside* the `.app`, and collapses to the binary path outside it — launchd versus direct spawn makes no difference.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 6. The Claude Session Channel server is Python, and speaks MCP with the standard library alone

Date: 2026-08-21 · Status: Superseded by [ADR 0013](0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md) · Source: [#8](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/8)
Date: 2026-08-21 · Status: Superseded by [ADR 0013](0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md) · Source: [#8](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/8)

The Answer Relay's route into a Claude Session is an MCP channel server that Claude Code spawns from a plugin manifest; the bridge only dials its socket. Legacy wrote it in Node (`claude-channel/channel.mjs`), which would put a second runtime in the bundle (ADR 0005) and outside the test suite.

Expand All @@ -12,4 +12,4 @@ The Answer Relay's route into a Claude Session is an MCP channel server that Cla

One runtime in the bundle; the handshake is covered by `pytest`. The only deliberate byte-level difference from `channel.mjs` is the bridge–channel wire carrying an explicit Relay kind. Handshake drift surfaces as a classified non-delivery, never as silence.

This route depends on launch-time injection and serves the pre-#67 Session definition. [#71](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/71) proved the inbox-socket route live on 2.1.245 and it replaces this one: see [ADR 0013](0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md). The server, its wire and its protocol transcription were removed in [#77](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/77); what the decision leaves behind is its one durable lesson, which ADR 0013 keeps — the channel's acknowledgement was a real receipt, and giving it up for reach is what made "an accepted write is not a receipt" a rule rather than a preference.
This route depends on launch-time injection and serves the pre-#67 Session definition. [#71](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/71) proved the inbox-socket route live on 2.1.245 and it replaces this one: see [ADR 0013](0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md). The server, its wire and its protocol transcription were removed in [#77](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/77); what the decision leaves behind is its one durable lesson, which ADR 0013 keeps — the channel's acknowledgement was a real receipt, and giving it up for reach is what made "an accepted write is not a receipt" a rule rather than a preference.
4 changes: 2 additions & 2 deletions docs/adr/0007-the-approval-hook-is-a-session-scoped-plugin.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 7. The Claude Approval Relay's hook is registered as a session-scoped plugin

Date: 2026-08-21 · Status: Superseded by [ADR 0011](0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md) · Source: [#14](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/14)
Date: 2026-08-21 · Status: Superseded by [ADR 0011](0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md) · Source: [#14](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/14)

The Approval Relay's route into a Claude Session is the `PermissionRequest` hook. Three ways to register it differ by blast radius: `~/.claude/settings.json` (the engine writes a user-owned file), an installed plugin (fires in every Claude session on the machine), or `claude --plugin-dir <path>` (that session only — verified live on 2.1.238).

Expand All @@ -14,4 +14,4 @@ A 2026-08-25 probe on 2.1.241 with an isolated `HOME` showed both plugins load i

Every launched Claude Session must pass `--plugin-dir` for the rendered hook. `--plugin-dir` is undocumented and part of the Claude Code version pin; an upgrade re-verifies it with the hook contract. If Claude Code withdraws it, the fallback is the installed-plugin mechanism with the hook's bootstrap-variable check as scope — a weaker guarantee, to be reopened here.

This route depends on launch-time injection and serves the pre-#67 Session definition. [#71](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/71) proved the user-scope route live on 2.1.245 and it replaces this one: see [ADR 0011](0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md).
This route depends on launch-time injection and serves the pre-#67 Session definition. [#71](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/71) proved the user-scope route live on 2.1.245 and it replaces this one: see [ADR 0011](0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md).
2 changes: 1 addition & 1 deletion docs/adr/0010-legacy-is-the-behaviour-spec.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 10. The seam architecture stays, and the first generation is the behaviour spec it must satisfy

Date: 2026-08-25 · Status: Accepted · Source: [#58](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/58)
Date: 2026-08-25 · Status: Accepted · Source: [#58](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/58)

This repository was built in two days from an architecture spec. In the four days after, twenty-three bug tickets were found by a human against the real environment while the suite stayed green — most describing behaviour the first generation already had. The build issues were derived from the seams, and nothing pointed a builder at the code that proved what the product must do.

Expand Down
Loading
Loading