diff --git a/LICENSE b/LICENSE
index 26448e3..6230a61 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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
diff --git a/README.md b/README.md
index 76e2b2b..fbdedac 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,9 @@


-
+*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
@@ -30,17 +26,18 @@ stopped and what it stopped on, and carries your spoken answer back into it. The
## Screenshots
-| | |
-|---|---|
-|  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 during a call: the three switches, the Call Agent's model and context use, the counts, and every live session. |
-|  Settings. Nine voices to pick the Live Call's from, and the realtime model behind it. |  Telegram. A stop notice you can reply to, `/sessions` drawn as buttons, and the bot's own command menu. |
+
+
+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
@@ -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 started by this product, joined by your Sessions"]
-
- subgraph engine["The engine"]
- core["Bridge Core every policy, one source of truth"]
- agents["Agent adapters"]
- callad["Call adapter"]
- chan["Companion Channel adapter"]
- plane["Control plane JSON over a Unix socket"]
- core --- agents
- core --- callad
- core --- chan
- core --- plane
- end
-
- live["Live Call Voice speaks, Call Agent acts"]
- tg["Telegram"]
- shell["Menu-bar app 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:
+
+
+
+
-```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
-```
+
+
+
+
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.
diff --git a/docs/adr/0001-hub-and-spoke-bridge-core-with-seams.md b/docs/adr/0001-hub-and-spoke-bridge-core-with-seams.md
index 7c5a353..ee7f07c 100644
--- a/docs/adr/0001-hub-and-spoke-bridge-core-with-seams.md
+++ b/docs/adr/0001-hub-and-spoke-bridge-core-with-seams.md
@@ -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 |
| --- | --- |
@@ -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
diff --git a/docs/adr/0002-the-control-plane-is-never-gated-by-switches.md b/docs/adr/0002-the-control-plane-is-never-gated-by-switches.md
index 00fdbc6..6b4729a 100644
--- a/docs/adr/0002-the-control-plane-is-never-gated-by-switches.md
+++ b/docs/adr/0002-the-control-plane-is-never-gated-by-switches.md
@@ -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.
diff --git a/docs/adr/0003-the-engine-reports-what-it-loaded.md b/docs/adr/0003-the-engine-reports-what-it-loaded.md
index c853a76..662b787 100644
--- a/docs/adr/0003-the-engine-reports-what-it-loaded.md
+++ b/docs/adr/0003-the-engine-reports-what-it-loaded.md
@@ -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.
@@ -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.
diff --git a/docs/adr/0004-the-engine-owns-its-log.md b/docs/adr/0004-the-engine-owns-its-log.md
index fa86660..6972f65 100644
--- a/docs/adr/0004-the-engine-owns-its-log.md
+++ b/docs/adr/0004-the-engine-owns-its-log.md
@@ -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.
diff --git a/docs/adr/0005-the-engine-lives-inside-the-app-bundle.md b/docs/adr/0005-the-engine-lives-inside-the-app-bundle.md
index ca6b149..ff528a9 100644
--- a/docs/adr/0005-the-engine-lives-inside-the-app-bundle.md
+++ b/docs/adr/0005-the-engine-lives-inside-the-app-bundle.md
@@ -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.
diff --git a/docs/adr/0006-the-claude-channel-server-is-python-and-stdlib-only.md b/docs/adr/0006-the-claude-channel-server-is-python-and-stdlib-only.md
index 3cbc6db..c72eb74 100644
--- a/docs/adr/0006-the-claude-channel-server-is-python-and-stdlib-only.md
+++ b/docs/adr/0006-the-claude-channel-server-is-python-and-stdlib-only.md
@@ -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.
@@ -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.
diff --git a/docs/adr/0007-the-approval-hook-is-a-session-scoped-plugin.md b/docs/adr/0007-the-approval-hook-is-a-session-scoped-plugin.md
index 4e58504..f918479 100644
--- a/docs/adr/0007-the-approval-hook-is-a-session-scoped-plugin.md
+++ b/docs/adr/0007-the-approval-hook-is-a-session-scoped-plugin.md
@@ -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 ` (that session only — verified live on 2.1.238).
@@ -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).
diff --git a/docs/adr/0010-legacy-is-the-behaviour-spec.md b/docs/adr/0010-legacy-is-the-behaviour-spec.md
index e7deb5e..b756a5c 100644
--- a/docs/adr/0010-legacy-is-the-behaviour-spec.md
+++ b/docs/adr/0010-legacy-is-the-behaviour-spec.md
@@ -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.
diff --git a/docs/adr/0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md b/docs/adr/0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md
index 2ecb010..62d0068 100644
--- a/docs/adr/0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md
+++ b/docs/adr/0011-the-claude-hooks-are-a-fingerprinted-block-in-the-user-settings-file.md
@@ -1,8 +1,8 @@
# 11. The Claude hooks are a fingerprinted block in the user's settings file
-Date: 2026-08-25 · Status: Accepted · Source: [#71](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/71)
+Date: 2026-08-25 · Status: Accepted · Source: [#71](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/71)
-v1.0 is a bridge over every Session the user starts ([#67](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/67)), so nothing can be injected at launch and ADR 0007's `--plugin-dir` no longer reaches the Sessions that matter. Approval has no other route: upstream enforces that a peer message is never the user's approval, so the inbox socket that carries the Answer Relay can never carry a verdict. Two user-scope mechanisms remain — a plugin named in `enabledPlugins`, or a block in the config directory's `settings.json` — and both were probed live on Claude Code 2.1.245 in `~/.claude-b`, where #71 records the runs.
+v1.0 is a bridge over every Session the user starts ([#67](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/67)), so nothing can be injected at launch and ADR 0007's `--plugin-dir` no longer reaches the Sessions that matter. Approval has no other route: upstream enforces that a peer message is never the user's approval, so the inbox socket that carries the Answer Relay can never carry a verdict. Two user-scope mechanisms remain — a plugin named in `enabledPlugins`, or a block in the config directory's `settings.json` — and both were probed live on Claude Code 2.1.245 in `~/.claude-b`, where #71 records the runs.
## Decision
diff --git a/docs/adr/0012-installation-runs-at-first-launch.md b/docs/adr/0012-installation-runs-at-first-launch.md
index bb766df..09e64d8 100644
--- a/docs/adr/0012-installation-runs-at-first-launch.md
+++ b/docs/adr/0012-installation-runs-at-first-launch.md
@@ -1,28 +1,28 @@
# 12. Installation runs at first launch, from a package that needs no engine
-Date: 2026-08-26 · Status: Accepted · Source: [#86](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/86)
+Date: 2026-08-26 · Status: Accepted · Source: [#86](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/86)
-v1.0 has to place two artifacts in files the user owns: ADR 0011's fingerprinted hook block in `/settings.json`, and the login `LaunchAgent` that starts Codex's shared app-server daemon ([#82](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/82)). Nothing in this repository owned that. `bridgectl` dials a socket and holds no policy and no state; the menu-bar panel says in its own first line that it is not an editor for installation settings; and a `.app` dragged into `/Applications` has no install step at all — macOS copies a directory and runs nothing. The two installer-shaped modules that did exist rendered ADR 0007's `--plugin-dir` plugin, which ADR 0011 supersedes, and had no caller left once the launcher was parked ([#72](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/72)).
+v1.0 has to place two artifacts in files the user owns: ADR 0011's fingerprinted hook block in `/settings.json`, and the login `LaunchAgent` that starts Codex's shared app-server daemon ([#82](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/82)). Nothing in this repository owned that. `bridgectl` dials a socket and holds no policy and no state; the menu-bar panel says in its own first line that it is not an editor for installation settings; and a `.app` dragged into `/Applications` has no install step at all — macOS copies a directory and runs nothing. The two installer-shaped modules that did exist rendered ADR 0007's `--plugin-dir` plugin, which ADR 0011 supersedes, and had no caller left once the launcher was parked ([#72](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/72)).
## Decision
**Installation is `gpt_voicecoding.installation` — four modules, and not a seam.** ADR 0001 puts a seam only where something varies, and names two adapters when it does. Here nothing varies: there are two artifacts, not two implementations of one interface, and no wire between the parts. So there is no vocabulary to share and no protocol to declare. `__init__.py` holds the one small result type every item answers with, the one write they all go through, and the recorded intent; one module per artifact holds one mechanism each; `__main__.py` names the items explicitly, the way the composition root names adapters. With two artifacts and no plugin story, a list of two calls **is** the registry, and a third item is a line. Bridge Core is not involved at any point — installation is not policy, and it has to work when no engine is running.
-At the time of writing there is one item, `claude_hooks.py`. The Codex login `LaunchAgent` is [#83](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/83)'s to write and it joins those lists there; nothing about the boundary waits on it. It has since been written, as `codex_launch_agent.py`, and the paragraph below is what it added.
+At the time of writing there is one item, `claude_hooks.py`. The Codex login `LaunchAgent` is [#83](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/83)'s to write and it joins those lists there; nothing about the boundary waits on it. It has since been written, as `codex_launch_agent.py`, and the paragraph below is what it added.
**The menu-bar shell runs it at every launch, and it writes only when the machine disagrees with what it should be.** A dragged `.app` has no install step, so first launch *is* the install: the shell spawns the bundled interpreter on `bridge-install reconcile` before it spawns the engine, and reconcile writes nothing when the machine already agrees. It deliberately does not go through the engine: the engine refuses to start without a `config.toml` the user has to write by hand, and an installation that waited for that file would not have happened by the time the user first opens the app.
-**The shell is a trigger and nothing more.** The merge, the fingerprint, the render and the reversibility stay in Python, in one place. A second copy of them in Swift is the shape of [#47](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/47), where the control-plane socket path is built independently on both sides with no test holding the two together.
+**The shell is a trigger and nothing more.** The merge, the fingerprint, the render and the reversibility stay in Python, in one place. A second copy of them in Swift is the shape of [#47](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/47), where the control-plane socket path is built independently on both sides with no test holding the two together.
**Uninstall is honoured, so the intent is written down.** Reconcile-at-every-launch and a meaningful uninstall cannot both be true unless something records that the user took the artifacts back. That record is `installation.json`, in the product's own directory rather than in any file the user owns: absent means never installed, and `{"wanted": false}` means uninstalled and reconcile leaves the machine alone. With the intent recorded, restoring a block that has gone missing is honouring what the user asked for rather than overriding them — and it is the only repair there is for the exposure the next paragraph describes.
**A write is atomic, read back, and reported.** Each item renders its whole file, writes it to a temporary path beside the target and `os.replace`s it, then reads it back and compares. Legacy wrote in place (`legacy@1d32845:bridge/hookconfig.py:153-155`); this is adapted rather than copied, because ADR 0011 records concurrent writers of `~/.claude/settings.json` as a real and untested exposure. Atomicity is what keeps a half-written settings file off the disk; it cannot prevent a lost update, where another writer's change between our read and our write is simply gone. So the read-back is the honest part: a mismatch is reported as a failed install naming the file, and never retried into a loop with whoever else is writing.
-**Amended by [#83](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/83): the Codex item carries one process action, and it is only ever `bootstrap`.** The principle above is *act, read back, report*, not *write files only* — a plist that launchd has not been told about does nothing until the user next logs out, so an install that only wrote the file would leave the Codex half of a freshly-installed product dead on the day it was installed, which is the one day ADR 0012 exists to cover. So that item writes the plist through the same atomic write and then asks launchd to load the job, and the read-back is `launchctl print` rather than the exit status, because bootstrapping an already-loaded job fails and that failure is the state it was reaching for. **No item ever runs `bootout`, and the asymmetry is the rule: this product starts a daemon the user's Sessions will join, and never stops one they are attached to.** By the time an uninstall runs, the user's own `codex` TUIs are thin clients of that daemon ([#82](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/82)), so an uninstall removes the plist and lets the running daemon live out the login session, and a render that changed while the job is loaded is written but not reloaded — it applies at the next login, and the `Outcome` says so. Which means the file on disk and the job launchd is holding can disagree for one login session, so a status run asks launchd which *program* it is actually running rather than reading our own file back: a job still on the previous render reports `stale` and names what it is running, because the alternative is a report that calls a machine current on the strength of a file nothing has loaded yet. Re-loading a job that died is the *next launch's* reconcile and nothing else, which is an event and not the polling supervisor #83 forbids.
+**Amended by [#83](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/83): the Codex item carries one process action, and it is only ever `bootstrap`.** The principle above is *act, read back, report*, not *write files only* — a plist that launchd has not been told about does nothing until the user next logs out, so an install that only wrote the file would leave the Codex half of a freshly-installed product dead on the day it was installed, which is the one day ADR 0012 exists to cover. So that item writes the plist through the same atomic write and then asks launchd to load the job, and the read-back is `launchctl print` rather than the exit status, because bootstrapping an already-loaded job fails and that failure is the state it was reaching for. **No item ever runs `bootout`, and the asymmetry is the rule: this product starts a daemon the user's Sessions will join, and never stops one they are attached to.** By the time an uninstall runs, the user's own `codex` TUIs are thin clients of that daemon ([#82](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/82)), so an uninstall removes the plist and lets the running daemon live out the login session, and a render that changed while the job is loaded is written but not reloaded — it applies at the next login, and the `Outcome` says so. Which means the file on disk and the job launchd is holding can disagree for one login session, so a status run asks launchd which *program* it is actually running rather than reading our own file back: a job still on the previous render reports `stale` and names what it is running, because the alternative is a report that calls a machine current on the strength of a file nothing has loaded yet. Re-loading a job that died is the *next launch's* reconcile and nothing else, which is an event and not the polling supervisor #83 forbids.
-**Amended by [#132](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/132): the program is only the foreign-origin guard; the loaded render's authority is this product's own SHA record plus launchd's GUI-login ASID.** #129 changed resource-limit keys without changing the program, and `launchctl print` exposed the program while exposing no resource-limit field, so program equality called an older loaded definition `current`. When this item explicitly bootstraps a plist, it records the exact file SHA and the job's ASID in `installation.json`, beside and independently of the user's install intent. `status` stays read-only and compares the file on disk with that recorded SHA: equal is `current`; different is `stale` until the next login; missing evidence is an unknown loaded render and fails closed as `stale`. A reconcile first reads the existing file, then renders: when the ASID changes, the existing file's SHA becomes authoritative because it is the file the new login had an opportunity to load. A changed render written later in that same reconcile remains stale. Within one ASID, neither a byte-identical reconcile nor a kickstart is evidence of a reload, so neither replaces the record. The program comparison remains and a foreign program remains stale. Legacy has no loaded-render read-back: `legacy@1d32845:install.sh:174-195` bootstrapped the job and never recorded what launchd loaded, so this behavior is not ported.
+**Amended by [#132](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/132): the program is only the foreign-origin guard; the loaded render's authority is this product's own SHA record plus launchd's GUI-login ASID.** #129 changed resource-limit keys without changing the program, and `launchctl print` exposed the program while exposing no resource-limit field, so program equality called an older loaded definition `current`. When this item explicitly bootstraps a plist, it records the exact file SHA and the job's ASID in `installation.json`, beside and independently of the user's install intent. `status` stays read-only and compares the file on disk with that recorded SHA: equal is `current`; different is `stale` until the next login; missing evidence is an unknown loaded render and fails closed as `stale`. A reconcile first reads the existing file, then renders: when the ASID changes, the existing file's SHA becomes authoritative because it is the file the new login had an opportunity to load. A changed render written later in that same reconcile remains stale. Within one ASID, neither a byte-identical reconcile nor a kickstart is evidence of a reload, so neither replaces the record. The program comparison remains and a foreign program remains stale. Legacy has no loaded-render read-back: `legacy@1d32845:install.sh:174-195` bootstrapped the job and never recorded what launchd loaded, so this behavior is not ported.
-**Amended by [#275](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/275): a login is the pair (ASID, boot session), because the ASID alone repeats across reboots.** The premise above — that a changed ASID proves a new login — is true within one boot and false across boots: macOS hands the first GUI login of every boot the same audit session id. Measured on the reference machine: a record written 2026-09-05 carrying asid `100024`, and after a reboot on 2026-09-07 `launchctl print` reporting asid `100024` again. So the new-login branch never ran, a record written before the render on disk was trusted as this login's evidence, and `status` reported "loaded job is a previous render; applies at the next login" while launchd held exactly the file on disk. The mirror image is the dangerous one and is the same collision: a carried-over record whose SHA happens to equal the new render reports `current` while launchd holds something else, which is the state this read-back exists to make impossible. So the record and the `launchctl print` read-back both carry the kernel's `kern.bootsessionuuid` beside the ASID, and every "did the login change?" compares the pair — a different ASID **or** a different boot session is a new login. A record that carries an ASID and no boot session (anything written before this) cannot say which login it was written in, so it is treated exactly as a record with no ASID is: the loaded render is unknown and it reports `stale` until the next login, at which point the pair differs, the new-login branch runs, and the on-disk SHA becomes the authority. It is never taken for a new login outright — that is the false-`current` direction. An unreadable boot session is the same unknown and fails closed the same way.
+**Amended by [#275](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/275): a login is the pair (ASID, boot session), because the ASID alone repeats across reboots.** The premise above — that a changed ASID proves a new login — is true within one boot and false across boots: macOS hands the first GUI login of every boot the same audit session id. Measured on the reference machine: a record written 2026-09-05 carrying asid `100024`, and after a reboot on 2026-09-07 `launchctl print` reporting asid `100024` again. So the new-login branch never ran, a record written before the render on disk was trusted as this login's evidence, and `status` reported "loaded job is a previous render; applies at the next login" while launchd held exactly the file on disk. The mirror image is the dangerous one and is the same collision: a carried-over record whose SHA happens to equal the new render reports `current` while launchd holds something else, which is the state this read-back exists to make impossible. So the record and the `launchctl print` read-back both carry the kernel's `kern.bootsessionuuid` beside the ASID, and every "did the login change?" compares the pair — a different ASID **or** a different boot session is a new login. A record that carries an ASID and no boot session (anything written before this) cannot say which login it was written in, so it is treated exactly as a record with no ASID is: the loaded render is unknown and it reports `stale` until the next login, at which point the pair differs, the new-login branch runs, and the on-disk SHA becomes the authority. It is never taken for a new login outright — that is the false-`current` direction. An unreadable boot session is the same unknown and fails closed the same way.
**Nothing rolls back.** The two items are independent — a Claude hook block that landed is not made wrong by a LaunchAgent that did not. A run reports per item and exits non-zero if any item failed, and the item that succeeded stays installed.
@@ -34,15 +34,15 @@ At the time of writing there is one item, `claude_hooks.py`. The Codex login `La
**The dependency direction is `adapters → installation`, never back.** The Claude adapter's `verify` asks installation what is on disk, because "is our block present and current" is exactly ADR 0003's question about reaching a far side. Installation imports no adapter, no seam and no `core`: the hook's event names and module path are strings, so nothing has to be imported to name them.
-**Amended by [#159](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/159): the Claude adapter now makes that read before it dials a Session inbox.** Installation combines its existing on-disk inspection with the recorded install intent into one read-only reach answer. The intent changes the failure's wording, not its verdict: `wanted: false` is still `FAIL` while the loaded adapter is configured to reach Claude Sessions, because the machine and configuration disagree. This behaviour is new rather than ported: legacy had no read-only hook inspection (`legacy@1d32845:bridge/hookconfig.py:143-156`), and its status verb only pinged the daemon (`legacy@1d32845:bridge/__main__.py:205-213`).
+**Amended by [#159](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/159): the Claude adapter now makes that read before it dials a Session inbox.** Installation combines its existing on-disk inspection with the recorded install intent into one read-only reach answer. The intent changes the failure's wording, not its verdict: `wanted: false` is still `FAIL` while the loaded adapter is configured to reach Claude Sessions, because the machine and configuration disagree. This behaviour is new rather than ported: legacy had no read-only hook inspection (`legacy@1d32845:bridge/hookconfig.py:143-156`), and its status verb only pinged the daemon (`legacy@1d32845:bridge/__main__.py:205-213`).
-**ADR 0007's `hook_plugin.py` and `plugin.py` are deleted** rather than left as dead code. `channel.py` is not touched here; the Relay route that replaced it is [#74](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/74)'s and [#77](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/77)'s to settle.
+**ADR 0007's `hook_plugin.py` and `plugin.py` are deleted** rather than left as dead code. `channel.py` is not touched here; the Relay route that replaced it is [#74](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/74)'s and [#77](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/77)'s to settle.
**The Codex item is a login `LaunchAgent`, which is the shape ADR 0005 moved away from** and which `LoginItem.swift` refuses for the app itself, in those words. The two do not conflict. ADR 0005's reason is the microphone grant, which attaches to whoever owns the process, and the Codex daemon neither wants a microphone nor is ours to own — it is the user's `codex`, started early so the user's own TUIs join a daemon that is already there. The engine remains a direct child of the shell.
-**Coverage is one config directory** — the default one. ADR 0011 wrote "installs into each" for a product told about several; v1.0 was scoped to the main account on 2026-08-26 ([#67](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/67)), so the list has one entry and the code takes a directory rather than assuming one.
+**Coverage is one config directory** — the default one. ADR 0011 wrote "installs into each" for a product told about several; v1.0 was scoped to the main account on 2026-08-26 ([#67](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/67)), so the list has one entry and the code takes a directory rather than assuming one.
-**Known limitation, left standing knowingly (Simon, 2026-08-26): the round trip is exact only for a file already written the way this renders.** Install read-modify-writes the whole document and renders it at `indent=2` with a trailing newline, so a `settings.json` kept at some other indent, or with its keys in another order, is reformatted by the first install — once, after which it round-trips. And a config directory that had no `settings.json` at all gets one, which uninstall empties rather than removes. No setting is ever lost either way; what is not true is the stronger claim, which ADR 0011 and `CONTEXT.md` both make in the words "byte for byte". Making it true would mean keeping a copy of the user's file in this product's own directory and restoring from it, and that copy is the worse failure: a user who edits `settings.json` while the product is installed would have those edits eaten by the uninstall. The claim is left as written and the gap is recorded here and on [#86](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/86) rather than papered over.
+**Known limitation, left standing knowingly (Simon, 2026-08-26): the round trip is exact only for a file already written the way this renders.** Install read-modify-writes the whole document and renders it at `indent=2` with a trailing newline, so a `settings.json` kept at some other indent, or with its keys in another order, is reformatted by the first install — once, after which it round-trips. And a config directory that had no `settings.json` at all gets one, which uninstall empties rather than removes. No setting is ever lost either way; what is not true is the stronger claim, which ADR 0011 and `CONTEXT.md` both make in the words "byte for byte". Making it true would mean keeping a copy of the user's file in this product's own directory and restoring from it, and that copy is the worse failure: a user who edits `settings.json` while the product is installed would have those edits eaten by the uninstall. The claim is left as written and the gap is recorded here and on [#86](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/86) rather than papered over.
**There is no uninstall in the menu bar**, because the panel is view-only in v1.0. Uninstall is `bridge-install uninstall`, a console script that lands in the bundle's `engine/bin/` beside `bridgectl` by the same pipeline rule that needs no list of script names.
diff --git a/docs/adr/0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md b/docs/adr/0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md
index f3f00de..61418af 100644
--- a/docs/adr/0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md
+++ b/docs/adr/0013-the-answer-relay-rides-the-sessions-own-inbox-socket.md
@@ -1,8 +1,8 @@
# 13. The Claude Answer Relay rides the Session's own inbox socket, and an accepted write is not a receipt
-Date: 2026-08-26 · Status: Accepted · Amended by [ADR 0015](0015-a-held-question-opens-the-reply-window.md) · Source: [#71](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/71), built in [#77](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/77)
+Date: 2026-08-26 · Status: Accepted · Amended by [ADR 0015](0015-a-held-question-opens-the-reply-window.md) · Source: [#71](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/71), built in [#77](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/77)
-v1.0 is a bridge over the Sessions the user already started ([#67](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/67)), and the route ADR 0006 chose cannot reach one: a channel server is spawned from a plugin manifest at launch, so it exists only in Sessions this product started. Claude Code has since grown an inbox socket of its own, bound by default for every Session. The documented external ingress routes are channels — a research preview requiring a startup `--channels` selector, which means taking over the user's `claude` command — and `-p --input-format stream-json`, which is not an interactive Session at all. Neither reaches a Session started by hand.
+v1.0 is a bridge over the Sessions the user already started ([#67](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/67)), and the route ADR 0006 chose cannot reach one: a channel server is spawned from a plugin manifest at launch, so it exists only in Sessions this product started. Claude Code has since grown an inbox socket of its own, bound by default for every Session. The documented external ingress routes are channels — a research preview requiring a startup `--channels` selector, which means taking over the user's `claude` command — and `-p --input-format stream-json`, which is not an interactive Session at all. Neither reaches a Session started by hand.
## Decision
@@ -36,8 +36,8 @@ ADR 0006 is superseded. ADR 0007 was already superseded by ADR 0011, which is th
## Amendment 2026-09-05: decision 3 stands, and the Voice says what a relayed answer is not
-Source: [#234](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/234), from the
-[#198](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/198) full run
+Source: [#234](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/234), from the
+[#198](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/198) full run
`20260904T202319Z`.
Decision 3 was written from what upstream enforces on a *permission* dialog. The full runs put the
@@ -88,14 +88,14 @@ authority — it is what the first generation did, launching every Session throu
with a `--channels` selector (`legacy@1d32845:bridge/claude.py:472-476`, serving
`legacy@1d32845:claude-channel/channel.mjs`) — and it is **dropped, because** it requires wrapping
the user's `claude` command, which
-[#67](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/67) /
-[#68](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/68) and
+[#67](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/67) /
+[#68](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/68) and
[ADR 0020](0020-a-codex-session-is-a-daemon-thread-a-terminal-vouches-for.md) refuse: this product
bridges Sessions the user already started. An upstream ingress that a registered external process
could be treated as the user through would settle it, and 2.1.261 documents none.
The acceptance `live call` `detail` phase depended on the compliant reading, so it is re-specified
against this amendment in
-[#238](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/238): the extra Session asks
+[#238](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/238): the extra Session asks
through `AskUserQuestion`, the answer rides the held-hook route, and the phase grades the dictated
reply as `newest` without resting on a model's reading of the wrapper.
diff --git a/docs/adr/0014-question-answers-ride-the-approval-hook.md b/docs/adr/0014-question-answers-ride-the-approval-hook.md
index b9d4a55..309330a 100644
--- a/docs/adr/0014-question-answers-ride-the-approval-hook.md
+++ b/docs/adr/0014-question-answers-ride-the-approval-hook.md
@@ -1,6 +1,6 @@
# 14. A question answer rides the Approval Relay as a typed verdict
-Date: 2026-08-28 · Status: Superseded by [ADR 0015](0015-a-held-question-opens-the-reply-window.md) · Source: [#103](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/103)
+Date: 2026-08-28 · Status: Superseded by [ADR 0015](0015-a-held-question-opens-the-reply-window.md) · Source: [#103](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/103)
ADR 0013 closes the Session inbox to answers to a Session's own question: the inbox carries peer words, not the user's authority, and Claude Code refuses to treat them as the user's answer. `AskUserQuestion` raises the same `PermissionRequest` hook as a permission dialog, however, and its payload carries the whole question plus `prompt_id`. On Claude Code 2.1.246, returning a denial whose message is the answer places that message in the Session as the tool result and the Session continues on it. The hook is therefore the route that exists; what was missing was a seam value able to name an answer without pretending it was allow or deny.
diff --git a/docs/adr/0015-a-held-question-opens-the-reply-window.md b/docs/adr/0015-a-held-question-opens-the-reply-window.md
index b0af034..ee5ade6 100644
--- a/docs/adr/0015-a-held-question-opens-the-reply-window.md
+++ b/docs/adr/0015-a-held-question-opens-the-reply-window.md
@@ -1,6 +1,6 @@
# 15. A held question opens the Reply Window
-Date: 2026-08-28 · Status: Accepted · Source: [#128](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/128)
+Date: 2026-08-28 · Status: Accepted · Source: [#128](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/128)
The public capability is the **Answer Relay**: it carries the user's own words, including an answer to a Session's question. The transport is selected inside the Claude adapter. Ordinary words use the Session inbox; while the listener still holds the exact `AskUserQuestion` prompt, the same `answer_relay` verb writes a framed denial message back through that hook because Claude consumes the message as the tool result. The Approval Relay remains only a permission verdict of `allow`, `deny`, or `ask`; questions never enter its pipeline, Control Plane document, command grammar, or Control Panel.
@@ -16,7 +16,7 @@ Legacy classification is per behaviour, as ruled by the Advisor for #128. Answer
## Amendment 2026-09-01: the engine keeps no clock on a held hook
-Source: [#172](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/172), under map [#164](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/164).
+Source: [#172](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/172), under map [#164](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/164).
This ADR gave a parked question a hold ceiling: `CorePolicy.approval_budget_seconds`, passed
through `BridgeCore.tick` into `sweep_question_budget`, so the listener would pop an expired
@@ -43,7 +43,7 @@ silence is the on-screen dialog's — not as engine policy.
## Amendment 2026-09-10: the frame is worded for a reader who is told it failed
-Source: [#328](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/328).
+Source: [#328](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/328).
This ADR settled that an answer rides the held hook as a framed denial, because Claude
consumes a denial's message as the `AskUserQuestion` tool result. What it did not record is
diff --git a/docs/adr/0016-progress-publishes-a-roster-summary-or-exact-detail.md b/docs/adr/0016-progress-publishes-a-roster-summary-or-exact-detail.md
index e6fb491..7ea2629 100644
--- a/docs/adr/0016-progress-publishes-a-roster-summary-or-exact-detail.md
+++ b/docs/adr/0016-progress-publishes-a-roster-summary-or-exact-detail.md
@@ -1,6 +1,6 @@
# 16. Progress publishes one observation as a roster summary or exact detail
-Date: 2026-08-30 · Status: Accepted · Source: [#76](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/76), whose Advisor Q2 bound is superseded by Simon on 2026-08-30
+Date: 2026-08-30 · Status: Accepted · Source: [#76](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/76), whose Advisor Q2 bound is superseded by Simon on 2026-08-30
Issue #76 made one `Progress` value serve three jobs: the normalized fact read from
Claude or Codex, the compact value stored on every Session roster row, and the exact
@@ -162,7 +162,7 @@ stands; what changes is that omission is represented honestly.
## Amendment 2026-09-01: a third publication, the History page
-Source: [#171](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/171), under map [#164](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/164).
+Source: [#171](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/171), under map [#164](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/164).
The 0901 flow asks for history the user can page through by voice — "the last five,
then the five before those". The exact `progress` publication above cannot answer it: it
@@ -211,7 +211,7 @@ whole-entry rule and the exact one-Session read remain adapted as above.
## Amendment 2026-09-06: one surface may cut what it lays out
Source: [ADR 0021](0021-the-companion-channel-is-a-surface-the-user-replies-on.md) §5, from
-[#251](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/251).
+[#251](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/251).
The whole-or-omitted rule above governs what Core reads, carries and hands across a seam, and it
is unchanged there: the structured brief a Stop Notice is laid out from still holds the newest
@@ -223,9 +223,9 @@ is marked where it happens, never silent, and this is the only place a text is c
## Amendment 2026-09-09: a publication ceiling is chosen by the wire the reader is on
-Source: [#302](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/302), the decision in
-[#298](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/298), under the map
-[#283](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/283). Evidence:
+Source: [#302](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/302), the decision in
+[#298](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/298), under the map
+[#283](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/283). Evidence:
`docs/research/2026-09-07-realtime-output-budget.md`.
One observation, a publication per **wire**. The rule above bounds a publication by the line it
diff --git a/docs/adr/0017-a-missed-call-is-briefed-from-a-fresh-reading.md b/docs/adr/0017-a-missed-call-is-briefed-from-a-fresh-reading.md
index fc4b8f3..40e9bfd 100644
--- a/docs/adr/0017-a-missed-call-is-briefed-from-a-fresh-reading.md
+++ b/docs/adr/0017-a-missed-call-is-briefed-from-a-fresh-reading.md
@@ -1,6 +1,6 @@
# 17. A missed call is briefed from a fresh reading, never from replayed events
-Date: 2026-09-01 · Status: Accepted · Source: [#165](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/165) Q5/Q10, [#167](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/167)
+Date: 2026-09-01 · Status: Accepted · Source: [#165](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/165) Q5/Q10, [#167](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/167)
Two calls a minute is worse than one call late. The reference implementation had no
cool-down at all and recorded the incident that shape produces: a Stop Notice arriving
diff --git a/docs/adr/0018-one-dial-two-audiences.md b/docs/adr/0018-one-dial-two-audiences.md
index 322a521..ca836bb 100644
--- a/docs/adr/0018-one-dial-two-audiences.md
+++ b/docs/adr/0018-one-dial-two-audiences.md
@@ -1,6 +1,6 @@
# 18. One dial, two audiences: the Voice hears prose, the Call Agent hears its rules
-Date: 2026-09-01 · Status: Accepted · Source: [#177](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/177), evidence [#175](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/175) and [#179](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/179)
+Date: 2026-09-01 · Status: Accepted · Source: [#177](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/177), evidence [#175](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/175) and [#179](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/179)
A codex v3 realtime call is two models: the **Voice** the user hears, and the **Call
Agent** behind it — the only half with tools. `thread/realtime/start` addresses them
@@ -19,7 +19,7 @@ all.
not the slot.** `CallAdapter.ensure_call(dial: Dial)` replaces `ensure_call(instructions:
str)`. `Dial` is a frozen carrier of three fields: `voice` — prose for the Voice;
`agent` — prose for the Call Agent; `hand_over` — the Briefing's dial-time items (ADR
-0017, [#166](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/166)). The
+0017, [#166](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/166)). The
realtime adapter alone maps them to `prompt`, `realtimeStartInstructions` and
`initialItems` (`role: "developer"`); Bridge Core never learns a codex field name. A
`Dial` refuses an empty `voice` or `agent` text at construction — sending no `prompt`
@@ -34,10 +34,10 @@ is capped at 8,192 bytes, well inside the 8,192 estimated tokens codex allows th
(see the amendment below); the voice set keeps this engine's own 8,000-byte cap, which
codex does not impose (the `prompt` slot is unbudgeted) and which stands as the measure of
"terse". Which rule belongs to which audience, and what each says, is
-[#173](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/173)'s work,
+[#173](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/173)'s work,
rule by rule against the 0901 flow — nothing is carried over unread.
-*Amended 2026-09-03 after [#215](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/215).*
+*Amended 2026-09-03 after [#215](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/215).*
The byte budgets above were first justified as "a byte is the floor of a token". That
over-states the margin, and it named the wrong counter. Nothing counts tokens on this
path: codex's own app-server rejects an over-budget request before the backend sees it,
@@ -56,7 +56,7 @@ claim the wire as their reason.
paragraphs — no headings, bullets, code or key-value text (Simon, 2026-09-01: 控制 voice
一定要用自然语言而不是代码语言). The agent set may keep its structure.
-*Amended 2026-09-08 after [#289](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/289).*
+*Amended 2026-09-08 after [#289](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/289).*
The paragraph above misread the 2026-09-01 instruction. It was about how the Voice is
**spoken to and fed** — the user talks to it and the Briefing hands it facts in natural
language, never in code-like text — not about the format of the prompt that shapes it.
@@ -79,7 +79,7 @@ caller varies them: `delegationAckFiller` off (the backend's own "好,等我
is the wordiness Round 1 Q9 removed), `codexResponsesAsItems` on with a prefix, and
`includeStartupContext` **off**.
-*Amended 2026-09-01 after [#179](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/179).*
+*Amended 2026-09-01 after [#179](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/179).*
`includeStartupContext` was left at its default pending that probe; the default is **on**,
and what it includes is up to 5,300 tokens of the current thread's history, a scan of the
user's forty most recent local codex threads and a local workspace map, appended to the
@@ -95,7 +95,7 @@ invisibly and were spoken. On this evidence the switch does not buy observabilit
adapter must not be built expecting it to. What the Call Agent says is reachable, if at all,
through the `[BACKEND] `-prefixed `user` messages codex's own Voice prompt describes
(`prompts/templates/realtime/backend_prompt.md:40-41`) — a carrier no ticket has examined.
-Sizing that is [#173](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/173)'s
+Sizing that is [#173](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/173)'s
to inherit.
**No `HandoffRequested` event.** The Voice's hand-off to the Call Agent
@@ -104,7 +104,7 @@ stays as it is. A voice hang-up is the Call Agent running `bridgectl live` — t
executes off a request it can see, and the model's own claim to have hung up (observed
live, #175 run 3) is trusted by nothing.
-*Confirmed 2026-09-01 by [#179](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/179).*
+*Confirmed 2026-09-01 by [#179](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/179).*
Told in `realtimeStartInstructions` that `bridgectl live` ends the call, the Call Agent ran
it on **3 of 3** spoken requests, four to five seconds after each hand-off. The rule reaches
the acting half and is obeyed; the Voice never needs to know the verb exists. So the event
diff --git a/docs/adr/0019-one-claude-approval-route-per-machine.md b/docs/adr/0019-one-claude-approval-route-per-machine.md
index 81ad076..1ad6096 100644
--- a/docs/adr/0019-one-claude-approval-route-per-machine.md
+++ b/docs/adr/0019-one-claude-approval-route-per-machine.md
@@ -1,6 +1,6 @@
# 19. One Claude approval route per user per machine, and the first live engine holds it
-Date: 2026-09-02 · Status: Accepted · Source: [#202](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/202)
+Date: 2026-09-02 · Status: Accepted · Source: [#202](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/202)
The `PermissionRequest` hook is a process Claude Code starts, once per displayed dialog, with no configuration and no parent in this engine (ADR 0011). The one thing it can read is the address the engine published at a fixed path, `~/Library/Application Support/GPT-VoiceCoding/engine/address.json` (`locations.py:56`). That path is derived from the user's home directory, so it is one file per user per machine — while an engine is not.
@@ -18,7 +18,7 @@ Publishing dials whatever address is already there. A socket nobody answers is d
**Deciding and acting are one step.** Probing the incumbent and writing are two syscalls, and so are reading the owner and unlinking. Engines interleaved between the halves of either pair put the file back where this issue found it: publishers that all see no holder all write, and an engine withdrawing after a newer one took over would unlink an address that is no longer its own. Both pairs are taken under one `flock` beside the address. It is an advisory lock rather than an exclusive create, because a lock has to survive the process holding it being killed — the kernel drops an `flock` when the descriptor closes, while an `O_EXCL` marker left by a killed engine is a machine that can never publish again.
-**An engine that published no address says so where the engine reports what it loaded** (ADR 0003), naming the reason — the holder, when the reason is a peer — in addition to the log line, and the report is honest about how far the failure reaches. Both Claude routes read this one address — the `PermissionRequest` hook (`approval_hook.py`) and the `SessionStart` registration hook (`registration.py`) — so an engine that published no address loses more than approvals: no Session can register with it either, and its roster stays empty for that reason. `verify` therefore has two branches. An unpublished address with an empty roster is **FAIL**, whatever the cause — refused by a peer, a socket that would not bind, an address file that could not be written ([#204](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/204)) — naming the reason and saying why the roster is empty; the empty-roster branch would otherwise report PASS — "no Claude Session is registered, so there is no inbox to reach" — which is precisely the guard that says nothing while the route is dead that ADR 0003 exists to prevent. That substitution is the **only** one: every other answer the report can give is a reason of its own — a missing hook block, a registry outside the config directory, an inbox that stopped answering — and each keeps its own outcome and its own detail, with the refusal prefixed rather than put in its place. A report that hid a missing hook block behind the refusal would be ADR 0003's original failure wearing this issue's clothes.
+**An engine that published no address says so where the engine reports what it loaded** (ADR 0003), naming the reason — the holder, when the reason is a peer — in addition to the log line, and the report is honest about how far the failure reaches. Both Claude routes read this one address — the `PermissionRequest` hook (`approval_hook.py`) and the `SessionStart` registration hook (`registration.py`) — so an engine that published no address loses more than approvals: no Session can register with it either, and its roster stays empty for that reason. `verify` therefore has two branches. An unpublished address with an empty roster is **FAIL**, whatever the cause — refused by a peer, a socket that would not bind, an address file that could not be written ([#204](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/204)) — naming the reason and saying why the roster is empty; the empty-roster branch would otherwise report PASS — "no Claude Session is registered, so there is no inbox to reach" — which is precisely the guard that says nothing while the route is dead that ADR 0003 exists to prevent. That substitution is the **only** one: every other answer the report can give is a reason of its own — a missing hook block, a registry outside the config directory, an inbox that stopped answering — and each keeps its own outcome and its own detail, with the refusal prefixed rather than put in its place. A report that hid a missing hook block behind the refusal would be ADR 0003's original failure wearing this issue's clothes.
There is no Telegram or menu-bar surface, because only development and acceptance ever run two engines.
diff --git a/docs/adr/0020-a-codex-session-is-a-daemon-thread-a-terminal-vouches-for.md b/docs/adr/0020-a-codex-session-is-a-daemon-thread-a-terminal-vouches-for.md
index 119a2ba..278da43 100644
--- a/docs/adr/0020-a-codex-session-is-a-daemon-thread-a-terminal-vouches-for.md
+++ b/docs/adr/0020-a-codex-session-is-a-daemon-thread-a-terminal-vouches-for.md
@@ -1,12 +1,12 @@
# 20. A Codex Session is a daemon-held user root that a live terminal vouches for
-Date: 2026-09-02 · Status: Accepted · Source: [#201](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/201)
+Date: 2026-09-02 · Status: Accepted · Source: [#201](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/201)
-> **Vocabulary amendment, 2026-09-07 ([#272](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/272), [ADR 0022](0022-the-shared-app-server-is-the-users-own-codex.md)).** Where this document says **daemon** it means **the shared Codex app-server** — the one this product's login job starts and the user's own TUIs join. Nothing decided below changes: a Codex Session is still a server-held user root that a live terminal vouches for, identity still comes from that server and liveness still from the process table, and #144's identity rule is still untouched. What changed is only *who starts the server* and *how this side finds it* — the user's own `codex`, started as `app-server --listen` on a derived control socket, in place of a managed standalone binary started with `daemon start`. This is flagged here rather than left as two words for one thing; the sentences are otherwise as they were ruled.
+> **Vocabulary amendment, 2026-09-07 ([#272](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/272), [ADR 0022](0022-the-shared-app-server-is-the-users-own-codex.md)).** Where this document says **daemon** it means **the shared Codex app-server** — the one this product's login job starts and the user's own TUIs join. Nothing decided below changes: a Codex Session is still a server-held user root that a live terminal vouches for, identity still comes from that server and liveness still from the process table, and #144's identity rule is still untouched. What changed is only *who starts the server* and *how this side finds it* — the user's own `codex`, started as `app-server --listen` on a derived control socket, in place of a managed standalone binary started with `daemon start`. This is flagged here rather than left as two words for one thing; the sentences are otherwise as they were ruled.
-ADR 0016's roster has two Codex sources, and the rule that merges them was written five times — [#112](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/112), [#113](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/113), [#123](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/123), [#144](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/144) and now this — while living inline across five branches of one I/O function, unnamed and with no test surface of its own.
+ADR 0016's roster has two Codex sources, and the rule that merges them was written five times — [#112](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/112), [#113](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/113), [#123](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/123), [#144](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/144) and now this — while living inline across five branches of one I/O function, unnamed and with no test surface of its own.
-#144 settled it as: **a loaded root is a roster row only when a live interactive process carries the same exact thread id as a native daemon thread or rollout.** That killed #123's ghost rows and #144's own duplicate rows, and it did so by requiring a shared key that only `codex resume ` puts in an argv. No other invocation does — not a bare `codex`, not `codex ""`, not `--last`, not the picker. So no hand-started Codex Session could ever reach the roster, and the acceptance harness's codex lane failed at its `roster` step in four consecutive real-machine runs (`20260902T002414Z`, `20260902T005111Z`, `20260902T005521Z`, `20260902T013516Z`), skipping every step behind it. The Codex lane was inert for the product's core promise ([#68](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/68)): a bridge over Sessions the *user* starts.
+#144 settled it as: **a loaded root is a roster row only when a live interactive process carries the same exact thread id as a native daemon thread or rollout.** That killed #123's ghost rows and #144's own duplicate rows, and it did so by requiring a shared key that only `codex resume ` puts in an argv. No other invocation does — not a bare `codex`, not `codex ""`, not `--last`, not the picker. So no hand-started Codex Session could ever reach the roster, and the acceptance harness's codex lane failed at its `roster` step in four consecutive real-machine runs (`20260902T002414Z`, `20260902T005111Z`, `20260902T005521Z`, `20260902T013516Z`), skipping every step behind it. The Codex lane was inert for the product's core promise ([#68](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/68)): a bridge over Sessions the *user* starts.
## The two measurements that force the shape
@@ -49,9 +49,9 @@ Identity can therefore only come from the daemon, and liveness can only come fro
**The under-reporting is stated, not silent.** When a live terminal vouches for nothing while its workspace holds daemon user roots, the lane says so through `LaneDiscovery.degraded`, beside whatever else that reading has to say. This is the condition on which the gap above was accepted.
-**And a terminal the daemon holds nothing for is stated too** ([#233](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/233)). The sentence above speaks only where the daemon holds user roots in the terminal's own workspace, so a TUI running its own core — the shape a `-c` override gives it — matched neither that sentence nor the drop log, which only ever speaks about threads the daemon holds. It produced no row and no word: run `20260904T202319Z` had one live for two minutes and the codex engine log has no line about it. Such a terminal is now named in the engine log once per pid — its pid, its workspace, and that no thread the daemon holds there is a row on this roster — on the same once-per-thing discipline the drops follow, and the pid is forgotten when the process is gone, because the machine reuses pids.
+**And a terminal the daemon holds nothing for is stated too** ([#233](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/233)). The sentence above speaks only where the daemon holds user roots in the terminal's own workspace, so a TUI running its own core — the shape a `-c` override gives it — matched neither that sentence nor the drop log, which only ever speaks about threads the daemon holds. It produced no row and no word: run `20260904T202319Z` had one live for two minutes and the codex engine log has no line about it. Such a terminal is now named in the engine log once per pid — its pid, its workspace, and that no thread the daemon holds there is a row on this roster — on the same once-per-thing discipline the drops follow, and the pid is forgotten when the process is gone, because the machine reuses pids.
-**The sentence is about this roster, not about what the daemon holds**, which is the only version of it the rule can prove ([#96](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/96): never claim anything about the daemon this build did not observe). "The daemon holds no user root there" would be a claim `compose` is in no position to make — a thread whose `thread/read` failed is one the daemon holds and the pass never saw, a `notLoaded` root was let go between the two readings, and a thread naming no source this build recognises is not thereby shown to be an errand; each leaves a workspace looking emptier than it is. What the rule observed in full is its own rows, and each thread that is not one leaves its own reason in the drops the log already prints beside this. The line is withheld entirely on a reading with no daemon in it: it would be true even then, but every live terminal on the machine would earn one and each is remembered per pid, latching a page of lines whose real subject is the one fact `degraded` is already carrying.
+**The sentence is about this roster, not about what the daemon holds**, which is the only version of it the rule can prove ([#96](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/96): never claim anything about the daemon this build did not observe). "The daemon holds no user root there" would be a claim `compose` is in no position to make — a thread whose `thread/read` failed is one the daemon holds and the pass never saw, a `notLoaded` root was let go between the two readings, and a thread naming no source this build recognises is not thereby shown to be an errand; each leaves a workspace looking emptier than it is. What the rule observed in full is its own rows, and each thread that is not one leaves its own reason in the drops the log already prints beside this. The line is withheld entirely on a reading with no daemon in it: it would be true even then, but every live terminal on the machine would earn one and each is remembered per pid, latching a page of lines whose real subject is the one fact `degraded` is already carrying.
The two sentences together now account for every live terminal that composed no row, and neither is a way in: this stays a note and never becomes a row.
@@ -61,11 +61,11 @@ The two sentences together now account for every live terminal that composed no
**Legacy citation** (ADR 0010). `legacy@1d32845:bridge/daemon.py:1192-1257` — **dropped, because** generation 1 only ever knew Sessions it had launched and wrapped itself, from its own launch records, so it never had to recognise one a person started. v1.0's promise (#68) is the opposite, and there is no legacy behaviour to port for it. The one habit that carries over is `pgrep` on an exact executable name (`legacy@1d32845:bridge/host.py:795`), which the process reader already does.
-**What this decision refuses.** Wrapping, launching, configuring or otherwise instrumenting the user's `codex` so that it would carry an identifier is out of scope permanently: the product does not own the user's Sessions (#68, [#83](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/83), ADR 0012), and that route is exactly what generation 1 did and what was dropped.
+**What this decision refuses.** Wrapping, launching, configuring or otherwise instrumenting the user's `codex` so that it would carry an identifier is out of scope permanently: the product does not own the user's Sessions (#68, [#83](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/83), ADR 0012), and that route is exactly what generation 1 did and what was dropped.
-Both the rule and its one-second allowance are adjustable where they conflict with measured behaviour ([#164](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/164)) — but only against a measurement, which is the standard the four runs above and the two probes here were held to.
+Both the rule and its one-second allowance are adjustable where they conflict with measured behaviour ([#164](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/164)) — but only against a measurement, which is the standard the four runs above and the two probes here were held to.
-## Amendment 2026-09-09: three tiers of run, told apart by the controlling terminal (map [#310](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/310), [#315](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/315))
+## Amendment 2026-09-09: three tiers of run, told apart by the controlling terminal (map [#310](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/310), [#315](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/315))
The sentence above — liveness and place come from the process table, identity from the agent's own records — now sorts every recognised run of either agent into one of three tiers, in Bridge Core and for both lanes alike:
diff --git a/docs/adr/0021-the-companion-channel-is-a-surface-the-user-replies-on.md b/docs/adr/0021-the-companion-channel-is-a-surface-the-user-replies-on.md
index 54e065f..be11753 100644
--- a/docs/adr/0021-the-companion-channel-is-a-surface-the-user-replies-on.md
+++ b/docs/adr/0021-the-companion-channel-is-a-surface-the-user-replies-on.md
@@ -1,6 +1,6 @@
# 21. The Companion Channel is a surface the user replies on: an Anchor names the target, a numeral picks the option
-Date: 2026-09-06 · Status: Accepted · Source: map [#246](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/246), requirements page Notion `0905_telegram` (Simon, 2026-09-05), decided on [#247](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/247)–[#259](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/259)
+Date: 2026-09-06 · Status: Accepted · Source: map [#246](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/246), requirements page Notion `0905_telegram` (Simon, 2026-09-05), decided on [#247](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/247)–[#259](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/259)
The Companion Channel today is a notification outlet with a typed command line behind it: every Stop reaches Telegram as `briefing.text`'s labelled lines, and a reply has to be `@: words`, `/command` or `>prompt`, or bare text that the router (`core/router.py`) accepts only when exactly one Session is live. Away from the Mac that is not a surface a person lives in — the notice reads like a log line and answering it means typing a Session Name on a phone. The requirements page asks for four things: receive what a Session stopped on with the agent's own words, reply by replying to a message, no log feel, no tmux. Two options were on the table: keep Telegram a peer of the Live Call on Bridge Core, or route everything through a model (a Delegate) that reads the chat and decides. This ADR takes the first and records the whole shape the map settled, because the shape is one mechanism repeated — a message the user can reply to, carrying choices a numeral picks — and the implementation issues are cut from it.
@@ -27,8 +27,8 @@ Every outbound message that names one target is an **Anchor** (`CONTEXT.md`). Br
- **A numeral picks that Anchor's Nth option label.** Before relaying, Core asks the Agent seam whether that Session's decision is *still answerable from here* — the fact ADR 0015 already exposes. If it is, the label goes as an Answer Relay, which the Claude adapter carries through the held `AskUserQuestion` hook (ADR 0015 — the route that carries the user's authority; **not** the Approval Relay, whose verdicts stay `allow` / `deny` / `ask`). If it is not, nothing is sent and the reply says the question was already answered on screen and asks for words. A numeral on a permission notice resolves to a verdict on the row's approval id — §6 — and a numeral on any other anchor kind is refused the same way. Free text is never subject to this check.
- **Any other text is the user's own words, relayed verbatim** through the ordinary Answer Relay — including a `/x` line naming a skill, and including text inside an Assistant Conversation.
- **`>` is a top-level form only**: a one-shot Delegated Turn with no Session context, unchanged. A `>` inside a reply to a Stop Notice is *not* a question about that Session — a reply to a notice is always the user's words to that Session. The Session-level assistant this would have been is refused; §7 is what replaces it.
-- **`/` at top level is the control plane; `/` inside a reply belongs to the Session.** The relay wrapper names the act (`run the skill /x with arguments …`), because neither transport expands a leading `/`: measured on [#248](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/248) (Claude Code 2.1.261, `codex-cli` 0.153.0), the Codex rollout stored `/review` literally and Claude's inbox delivered our text verbatim inside Claude Code's own peer preamble, which already tells the receiver to act on it. Whether the skill starts is the Session's own judgement on both lanes, and Bridge Core reports what it did rather than promising it ran. Skills marked `disable-model-invocation` are unreachable by any relay route (measured: `Skill … cannot be used with Skill tool due to disable-model-invocation`); the Session says so and the user types it at the terminal. There is no keystroke injection.
-- **No pre-check of a skill name** ([#256](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/256)). The Session resolves the name itself (measured: Claude lists `~/.claude/skills` before calling the Skill tool, Codex reads the skill's `SKILL.md`), so a wrong name costs one turn and is never wrongly refused. A pre-check would be instant and wrong whenever its list was stale or blind to the invocation ceiling, and Bridge Core reads no skill roster from either lane — building one would make the hub know a per-lane fact it has never needed.
+- **`/` at top level is the control plane; `/` inside a reply belongs to the Session.** The relay wrapper names the act (`run the skill /x with arguments …`), because neither transport expands a leading `/`: measured on [#248](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/248) (Claude Code 2.1.261, `codex-cli` 0.153.0), the Codex rollout stored `/review` literally and Claude's inbox delivered our text verbatim inside Claude Code's own peer preamble, which already tells the receiver to act on it. Whether the skill starts is the Session's own judgement on both lanes, and Bridge Core reports what it did rather than promising it ran. Skills marked `disable-model-invocation` are unreachable by any relay route (measured: `Skill … cannot be used with Skill tool due to disable-model-invocation`); the Session says so and the user types it at the terminal. There is no keystroke injection.
+- **No pre-check of a skill name** ([#256](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/256)). The Session resolves the name itself (measured: Claude lists `~/.claude/skills` before calling the Skill tool, Codex reads the skill's `SKILL.md`), so a wrong name costs one turn and is never wrongly refused. A pre-check would be instant and wrong whenever its list was stale or blind to the invocation ceiling, and Bridge Core reads no skill roster from either lane — building one would make the hub know a per-lane fact it has never needed.
### 4. The seam grows optional fields, and a button never crosses it
@@ -37,7 +37,7 @@ Every outbound message that names one target is an **Anchor** (`CONTEXT.md`). Br
- **`InboundText.in_reply_to: str = ""`** — the adapter's opaque id of the message the user replied to, empty when they replied to nothing. It is of the same nature as `origin`: a fact the adapter can see, not an opinion about meaning, so the seam's "no field an adapter could use to volunteer a classification" contract holds. Core matches it by string equality against the Anchor Table and never parses it. A second event type was rejected: it would duplicate `text` and `origin` and add a Core branch doing the same work.
- **`send` returns `ChannelReceipt`**, a `DeliveryReceipt` subclass with `message_ids: tuple[str, ...] = ()` — the provider ids of every part that landed, in order. Chosen over growing the shared `DeliveryReceipt` (three seams use it; Telegram ids do not belong in the Agent or Call vocabulary) and over a tuple returned beside the receipt. An `UNKNOWN` receipt from a split send that failed after a part landed still carries the landed ids, and those parts are Anchors. Core's existing `send` sites keep reading `is_delivered`.
- **`send` takes `origin: str = ""`** — Core echoes the inbound event's `origin` when it replies to that event and leaves it empty for unbidden pushes. This is what the `origin` docstring already promised; Core starts honouring it.
-- **A button press is a numeral reply, folded inside the Telegram adapter.** The adapter subscribes to `callback_query` (added to `ALLOWED_UPDATES`; a set list persists on the bot — [#247](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/247), which found that our own `["message"]` list is what had been excluding it). A press becomes `InboundText(text=, origin=, in_reply_to=)` — identical to typing that numeral in a reply. Core's reply to that `origin` is shown as a **toast** through `answerCallbackQuery`, never as a chat message: a toast holds ≤ 200 characters and a callback can be answered once, so a longer reply or a second one falls back to a chat message. A toast is no chat message and so registers no Anchor. If `answerCallbackQuery` itself fails, the failure is logged and the reply is sent as a chat message instead. Core never learns the word "button".
+- **A button press is a numeral reply, folded inside the Telegram adapter.** The adapter subscribes to `callback_query` (added to `ALLOWED_UPDATES`; a set list persists on the bot — [#247](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/247), which found that our own `["message"]` list is what had been excluding it). A press becomes `InboundText(text=, origin=, in_reply_to=)` — identical to typing that numeral in a reply. Core's reply to that `origin` is shown as a **toast** through `answerCallbackQuery`, never as a chat message: a toast holds ≤ 200 characters and a callback can be answered once, so a longer reply or a second one falls back to a chat message. A toast is no chat message and so registers no Anchor. If `answerCallbackQuery` itself fails, the failure is logged and the reply is sent as a chat message instead. Core never learns the word "button".
- **Text only, this iteration.** A message from the configured chat that carries no `text` (voice note, photo, file — captions are not mined) is raised as `InboundText(text="")` and the router's existing cannot-classify path answers with one fixed hint, worded once in Core. Strangers are dropped silently before this, as today.
`in_reply_to`, `message_ids` and `origin` are opaque strings; a channel with no reply concept emits empty and gets the newest-Anchor rule. `callback_query` and `answerCallbackQuery`, the only genuinely Telegram things, never leave the adapter.
@@ -49,7 +49,7 @@ The Voice already has the precedent: `briefing.spoken()` fills the Call seam's `
- **The notice shape.** Line one: a state light and the state word (`🟡 waiting for your decision`). Line two: ` · · `. A blank line; the question in bold; the numbered options; a blank line; the original folded. No other symbols. A notice with no question (a turn that ended: `finished`) is the same layout with the question slot empty and the whole answer in the fold — that is the requirements page's "more original content than the delegate's summary", and it needs no special layout.
- **The agent's words are never escaped**, by construction: the adapter sends plain text plus an `entities` array (bold for the question, `expandable_blockquote` for the original — Bot API 7.4 allows it to be given explicitly, offsets in UTF-16 code units which the adapter already counts) instead of MarkdownV2, whose eighteen-character escape list applies inside blockquote bodies too and whose every escaping bug shows up as a refused `sendMessage`. A stray `*`, `_` or backtick in a Claude message cannot break a notice. Code inside the original shows as plain text inside the fold — Telegram forbids `pre` and `code` inside a blockquote — with fences as literal characters. Accepted.
- **A notice is one message.** When headline plus original exceed the adapter's limit, the original is cut to what remains, preferring the last line break then the last space, and the fold ends with a fixed marker (Core's words). The headline is never cut. A `/full` command and a "show more" button that pages History were both refused as too much machinery: the full text is on the terminal. The cut lives in the adapter because the limit is Telegram's; the marker's words are Core's because the vocabulary is the user's. `split_message` stays for the messages that are not notices. **This is the one place a message text is cut.** ADR 0016's rule — text is carried whole or named as omitted, never sliced — still governs what Core reads and carries; the structured brief still holds the original whole. The cut is a layout act on one surface with a hard limit, marked where it happens, and this ADR amends ADR 0016 to that extent and no further (Simon, 2026-09-05, on #251).
-- **The tail: the ending of a prose question, ahead of the fold** (amended 2026-09-10, [#324](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/324)). A notice whose state is `DECISION` and whose question slot is empty is a question the Session asked in prose — `_decision` had no structured ask to word the slot with, and #320's question mark promoted the state anyway. Between the headline and the fold the adapter lays out the **tail** of the newest message: its last characters, opened with `[…]` and bold, as a structured question is bold. **The length is computed, never chosen** — the preview window less the headline just built. The window is a Telegram fact of the same kind as the message cap and lives beside it in the adapter's settings: Telegram Desktop's `notificationText()` cuts the raw text at 255 characters, Android's collapsed shade shows the raw message, iOS takes the push body verbatim, and all three drop the blockquote **entity** while keeping its text — so what hides the ask is *position*, not the fold, and the fix is layout. The cut is `_fitted`'s, mirrored: the first line break inside the window, else the first space, else the character boundary, and never through a surrogate pair. **No tail where it would say nothing new**: a message the preview already shows whole gets none, and a headline that fills the preview by itself gets none. The fold's budget pays for the tail — for its **window**, not its length, so the fold sits at the cap less the preview window however the cut fell — and gives way as it already does; the headline still never does. The tail draws no buttons and offers no numerals — a menu parsed out of prose would route the user's choice to the wrong answer, which costs more than showing nothing — and a reply in words reaches the Session through the notice's Anchor, as today. **The notice does not locate the question** (Simon, 2026-09-10): rules that tried to find the question sentence were measured on the corpus and left most outputs unreadable alone. `[…]` is the adapter's own symbol, beside the button's cut mark, on the same rule as the rest of this section — this module adds symbols and markup and no words; the fold's marker stays Core's sentence for the place a text was *ended*.
+- **The tail: the ending of a prose question, ahead of the fold** (amended 2026-09-10, [#324](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/324)). A notice whose state is `DECISION` and whose question slot is empty is a question the Session asked in prose — `_decision` had no structured ask to word the slot with, and #320's question mark promoted the state anyway. Between the headline and the fold the adapter lays out the **tail** of the newest message: its last characters, opened with `[…]` and bold, as a structured question is bold. **The length is computed, never chosen** — the preview window less the headline just built. The window is a Telegram fact of the same kind as the message cap and lives beside it in the adapter's settings: Telegram Desktop's `notificationText()` cuts the raw text at 255 characters, Android's collapsed shade shows the raw message, iOS takes the push body verbatim, and all three drop the blockquote **entity** while keeping its text — so what hides the ask is *position*, not the fold, and the fix is layout. The cut is `_fitted`'s, mirrored: the first line break inside the window, else the first space, else the character boundary, and never through a surrogate pair. **No tail where it would say nothing new**: a message the preview already shows whole gets none, and a headline that fills the preview by itself gets none. The fold's budget pays for the tail — for its **window**, not its length, so the fold sits at the cap less the preview window however the cut fell — and gives way as it already does; the headline still never does. The tail draws no buttons and offers no numerals — a menu parsed out of prose would route the user's choice to the wrong answer, which costs more than showing nothing — and a reply in words reaches the Session through the notice's Anchor, as today. **The notice does not locate the question** (Simon, 2026-09-10): rules that tried to find the question sentence were measured on the corpus and left most outputs unreadable alone. `[…]` is the adapter's own symbol, beside the button's cut mark, on the same rule as the rest of this section — this module adds symbols and markup and no words; the fold's marker stays Core's sentence for the place a text was *ended*.
- **The roster** is laid out in the same visual language — one line per Session, ` · · `, a counts line under it — from the same structured brief, and it is an Anchor (§6).
### 6. Options are labels; a surface that can draw buttons draws one per label
@@ -57,8 +57,8 @@ The Voice already has the precedent: `briefing.spoken()` fills the Call seam's `
`send` grows no `options` argument: the labels already ride in the structured brief, and the Telegram adapter draws one inline button per label, in order, with `callback_data` the 1-based position (adapter-private, within 64 bytes). One mechanism serves every screen:
- **Question notices**: one button per option label, the label verbatim on the notice line and truncated for width on the button. A press is the numeral of §3.
-- **Permission notices — buttons too** (Simon, [#255](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/255)): `allow` / `deny`, from Core's wording table, with the matching numbered lines `1. allow 2. deny` in the notice so a typed numeral works identically. Numeral 1 resolves to `allow` and 2 to `deny` on the pending approval id the row holds — an Approval Relay, the user's own verdict. `ask` gets no button: not answering hands the dialog back to the terminal, which is already the default. **This goes one step past the Control Panel**, which keeps permissions outside its question panel (ADR 0014's panel, still in place); the reason is that approving from away is this channel's core scenario. A Codex Session, which has no hook route, still gets the buttons, and a press gets the honest refusal `approve` gives today.
-- **The command menu** ([#253](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/253)) has four entries: `/assistant` (§7), `/sessions`, `/status`, `/config`. **The three new verbs join the shared control-plane command set** (`seams/control_plane.py`, one parser for `bridgectl` and every `/` — `docs/control-plane.md`): `sessions` answers with the roster brief carrying labels, `config` with the config screen, `assistant` opens an Assistant Conversation. `bridgectl` therefore gains the same three, answering in text and ignoring the labels like any surface that draws no buttons. A Telegram-private parser was refused as a second command interface; `setMyCommands` only advertises what the shared set already accepts. Every menu screen is an Anchor carrying option labels: `/sessions` → the roster, one label per live Session — the Roster Brief lists live Sessions only (`CONTEXT.md`), an ended one is announced by §9's line and appears nowhere here — resolving by position and never by label text; pressing a Session → a greeting Anchor for that Session with `brief` / `history` / `send message`; `send message` → the prompt "Say to :" (Core's words) with Telegram's ForceReply, an Anchor with no expiry of its own that a later Stop Notice supersedes by the newest-Anchor rule; `/config` → `switch` / `verify` / `live`; `switch` → one label per switch showing its current state, a press meaning "flip". `/status` is plain text and not an Anchor; `verify` and `live` run at once and answer with text. `relay` and `approve` are folded away on this surface — a reply *is* the relay and a numeral *is* the approval — and `switch`, `verify`, `live`, `brief`, `history` stay typeable. The withheld-from-the-Call-Agent precedent (`status`, `switch`, `verify`) does not apply here: the presser is the user, not a model.
+- **Permission notices — buttons too** (Simon, [#255](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/255)): `allow` / `deny`, from Core's wording table, with the matching numbered lines `1. allow 2. deny` in the notice so a typed numeral works identically. Numeral 1 resolves to `allow` and 2 to `deny` on the pending approval id the row holds — an Approval Relay, the user's own verdict. `ask` gets no button: not answering hands the dialog back to the terminal, which is already the default. **This goes one step past the Control Panel**, which keeps permissions outside its question panel (ADR 0014's panel, still in place); the reason is that approving from away is this channel's core scenario. A Codex Session, which has no hook route, still gets the buttons, and a press gets the honest refusal `approve` gives today.
+- **The command menu** ([#253](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/253)) has four entries: `/assistant` (§7), `/sessions`, `/status`, `/config`. **The three new verbs join the shared control-plane command set** (`seams/control_plane.py`, one parser for `bridgectl` and every `/` — `docs/control-plane.md`): `sessions` answers with the roster brief carrying labels, `config` with the config screen, `assistant` opens an Assistant Conversation. `bridgectl` therefore gains the same three, answering in text and ignoring the labels like any surface that draws no buttons. A Telegram-private parser was refused as a second command interface; `setMyCommands` only advertises what the shared set already accepts. Every menu screen is an Anchor carrying option labels: `/sessions` → the roster, one label per live Session — the Roster Brief lists live Sessions only (`CONTEXT.md`), an ended one is announced by §9's line and appears nowhere here — resolving by position and never by label text; pressing a Session → a greeting Anchor for that Session with `brief` / `history` / `send message`; `send message` → the prompt "Say to :" (Core's words) with Telegram's ForceReply, an Anchor with no expiry of its own that a later Stop Notice supersedes by the newest-Anchor rule; `/config` → `switch` / `verify` / `live`; `switch` → one label per switch showing its current state, a press meaning "flip". `/status` is plain text and not an Anchor; `verify` and `live` run at once and answer with text. `relay` and `approve` are folded away on this surface — a reply *is* the relay and a numeral *is* the approval — and `switch`, `verify`, `live`, `brief`, `history` stay typeable. The withheld-from-the-Call-Agent precedent (`status`, `switch`, `verify`) does not apply here: the presser is the user, not a model.
- **A menu answer that is a refusal is not an Anchor.** `history` is the one menu choice answered through the control-plane surface instead of read in Core, so it is the one that can come back a refusal — a lane that is down, a Session that cannot be read — and it used to register a row on one regardless. The surface now reports which it gave (`ControlAnswer`: the words, and whether they are a refusal), and only a page registers a row. The words were never the harm: with no row they fall through to the newest Anchor and reach the Session anyway (§2). What the row cost was a *slot* — each Session keeps its newest N rows, so a failed read could evict a real Stop Notice from the table — and it gave a numeral the wrong one of the two hints (§2's existing rule, applied to this path on #264's review, 2026-09-06).
- **Receipts, replies, `/status` and `>` answers** carry no labels and get no buttons.
- **A channel that cannot draw buttons ignores the labels**: the numbered lines are already in the text, so nothing is lost and nothing is appended.
@@ -68,12 +68,12 @@ The Voice already has the precedent: `briefing.spoken()` fills the Call seam's `
`/assistant` opens an **Assistant Conversation** (`CONTEXT.md`): Bridge Core starts a fresh Codex thread with the delegated instructions and the `[delegate] model`, sends a fixed opening line, and that message is an Anchor of a new kind whose row carries the thread id instead of a `SessionTarget`. A reply to *any* message of that conversation resumes the same thread (`thread/resume`, which the Codex Session adapter already uses); every answer is an Anchor of the same conversation. Two conversations coexist, and which one a message continues is decided only by what it replies to. Plain typing goes to the target of the newest Anchor, whichever kind it is — an assistant answer, or a Stop Notice — so §2's rule reads "the target of the newest Anchor", not "the Session of". Every assistant answer carries ForceReply as a courtesy — on its last part when it is split — so the client shows the reply bar and a placeholder; the routing is the Anchor rule and nothing else.
- **No transcript in Bridge Core.** Codex keeps the thread and its rollout; Core keeps the thread id on the row. Assistant Anchors are not a Session's, so they are capped per chat at the newest N conversations (configured, the same policy shape as the per-Session cap). A reply to an expired assistant Anchor, or to a thread that cannot be resumed, gets one fixed hint to start again from the menu.
-- **The rest is today's Delegated Turn unchanged**: delegated instructions plus the user's words, no pre-loaded Session facts (the turn reads `brief` / `history` itself, as the Call Agent does); the full delegated action set, `relay` and `approve` included, the turn reporting only what the engine confirmed; the answer as a threaded reply through `send(origin=…)`, a failure sending the error's own words the same way; the "typing" chat action while a turn runs, adapter-owned; one `[delegate] model` setting for every surface. **A Delegated Turn never holds the hub's dispatch loop** (amended on [#268](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/268), 2026-09-06): dispatch hands the turn to a task the hub tracks and returns at once, and the turn's answer re-enters Core as an event on Core's own queue, where the send, the Anchor and the reply bar are written by the serial loop alone (ADR 0001). Awaiting one inside dispatch made every other event — a Stop Notice, a numeral verdict, an Answer Relay, a `/status` — wait behind it for up to `delegated_turn_timeout_seconds`. **Per thread the turns are strictly sequential and in order**: replies typed to a conversation whose turn is running are queued first-in-first-out and each becomes its own turn as the one before it ends, with no cap, no coalescing and no refusal (Simon, 2026-09-06) — the user may send several messages and each is answered, in the order typed. Different conversations run at the same time, and the top-level `>`, which names no thread, queues behind other top-level `>` turns and nothing else — each still on its own fresh thread, so ten typed in a row never become ten simultaneous approval-free agents. Core therefore never issues a second `turn/start` on a busy thread, so what codex would do with one stays irrelevant. Engine shutdown cancels every turn in flight — which is what reaches the adapter's own `turn/interrupt` and `thread/unsubscribe` — and discards the queues; no answer is sent for a cancelled turn. A queued reply whose thread turns out to be gone gets the fixed start-again hint once, and the rest of that thread's queue is dropped with the conversation's rows.
+- **The rest is today's Delegated Turn unchanged**: delegated instructions plus the user's words, no pre-loaded Session facts (the turn reads `brief` / `history` itself, as the Call Agent does); the full delegated action set, `relay` and `approve` included, the turn reporting only what the engine confirmed; the answer as a threaded reply through `send(origin=…)`, a failure sending the error's own words the same way; the "typing" chat action while a turn runs, adapter-owned; one `[delegate] model` setting for every surface. **A Delegated Turn never holds the hub's dispatch loop** (amended on [#268](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/268), 2026-09-06): dispatch hands the turn to a task the hub tracks and returns at once, and the turn's answer re-enters Core as an event on Core's own queue, where the send, the Anchor and the reply bar are written by the serial loop alone (ADR 0001). Awaiting one inside dispatch made every other event — a Stop Notice, a numeral verdict, an Answer Relay, a `/status` — wait behind it for up to `delegated_turn_timeout_seconds`. **Per thread the turns are strictly sequential and in order**: replies typed to a conversation whose turn is running are queued first-in-first-out and each becomes its own turn as the one before it ends, with no cap, no coalescing and no refusal (Simon, 2026-09-06) — the user may send several messages and each is answered, in the order typed. Different conversations run at the same time, and the top-level `>`, which names no thread, queues behind other top-level `>` turns and nothing else — each still on its own fresh thread, so ten typed in a row never become ten simultaneous approval-free agents. Core therefore never issues a second `turn/start` on a busy thread, so what codex would do with one stays irrelevant. Engine shutdown cancels every turn in flight — which is what reaches the adapter's own `turn/interrupt` and `thread/unsubscribe` — and discards the queues; no answer is sent for a cancelled turn. A queued reply whose thread turns out to be gone gets the fixed start-again hint once, and the rest of that thread's queue is dropped with the conversation's rows.
- **A Delegated Turn's thread may now outlive the turn.** The realtime adapter states the opposite ("Fresh thread, one turn, then gone … nothing in the locked decisions asks for continuity between delegated turns"); this decision asks for it. The top-level `>` keeps its one-turn thread; an assistant thread lives as long as its conversation, and each turn still unsubscribes when it ends. The adapter already leaves a thread in place on retire — it interrupts and unsubscribes, never deletes — so the change is the stated rule, not the mechanism.
### 8. A notice whose decision has closed is edited in place, minimally, and stays the same Anchor
-When an Anchor's question or permission **stops being answerable from here** — answered from Telegram, answered at the terminal, permission handed back (`ask`, or the hook ending without a verdict), Session ended, Session left the roster — the notice is edited where it was sent: the state line becomes one fixed closed word (`handled`, from the wording table) and the buttons go; the question, the numbered lines and the fold stay as sent, so the chat still reads as a record. Nothing is added — no mark on the chosen option, no "answered from …" line; the user's own reply and its receipt already sit below. The word is fixed rather than the Session's current state, because a notice is the record of one stop, not a live roster row, and tracking later states would mean re-editing on every transition. Core already sees every cause (`ReplyWindowChanged` → CLOSED, an approval settling, `SessionEnded`) and does not distinguish them, because the edit does not show them. **Amended 2026-09-09 (map [#310](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/310)): a question the user answered at the terminal is no longer a cause — that notice stays as sent, unedited.** The remaining causes are unchanged. **Amended 2026-09-10 ([#324](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/324)): what is eligible for the edit is a notice *whose state asks the user* — `DECISION` or `PERMISSION` — and no longer a notice with a non-empty question line.** The two read alike for a structured ask and differ for a question asked in prose, which fills no question line and is a decision all the same: under the old test it was never edited, so a prose question answered from Telegram, or raised by a Session that has since died, stayed 🟡 for ever. A closed prose-question notice keeps its headline, tail and fold, with only the state line replaced. **The tail is laid out against the headline as it reads at that moment** (Simon, 2026-09-10): the closed word is shorter than the word it replaces, so the edit leaves more room and may spend it on more run-up. The edited notice therefore ends on the characters it was sent with and its tail is at least as long — more of the same ending, never less of it and never a different one, because both tails are suffixes of the same message taken from the same end. Nothing is carried on the notice to freeze it: the preview window is a fact about a message being *pushed*, and an edit notifies nobody, so the closed record has no preview to fit inside. The fold is untouched by any of it, because a notice that carries a tail gives the **whole preview window** to its headline-and-tail block and the fold takes what is left of the cap: reserve the window rather than the tail's length, and neither the state word nor where the tail's cut fell can move the fold. Spending only the tail's length looked equivalent and is not — a wider window can swallow a whole earlier line and grow the tail by far more than the state word gave up, taking the difference out of a fold already at the cap (measured on 507 line shapes; worst case 210 units).
+When an Anchor's question or permission **stops being answerable from here** — answered from Telegram, answered at the terminal, permission handed back (`ask`, or the hook ending without a verdict), Session ended, Session left the roster — the notice is edited where it was sent: the state line becomes one fixed closed word (`handled`, from the wording table) and the buttons go; the question, the numbered lines and the fold stay as sent, so the chat still reads as a record. Nothing is added — no mark on the chosen option, no "answered from …" line; the user's own reply and its receipt already sit below. The word is fixed rather than the Session's current state, because a notice is the record of one stop, not a live roster row, and tracking later states would mean re-editing on every transition. Core already sees every cause (`ReplyWindowChanged` → CLOSED, an approval settling, `SessionEnded`) and does not distinguish them, because the edit does not show them. **Amended 2026-09-09 (map [#310](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/310)): a question the user answered at the terminal is no longer a cause — that notice stays as sent, unedited.** The remaining causes are unchanged. **Amended 2026-09-10 ([#324](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/324)): what is eligible for the edit is a notice *whose state asks the user* — `DECISION` or `PERMISSION` — and no longer a notice with a non-empty question line.** The two read alike for a structured ask and differ for a question asked in prose, which fills no question line and is a decision all the same: under the old test it was never edited, so a prose question answered from Telegram, or raised by a Session that has since died, stayed 🟡 for ever. A closed prose-question notice keeps its headline, tail and fold, with only the state line replaced. **The tail is laid out against the headline as it reads at that moment** (Simon, 2026-09-10): the closed word is shorter than the word it replaces, so the edit leaves more room and may spend it on more run-up. The edited notice therefore ends on the characters it was sent with and its tail is at least as long — more of the same ending, never less of it and never a different one, because both tails are suffixes of the same message taken from the same end. Nothing is carried on the notice to freeze it: the preview window is a fact about a message being *pushed*, and an edit notifies nobody, so the closed record has no preview to fit inside. The fold is untouched by any of it, because a notice that carries a tail gives the **whole preview window** to its headline-and-tail block and the fold takes what is left of the cap: reserve the window rather than the tail's length, and neither the state word nor where the tail's cut fell can move the fold. Spending only the tail's length looked equivalent and is not — a wider window can swallow a whole earlier line and grow the tail by far more than the state word gave up, taking the difference out of a fold already at the cap (measured on 507 line shapes; worst case 210 units).
- **Seam: `send` grows `revises: tuple[str, ...] = ()`** — the provider ids from the earlier `ChannelReceipt`. Non-empty means "replace those messages' content with this brief"; the Telegram adapter calls `editMessageText` (same `entities`, inline markup only, no age limit for bot-sent messages), and the null channel ignores it. Core re-fills the same structured brief with the closed word and **empty option labels** — empty labels are what draws no buttons, so no "remove buttons" instruction exists anywhere. A new seam verb was refused as a shape change. The row is untouched: a reply in words still reaches its Session, a numeral is refused as in §3, and `sent_at` is not refreshed, so an old notice never becomes the newest Anchor again.
- **Gating.** A notice edit is not a push: it notifies nobody and only corrects a message already sent. It obeys the Duty Switch like every unbidden act toward the user and **not the Message Switch**, so a notice that is out still closes on screen after the user flips Message off — stale buttons would otherwise invite a press that earns only a refusal. The switches-screen edit below is different in kind: it is the *answer* to a control-plane action the user just took, and ADR 0002 puts it past every switch — Duty included, or the user who flips Duty off from the phone would never see the flip land.
@@ -84,17 +84,17 @@ When an Anchor's question or permission **stops being answerable from here** —
- **Every Stop pushes one notice**, whatever it stopped on — the code already does this (`core/bridge.py::_session_stopped`), `finished` included, and it stays. No per-state difference, no Focus Session bias, one format with only the state word and the presence of buttons differing.
- **Mid-turn progress is never pushed.** Nothing mid-turn reaches Core today (ADR 0016: status carries no chat body), the requirements page rules out "log feel", a turn in progress has no decision point to reply to, and pushing it would bury the notices that do. It is read on demand through `/sessions` → `history`.
-- **A Session's end is announced: one line, new** — `⚫ ended · · ` — the name is ` · ` as on every other surface, because two Sessions in one project would otherwise end as the same line (ruled on #266, 2026-09-06) — no question, no fold, no buttons, on `SessionEnded` from any cause (Core does not distinguish exit, crash or closed terminal). Away from the Mac, "it is gone" is the one fact the user most needs, or they reply to a stale notice and get only a refusal. It is **not an Anchor**: it names a Session that is gone and its rows drop on the same event, so a reply to it takes the unknown-Anchor path. The order on `SessionEnded` is fixed by the ids: edit the Session's open notices to `handled` (§8), push the ended line, then drop its rows. `ended` is a wording entry beside the state words, not a sixth `BriefState` — a brief describes a Session on the roster and this one has left it. Engine shutdown sends no ended lines (Sessions are re-recognised at the next start), and spawned children, which get no Stop Notice, get no ended line. **Amended 2026-09-09 (map [#310](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/310), [#315](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/315)): a Headless Run — a run with no controlling terminal, ADR 0020 as amended — gets no Stop Notice and no ended line either; only a Session is announced, at its stops and at its end.**
+- **A Session's end is announced: one line, new** — `⚫ ended · · ` — the name is ` · ` as on every other surface, because two Sessions in one project would otherwise end as the same line (ruled on #266, 2026-09-06) — no question, no fold, no buttons, on `SessionEnded` from any cause (Core does not distinguish exit, crash or closed terminal). Away from the Mac, "it is gone" is the one fact the user most needs, or they reply to a stale notice and get only a refusal. It is **not an Anchor**: it names a Session that is gone and its rows drop on the same event, so a reply to it takes the unknown-Anchor path. The order on `SessionEnded` is fixed by the ids: edit the Session's open notices to `handled` (§8), push the ended line, then drop its rows. `ended` is a wording entry beside the state words, not a sixth `BriefState` — a brief describes a Session on the roster and this one has left it. Engine shutdown sends no ended lines (Sessions are re-recognised at the next start), and spawned children, which get no Stop Notice, get no ended line. **Amended 2026-09-09 (map [#310](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/310), [#315](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/315)): a Headless Run — a run with no controlling terminal, ADR 0020 as amended — gets no Stop Notice and no ended line either; only a Session is announced, at its stops and at its end.**
- **No new Feature Switch.** Every push rides the Message Switch alone. A "questions only" switch, if `finished` notices ever prove noisy, is an independent later ticket.
### 10. Proof: two layers, three real facts, three named steps, states read by product-issued id
-Decided on [#259](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/259) against 131 acceptance runs on the machine of record: of 113 `live call` walks, 73 were red; every other step was red under a tenth of the time and `stop notice` never. The "often red, product fine" experience is that one step — a model in the loop, edges graded inside derived windows — and the text side, where this surface lives, is almost never red.
+Decided on [#259](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/259) against 131 acceptance runs on the machine of record: of 113 `live call` walks, 73 were red; every other step was red under a tenth of the time and `stop notice` never. The "often red, product fine" experience is that one step — a model in the loop, edges graded inside derived windows — and the text side, where this surface lives, is almost never red.
- **The unit layer proves everything deterministic**, in CI in seconds: the adapter's mappings (`callback_query` → numeral reply, `reply_to_message` → `in_reply_to`, `revises` → `editMessageText`, labels → inline keyboard, toast ≤ 200 else message) against `FakeTelegram` **grown** to speak `callback_query` updates, `answerCallbackQuery`, `editMessageText`, `reply_to_message` on inbound and an incrementing `message_id` on `sendMessage`, journaling each edit with the id it was given; and Core's Anchor Table, numeral resolution, newest-Anchor rule, `handled` trigger and ended line against the existing seam fakes.
-- **The real-environment run proves three facts only**: words replied to a notice reach *that* Session and it acts; a button press yields a toast and a delivered `relay`; the answered notice is edited to `handled` with its keyboard gone. Three new steps carry them, named now because step names are a contract build tickets cite: **`reply`** (words to a notice, proved by a file effect; the numeral refusal after a terminal answer is a wording and a non-event, so it is unit-proved and never walked), **`button`** (press, toast, edit — on a permission notice as well as a question notice), **`menu`** (`/sessions` → a Session → `send message` → words delivered). `companion inbound` keeps its name and becomes a reply-anchored message; the `@:` fallback is proved at the unit layer only. **Amended 2026-09-10 (map [#304](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/304), Simon): the real-environment run no longer proves these three facts and the `reply`, `button` and `menu` steps are not built — the acceptance harness proves Telegram connected only, one message out (`stop notice`) and one in (`companion inbound`, still reply-anchored), and the three behaviours are proved at the unit layer alone. [#267](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/267) closed unbuilt.**
-- **Never on the `live call` walk.** Every Telegram step runs with Voice off and Message on, on the text-side journey. Taking `live call` out of the pre-merge gate altogether amends the acceptance contract ([#180](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/180)), not this spec, and is [#260](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/260).
-- **The grading rule**, for the implementation issues to cite: *each Telegram acceptance observation reads one state keyed by a product-issued id; no observation that needs a waiting window or a wording match may be graded.* The harness takes the receipt synchronously — a Stop Notice is unbidden, so its `ChannelReceipt.message_ids` reach the harness through **one named engine-log record per send**, written under the engine's `getLogger` convention and part of the acceptance contract as the inbound line already is (#48); a relay's ids come back on the `bridgectl relay` reply — then reads the Telegram message once by that id through the user account — a snapshot, not "within N seconds". No negative observations are added; no model output is graded — "the Session acted" is a file effect in the workspace, as today's `relay` step proves it. A read that finds nothing is FAIL, and only two things produce it: the product did not send, or Telegram was unreachable, which preflight already gates. An INCONCLUSIVE grade was refused as naming the uncertainty instead of removing it. Telethon 1.44.0, the run's user-account client, already presses inline buttons, reads keyboards and reply anchors, and re-reads one message by id. **Amended 2026-09-11 ([#354](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/354), Simon): a Telegram private chat has two message-id sequences — the Bot API's `message_id` is the id in the bot's own dialog, and the user account cannot address a message by it (run `20260910T191643Z`: ids 1806 and 306 sent, 5939 and 5940 as the account sees them). So the harness does not re-read by the product-issued id. The observation stays a snapshot — one read, no window, no wording match — but it is keyed by a **mark** the harness took in the chat before the turn (the newest account-side id) and bounded by the engine-log record's count of ids: as many new messages after the mark as ids issued, none the account's own. The account-side id of the first is what `companion inbound` replies to; the product-issued ids are journalled as evidence and never graded. The Anchor Table above is untouched — it is keyed by the bot-side id, which is the space a reply's `reply_to_message` arrives in.** **Amended 2026-09-11 ([#355](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/355), Simon): the record also names who the message is about — a `target=` field carrying the Anchor's target address, spelled as the roster row and the `Session stopped:` line spell it and empty when the send names nobody — because one engine bridges every Session on the machine and the harness must take only the lines addressed to its own Session: reading the newest send line as this turn's graded another lane's Stop Notice as this lane's turn (run `20260910T191643Z`).**
+- **The real-environment run proves three facts only**: words replied to a notice reach *that* Session and it acts; a button press yields a toast and a delivered `relay`; the answered notice is edited to `handled` with its keyboard gone. Three new steps carry them, named now because step names are a contract build tickets cite: **`reply`** (words to a notice, proved by a file effect; the numeral refusal after a terminal answer is a wording and a non-event, so it is unit-proved and never walked), **`button`** (press, toast, edit — on a permission notice as well as a question notice), **`menu`** (`/sessions` → a Session → `send message` → words delivered). `companion inbound` keeps its name and becomes a reply-anchored message; the `@:` fallback is proved at the unit layer only. **Amended 2026-09-10 (map [#304](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/304), Simon): the real-environment run no longer proves these three facts and the `reply`, `button` and `menu` steps are not built — the acceptance harness proves Telegram connected only, one message out (`stop notice`) and one in (`companion inbound`, still reply-anchored), and the three behaviours are proved at the unit layer alone. [#267](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/267) closed unbuilt.**
+- **Never on the `live call` walk.** Every Telegram step runs with Voice off and Message on, on the text-side journey. Taking `live call` out of the pre-merge gate altogether amends the acceptance contract ([#180](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/180)), not this spec, and is [#260](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/260).
+- **The grading rule**, for the implementation issues to cite: *each Telegram acceptance observation reads one state keyed by a product-issued id; no observation that needs a waiting window or a wording match may be graded.* The harness takes the receipt synchronously — a Stop Notice is unbidden, so its `ChannelReceipt.message_ids` reach the harness through **one named engine-log record per send**, written under the engine's `getLogger` convention and part of the acceptance contract as the inbound line already is (#48); a relay's ids come back on the `bridgectl relay` reply — then reads the Telegram message once by that id through the user account — a snapshot, not "within N seconds". No negative observations are added; no model output is graded — "the Session acted" is a file effect in the workspace, as today's `relay` step proves it. A read that finds nothing is FAIL, and only two things produce it: the product did not send, or Telegram was unreachable, which preflight already gates. An INCONCLUSIVE grade was refused as naming the uncertainty instead of removing it. Telethon 1.44.0, the run's user-account client, already presses inline buttons, reads keyboards and reply anchors, and re-reads one message by id. **Amended 2026-09-11 ([#354](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/354), Simon): a Telegram private chat has two message-id sequences — the Bot API's `message_id` is the id in the bot's own dialog, and the user account cannot address a message by it (run `20260910T191643Z`: ids 1806 and 306 sent, 5939 and 5940 as the account sees them). So the harness does not re-read by the product-issued id. The observation stays a snapshot — one read, no window, no wording match — but it is keyed by a **mark** the harness took in the chat before the turn (the newest account-side id) and bounded by the engine-log record's count of ids: as many new messages after the mark as ids issued, none the account's own. The account-side id of the first is what `companion inbound` replies to; the product-issued ids are journalled as evidence and never graded. The Anchor Table above is untouched — it is keyed by the bot-side id, which is the space a reply's `reply_to_message` arrives in.** **Amended 2026-09-11 ([#355](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/355), Simon): the record also names who the message is about — a `target=` field carrying the Anchor's target address, spelled as the roster row and the `Session stopped:` line spell it and empty when the send names nobody — because one engine bridges every Session on the machine and the harness must take only the lines addressed to its own Session: reading the newest send line as this turn's graded another lane's Stop Notice as this lane's turn (run `20260910T191643Z`).**
- **The protocol-level e2e fake suite (`docs/e2e-suite-design.md`) is not built.** It would be a third harness, the cost the decision exists to avoid; `FakeTelegram` is already a real HTTP server in the Bot API's shape, and five more methods buy the same determinism.
## Considered and rejected
@@ -115,7 +115,7 @@ Decided on [#259](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issue
**Words.** The state words, the closed word, the ended word, 允许 / 拒绝, the "对 X 说:" prompt, the greeting and config labels, the truncation marker, the non-text hint, the assistant's opening line and the start-again hint are all entries in Core's wording tables (`core/briefing.py` and beside it), chosen by Core and laid out by the adapter, which chooses none. **All of them are English** (Simon, 2026-09-06): the table stays the one it is, the Voice keeps re-rendering it in speech, and the Telegram layout prints it as it stands. The Chinese in the map's mock-ups (`等你做决定`, `已处理`, `已结束`, `允许 / 拒绝`, `对 X 说:`) was the sample layout verified live on 2026-09-05, and the layout survives; the words in it do not. A per-surface language, and an adapter-held translation table, were both refused — the first is a layer of configuration nobody asked for, the second is a second vocabulary, which is the fork §5 exists to prevent.
-**Receipts.** *Superseded 2026-09-09 by the amendment below (map [#310](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/310), [#316](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/316)): a receipt is a reaction on the user's own message, and only three outcomes keep a sentence.* The original rule, for the record: a reply's receipt on this surface is **a sentence from Core's wording tables, not the three codes** (Simon, 2026-09-06). `bridgectl relay` keeps printing `state= grade= reason=` — the machine-readable line `docs/control-plane.md` fixes — and the Companion Channel's inbound path answers with it today; under this ADR the path renders those same three facts into one sentence, held once in Core beside the receipt line (`core/relays.py`), and the Telegram adapter prints or toasts it unchanged. That sentence carries the clause ADR 0013's amendment gives the Voice, on the same test: when the words answered a question the Session merely *said* — plain text, no held hook — the receipt adds that the Session may not treat it as the user's own confirmation; an answer through the held-hook route (ADR 0015) and a permission verdict get no such clause, because both carry the user's authority. One rule, one place, two surfaces reading it. Showing the raw codes was refused as the log feel the requirements page rejects, and a sentence without the clause was refused because it would tell the phone user their answer was their own on a route that carries nobody's.
+**Receipts.** *Superseded 2026-09-09 by the amendment below (map [#310](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/310), [#316](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/316)): a receipt is a reaction on the user's own message, and only three outcomes keep a sentence.* The original rule, for the record: a reply's receipt on this surface is **a sentence from Core's wording tables, not the three codes** (Simon, 2026-09-06). `bridgectl relay` keeps printing `state= grade= reason=` — the machine-readable line `docs/control-plane.md` fixes — and the Companion Channel's inbound path answers with it today; under this ADR the path renders those same three facts into one sentence, held once in Core beside the receipt line (`core/relays.py`), and the Telegram adapter prints or toasts it unchanged. That sentence carries the clause ADR 0013's amendment gives the Voice, on the same test: when the words answered a question the Session merely *said* — plain text, no held hook — the receipt adds that the Session may not treat it as the user's own confirmation; an answer through the held-hook route (ADR 0015) and a permission verdict get no such clause, because both carry the user's authority. One rule, one place, two surfaces reading it. Showing the raw codes was refused as the log feel the requirements page rejects, and a sentence without the clause was refused because it would tell the phone user their answer was their own on a route that carries nobody's.
**Docs that describe running code follow the code, not this ADR**: the "one renderer" paragraph of `docs/control-plane.md` and the `companion inbound` row of `docs/acceptance-design.md` are true of today's build and are amended by the implementation issue that changes each, citing §5 and §10 here. The realtime adapter's "does not outlive it" paragraph is amended by the issue that builds §7.
@@ -127,11 +127,11 @@ Decided on [#259](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issue
`legacy@1d32845` has the Companion Channel seam and a null channel only (`bridge/channel.py`); it has **no Telegram implementation**, no reply anchoring, no buttons, no message editing, no menu and no assistant. Its one channel behaviour — mirroring every user-visible message of the Bridge's own (`bridge/channel.py:11-14`, `daemon.py:2179-2200`) — is **dropped, because** this generation's Message Switch and Cool-down exist precisely to stop that chatter (§9). The Stop Notice for every stop state, `finished` included, is **ported** (the FINISHED wording is legacy's own sentence, `bridge/host.py:226-234`, per #165 Q7). The numeral-to-label rule is **adapted** from ADR 0014's option-label rule; the assistant is **adapted** from this generation's own Delegated Turn (`adapters/call/realtime/adapter.py::delegate`). Everything else under this ADR is **new**.
-## Amendment 2026-09-09: the state words, the Headless Run's silence, the receipt as a reaction (map [#310](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/310))
+## Amendment 2026-09-09: the state words, the Headless Run's silence, the receipt as a reaction (map [#310](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/310))
-Decided on [#314](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/314), [#315](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/315), [#316](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/316) and [#317](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/317) against every push of 2026-09-09 (189 pushes, 117 Stop Notices, of which 32 said what the Session was doing).
+Decided on [#314](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/314), [#315](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/315), [#316](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/316) and [#317](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/317) against every push of 2026-09-09 (189 pushes, 117 Stop Notices, of which 32 said what the Session was doing).
-**The state words are six** ([#314](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/314)). `core/briefing.py` stays the one wording table; the Telegram layout lights them:
+**The state words are six** ([#314](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/314)). `core/briefing.py` stays the one wording table; the Telegram layout lights them:
| light | word | meaning |
| --- | --- | --- |
@@ -144,9 +144,9 @@ Decided on [#314](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issue
`unreadable` is retired on every surface. **A question mark makes a decision; nothing else does** — the deterministic rule that replaces reading a hand-over out of prose (the same shape got both words on the same day). **🟣 is symmetric**: a turn whose tail closes on a peer `SendMessage` (one whose result carries no `routing` object, addressed to a Session the engine knows) enters it whether the message was a question or a ruling, because the transcript cannot tell the two apart and the user's action is the same — not their turn; it is left on the first later `origin.kind == "peer"` record from that Session (never from the engine's own `vc-relay-` socket) and, as the backstop, on the Session's next turn beginning. A wait on the Session's own Child Process enters it through the existing child tracking. Leaving pushes nothing; the next stop raises a fresh notice. A 🟣 notice is an Anchor like every Stop Notice: a reply goes to the Session that raised it. Claude lane only for the peer half — the Codex lane has no peer channel.
-**A Headless Run is silent** ([#315](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/315); ADR 0020 as amended): no Stop Notice, no ended line, no Anchor, no Relay, no Roster Brief row. Noise leaves the channel by not announcing what is not a Session — never by merging or hiding a Session's states: a crew worker in its tmux pane is a Session and pushes every state, one event one message.
+**A Headless Run is silent** ([#315](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/315); ADR 0020 as amended): no Stop Notice, no ended line, no Anchor, no Relay, no Roster Brief row. Noise leaves the channel by not announcing what is not a Session — never by merging or hiding a Session's states: a crew worker in its tmux pane is a Session and pushes every state, one event one message.
-**A receipt is a reaction on the user's own message** ([#316](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/316); measured on [#311](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/311): a bot may set exactly one emoji on a message the user sent, swap it, or clear it, silently — the phone neither sounds nor banners). The reaction tracks the Relay's state and rests on the final one; a sentence is sent only where the table says so, as a reply to the user's message:
+**A receipt is a reaction on the user's own message** ([#316](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/316); measured on [#311](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/311): a bot may set exactly one emoji on a message the user sent, swap it, or clear it, silently — the phone neither sounds nor banners). The reaction tracks the Relay's state and rests on the final one; a sentence is sent only where the table says so, as a reply to the user's message:
| Relay outcome | reaction | sentence |
| --- | --- | --- |
@@ -161,5 +161,5 @@ Emoji come from Telegram's fixed list of 73 and are sent as the reference lists
**§8 loses one cause**: a question the user answered at the terminal leaves its notice as sent. **§9 gains one silence**: the Headless Run.
-**The first notice's name** ([#317](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/317)) is unaffected — it was always the derived name; see ADR 0024's amendment for the rung that climbed over it.
+**The first notice's name** ([#317](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/317)) is unaffected — it was always the derived name; see ADR 0024's amendment for the rung that climbed over it.
diff --git a/docs/adr/0022-the-shared-app-server-is-the-users-own-codex.md b/docs/adr/0022-the-shared-app-server-is-the-users-own-codex.md
index f18824c..bb27854 100644
--- a/docs/adr/0022-the-shared-app-server-is-the-users-own-codex.md
+++ b/docs/adr/0022-the-shared-app-server-is-the-users-own-codex.md
@@ -1,8 +1,8 @@
# 22. The shared app-server is the user's own codex, on a derived control socket
-Date: 2026-09-07 · Status: Accepted · Source: [#272](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/272), prototyped by [#271](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/271)
+Date: 2026-09-07 · Status: Accepted · Source: [#272](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/272), prototyped by [#271](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/271)
-[ADR 0012](0012-installation-runs-at-first-launch.md) put a login `LaunchAgent` on the machine and [#82](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/82) chose what it starts: the **managed standalone** binary at `$CODEX_HOME/packages/standalone/current/codex`, run as `app-server daemon start`. That choice was deliberate and it was argued from a real hazard — what `codex` resolves to in a shell is whatever the user's profile says, and on this product's own author's machine it was once a generation-1 wrapper function. Deriving one binary from one `CODEX_HOME` made the LaunchAgent and the Codex adapter agree.
+[ADR 0012](0012-installation-runs-at-first-launch.md) put a login `LaunchAgent` on the machine and [#82](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/82) chose what it starts: the **managed standalone** binary at `$CODEX_HOME/packages/standalone/current/codex`, run as `app-server daemon start`. That choice was deliberate and it was argued from a real hazard — what `codex` resolves to in a shell is whatever the user's profile says, and on this product's own author's machine it was once a generation-1 wrapper function. Deriving one binary from one `CODEX_HOME` made the LaunchAgent and the Codex adapter agree.
What it bought was agreement between two parts of this product. What it cost was agreement with the user.
@@ -51,15 +51,15 @@ Upstream hard-codes the managed path in `managed_install.rs`, and `openai/codex#
The engine already runs on the user's real `PATH` — `ProcessLauncher.swift` reads it from their **login and interactive** shell (`LoginShellPath.swift`) and spawns with it. `InstallationRunner` now does the same, so the reconcile resolves codex with an ordinary `which` over that `PATH`, and the plist's `PATH` is that same value.
-**Amended by [#275](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/275): that same value, read with no inherited `PATH`, so it states the profile and nothing about the launch.** `LoginShellPath` runs the shell as a child of the app, and zsh's profile *prepends* to the `PATH` it inherits — so the answer was ` + `, and the app's own `PATH` is a property of how it was started, not of the machine. Measured on the reference machine: a terminal-launched app rendered a plist carrying that terminal's trailing entries, a Finder-launched one rendered the same plist without them, each launch called the other's render "a job this build would write differently", and `status` reported the LaunchAgent `stale` on a machine that was current. The reader now gives the child an environment with **no `PATH` at all**; `/etc/zprofile` rebuilds the system part through `path_helper` and the user's own files prepend on top, and the answer is byte-identical to the Finder render. This removes an input rather than supplying one, so [#38](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/38) is untouched, and "one reading, one implementation" holds: both `ProcessLauncher` and `InstallationRunner` get the same machine-determined answer. The rendered `PATH` stays the whole profile `PATH` and is not narrowed to the executable's directory — the shared app-server runs the user's Sessions' shell commands under it.
+**Amended by [#275](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/275): that same value, read with no inherited `PATH`, so it states the profile and nothing about the launch.** `LoginShellPath` runs the shell as a child of the app, and zsh's profile *prepends* to the `PATH` it inherits — so the answer was ` + `, and the app's own `PATH` is a property of how it was started, not of the machine. Measured on the reference machine: a terminal-launched app rendered a plist carrying that terminal's trailing entries, a Finder-launched one rendered the same plist without them, each launch called the other's render "a job this build would write differently", and `status` reported the LaunchAgent `stale` on a machine that was current. The reader now gives the child an environment with **no `PATH` at all**; `/etc/zprofile` rebuilds the system part through `path_helper` and the user's own files prepend on top, and the answer is byte-identical to the Finder render. This removes an input rather than supplying one, so [#38](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/38) is untouched, and "one reading, one implementation" holds: both `ProcessLauncher` and `InstallationRunner` get the same machine-determined answer. The rendered `PATH` stays the whole profile `PATH` and is not narrowed to the executable's directory — the shared app-server runs the user's Sessions' shell commands under it.
**And the CLI still reads no login shell — but it no longer renders over the terminal's own `PATH` either.** This paragraph accepted a false `stale`: `bridge-install status` compared against the terminal it was typed in, so a terminal whose `PATH` differed from the profile's reported "would write differently" about a plist that was correct, and the note was made to name the differing top-level key so it was explainable at a glance.
-**Amended by [#327](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/327): that acceptance is retired, because the same render was also being written.** What this paragraph reasoned about is `status`, which is documented as *say what is on this machine, and write nothing*. `reconcile` and `install` render from the same caller-dependent `PATH` and put the result on disk, and that was never argued anywhere. Measured on the reference machine on 2026-09-11: a terminal-run `reconcile` replaced the job's `PATH` with the profile's own entries followed by that session's — duplicated directories, plus two version-pinned plugin-cache paths that the next plugin upgrade deletes — and the `status` line above is what tells a reader to run it. Nothing was lost on that machine, and neither guard was one: `reconcile` declines to reload a job with live Sessions under it, and the app's next launch reconcile restored the file.
+**Amended by [#327](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/327): that acceptance is retired, because the same render was also being written.** What this paragraph reasoned about is `status`, which is documented as *say what is on this machine, and write nothing*. `reconcile` and `install` render from the same caller-dependent `PATH` and put the result on disk, and that was never argued anywhere. Measured on the reference machine on 2026-09-11: a terminal-run `reconcile` replaced the job's `PATH` with the profile's own entries followed by that session's — duplicated directories, plus two version-pinned plugin-cache paths that the next plugin upgrade deletes — and the `status` line above is what tells a reader to run it. Nothing was lost on that machine, and neither guard was one: `reconcile` declines to reload a job with live Sessions under it, and the app's next launch reconcile restored the file.
The rule now is that the rendered `PATH` has two sources and no third — **what the caller states, else what the standing job records** — and the process's own `PATH` is not one of them. The shell states its login-shell reading on the installation subprocess under `GPT_VOICECODING_LOGIN_PATH`, and only when the reading was actually taken; a run nobody stated one to defers to the `PATH` the plist already carries, which is what that one reading last wrote and so is no second copy of the profile. This side still reads no shell, nothing constant is introduced (#38 untouched), and the `PATH` is still the whole profile one rather than the executable's directory. `_differing_keys`' precision stays — it is useful for every other key — and the `which` that picks the executable runs over the same machine `PATH`, because a terminal choosing what goes into `ProgramArguments` is this defect one key across.
-A third state falls out of the rule, and it is a refusal rather than a report. A run that was stated nothing and finds no standing job to read cannot say what this machine's `PATH` is, so it has no render to compare against and none to write: it says so and the run is **non-zero** (Simon's ruling on this ticket's review, 2026-09-11). [#276](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/276)'s "`ok` stays true even when the `PATH` is the reason" is not reopened by that — it ruled on *this machine's `PATH` is known and has no codex on it*, which is a fact about the machine, and this is a caller with no standing over the question at all. The two are told apart by `Resolution.path_unknown` rather than by matching on a sentence.
+A third state falls out of the rule, and it is a refusal rather than a report. A run that was stated nothing and finds no standing job to read cannot say what this machine's `PATH` is, so it has no render to compare against and none to write: it says so and the run is **non-zero** (Simon's ruling on this ticket's review, 2026-09-11). [#276](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/276)'s "`ok` stays true even when the `PATH` is the reason" is not reopened by that — it ruled on *this machine's `PATH` is known and has no codex on it*, which is a fact about the machine, and this is a caller with no standing over the question at all. The two are told apart by `Resolution.path_unknown` rather than by matching on a sentence.
Two further consequences, both ruled rather than assumed. A terminal-run `status` on a current machine now reports **`current`**: a terminal has no standing to judge the profile `PATH`, the authority on whether it changed is the shell's reading at every app launch, and the alternative — a "this key cannot be judged from here" sentence on every run — was considered and declined as permanent noise (Simon's ruling, 2026-09-11). And checking a real machine's installation state now takes only the installed interpreter — `/Applications/GPT-VoiceCoding.app/Contents/Resources/engine/bin/python3 -m gpt_voicecoding.installation status`, because the hook block names the interpreter that wrote it (#38) — rather than that **and** a terminal whose `PATH` is the profile's.
@@ -78,21 +78,21 @@ Two consequences fall out of resolving over a `PATH` rather than asking a shell
env: node: No such file or directory
```
-and never held the socket. `daemon start` never met this because the managed standalone is a native binary. So the job carries the `PATH` the reconcile ran on. Nothing rendered is hard-coded ([#38](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/38)): writing launchd's own default into a plist would be a constant standing in for something the environment already states. The directory holding the executable is on that `PATH` by construction — it is where `which` found it — and nvm and npm put `node` in that same directory beside the shim. A Homebrew or standalone codex is native and needs none of this; the entry is harmless there, so there is no branch on install kind.
+and never held the socket. `daemon start` never met this because the managed standalone is a native binary. So the job carries the `PATH` the reconcile ran on. Nothing rendered is hard-coded ([#38](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/38)): writing launchd's own default into a plist would be a constant standing in for something the environment already states. The directory holding the executable is on that `PATH` by construction — it is where `which` found it — and nvm and npm put `node` in that same directory beside the shim. A Homebrew or standalone codex is native and needs none of this; the entry is harmless there, so there is no branch on install kind.
### launchd owns the process now
-With `--listen` the job **is** the server: `state = running, active count = 1`, against `daemon start`'s `state = not running`. `KeepAlive` stays absent, so this is still not a supervisor and [#83](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/83)'s scope is unchanged — but `launchctl print` now answers "is the shared server up" truthfully, which it never did before.
+With `--listen` the job **is** the server: `state = running, active count = 1`, against `daemon start`'s `state = not running`. `KeepAlive` stays absent, so this is still not a supervisor and [#83](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/83)'s scope is unchanged — but `launchctl print` now answers "is the shared server up" truthfully, which it never did before.
### Status is a handshake, and it names no version
`bridge-install status` connects to the derived socket and completes a codex `initialize`. Measured at **1 ms** against a live server, against 139 ms for the subprocess it replaces. It reports three states — no socket, a socket nothing is behind, a server that answered — and **says nothing about versions**. The running server's version is available only as a substring of `userAgent`, and with one codex on the machine there is no second version for the first to disagree with.
-**[#67](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/67)'s no-version-pin ruling is not reopened, it is dissolved.** There is no pair of versions. The `degraded` note for a version disagreement goes; everything else `degraded` carries stays.
+**[#67](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/67)'s no-version-pin ruling is not reopened, it is dissolved.** There is no pair of versions. The `degraded` note for a version disagreement goes; everything else `degraded` carries stays.
### The protocol moved down, the transport did not
-The handshake gave this product a second caller for its hand-rolled RFC 6455, and ADR 0012 forbids installation importing an adapter. Rather than write the framing twice — [#47](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/47)'s shape exactly — the upgrade request, the accept-key proof and the frame layout moved into `gpt_voicecoding/websocket.py`, a stdlib-only leaf beside `locations.py`. The engine's client stays `asyncio` because it lives in the engine's loop; installation's stays a blocking socket because it runs before any loop exists. The move was pure: `adapters/codex_app_server/wire.py` kept its behaviour and its public names, and every existing test of it passes unchanged.
+The handshake gave this product a second caller for its hand-rolled RFC 6455, and ADR 0012 forbids installation importing an adapter. Rather than write the framing twice — [#47](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/47)'s shape exactly — the upgrade request, the accept-key proof and the frame layout moved into `gpt_voicecoding/websocket.py`, a stdlib-only leaf beside `locations.py`. The engine's client stays `asyncio` because it lives in the engine's loop; installation's stays a blocking socket because it runs before any loop exists. The move was pure: `adapters/codex_app_server/wire.py` kept its behaviour and its public names, and every existing test of it passes unchanged.
## Migration
@@ -104,7 +104,7 @@ A machine that ran #82's job carries `com.gpt-voicecoding.codex-daemon` with the
## What this does not decide
-**ADR 0020 is not reopened.** Its rule — a Codex Session is a daemon-held user root that a live terminal vouches for — is unchanged. What changed is who started the server holding those threads; see the vocabulary amendment at the head of that document. [#144](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/144)'s identity rule is untouched, and so is [#132](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/132)'s plist-SHA read-back.
+**ADR 0020 is not reopened.** Its rule — a Codex Session is a daemon-held user root that a live terminal vouches for — is unchanged. What changed is who started the server holding those threads; see the vocabulary amendment at the head of that document. [#144](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/144)'s identity rule is untouched, and so is [#132](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/132)'s plist-SHA read-back.
**Installing codex for the user is out of scope, permanently.** The product uses the codex the user has and reports when there is none — the Codex lane says it is absent, with the reason, and the run carries on. Reviving generation 1's per-Session wrapped app-server (ADR 0010) stays dropped.
@@ -112,7 +112,7 @@ A machine that ran #82's job carries `com.gpt-voicecoding.codex-daemon` with the
Stated rather than implied, because this ADR is written on measurements and these are the ones not yet taken:
-1. **A Live Call on the resolved executable.** Needs a person and a microphone; also [#270](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/270)'s scenario.
+1. **A Live Call on the resolved executable.** Needs a person and a microphone; also [#270](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/270)'s scenario.
2. **A real upgrade with the job up.** `npm view @openai/codex version` was `0.153.4` on the day, which is what was installed, so there was no upgrade to run.
3. **Homebrew.** The `PATH` rule is argued for a native binary, not measured against one.
diff --git a/docs/adr/0024-the-task-half-of-a-session-name-is-descriptive-and-climbs.md b/docs/adr/0024-the-task-half-of-a-session-name-is-descriptive-and-climbs.md
index d8250af..7ee350e 100644
--- a/docs/adr/0024-the-task-half-of-a-session-name-is-descriptive-and-climbs.md
+++ b/docs/adr/0024-the-task-half-of-a-session-name-is-descriptive-and-climbs.md
@@ -47,7 +47,7 @@ facts and the resolved project name; the module lives in Bridge Core (ADR 0001:
`core` imports no adapter), which calls it and stores the result and the rung it
came from.
-## Amendment 2026-09-09: an absolute path is refused as a first-words candidate (map [#310](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/310), [#317](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/317))
+## Amendment 2026-09-09: an absolute path is refused as a first-words candidate (map [#310](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/310), [#317](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/317))
On 2026-09-09 seven crew workers were first named by the floor — `306-306-ed` and its siblings — and then **climbed to a path**: their first prompt was a slash command whose argument begins with a file path, cleaning stripped the wrapper to that argument, and forty characters of it, `/Users/simon/Documents/coding/GPT-VoiceC`, outranked the derived name for the rest of the day. The ladder did exactly what this document says; the candidate was the problem.
diff --git a/docs/adr/0026-the-call-agent-shapes-the-relayed-instruction.md b/docs/adr/0026-the-call-agent-shapes-the-relayed-instruction.md
index 71b2462..0b5adf7 100644
--- a/docs/adr/0026-the-call-agent-shapes-the-relayed-instruction.md
+++ b/docs/adr/0026-the-call-agent-shapes-the-relayed-instruction.md
@@ -1,6 +1,6 @@
# 26. The Call Agent shapes the Relayed Instruction, in prompt alone; the engine carries words and adds no guard
-Date: 2026-09-09 · Status: Accepted · Source: [#289](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/289) on map [#283](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/283); evidence [#284](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/284), [#285](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/285), [#287](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/287), [#288](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/288), [#294](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/294), [#299](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/299)
+Date: 2026-09-09 · Status: Accepted · Source: [#289](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/289) on map [#283](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/283); evidence [#284](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/284), [#285](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/285), [#287](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/287), [#288](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/288), [#294](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/294), [#299](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/299)
A Live Call is two models (ADR 0018), and a spoken instruction passes through both
before a Relay puts it into a Session. The map asked where it is shaped into the
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 4fcc38f..19076af 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -31,8 +31,8 @@
| [0029](0029-the-control-window-owns-its-height.md) | The Control Window owns its height; nothing outside it measures content |
| [0030](0030-each-live-call-audio-crossing-has-one-owner.md) | Each Live Call audio crossing has one owner: the speaker keeps listening continuous, the microphone keeps speaking fresh — **no bare buffer, no fixed prebuffer** |
-0008 (headless direct-child launcher) and 0009 (a launch carries its Opening Instruction) were removed with the launcher when v1.0 became a bridge over Sessions the user starts ([#67](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/67), [#68](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/68)); they return with the launch map, from git history.
+0008 (headless direct-child launcher) and 0009 (a launch carries its Opening Instruction) were removed with the launcher when v1.0 became a bridge over Sessions the user starts ([#67](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/67), [#68](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/68)); they return with the launch map, from git history.
## Provenance
-The decisions above were taken in the reference implementation and locked by its [wayfinding map](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/issues/11); each ADR links its source. Legacy ADRs deliberately not carried: [0001 launchd supervision](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/blob/main/docs/adr/0001-launchd-supervision-for-the-bridge-daemon.md) (superseded by ADR 0005), [0002 session retention](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/blob/main/docs/adr/0002-session-retention-and-indexed-session-reads.md) (a schema lesson for a store not yet built), [0005 Live Driver on the GUI](https://github.com/okqixiaobao727-design/GPT-VoiceCoding-legacy/blob/main/docs/adr/0005-live-driver-stays-on-the-gui-hotkey-quarantined-as-an-adapter.md) (superseded by the bridge-owned call, ADR 0001).
+The decisions above were taken in the reference implementation and locked by its [wayfinding map](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/issues/11); each ADR links its source. Legacy ADRs deliberately not carried: [0001 launchd supervision](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/blob/main/docs/adr/0001-launchd-supervision-for-the-bridge-daemon.md) (superseded by ADR 0005), [0002 session retention](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/blob/main/docs/adr/0002-session-retention-and-indexed-session-reads.md) (a schema lesson for a store not yet built), [0005 Live Driver on the GUI](https://github.com/Simon-AI-coding/GPT-VoiceCoding-legacy/blob/main/docs/adr/0005-live-driver-stays-on-the-gui-hotkey-quarantined-as-an-adapter.md) (superseded by the bridge-owned call, ADR 0001).
diff --git a/docs/control-plane.md b/docs/control-plane.md
index 0fb01a6..0d0652a 100644
--- a/docs/control-plane.md
+++ b/docs/control-plane.md
@@ -66,7 +66,7 @@ existed before the field, which is what keeps the Companion Channel byte for
byte unchanged. An unrecognised value is `malformed_request`, naming the values
there are. The engine sets it, in the `bridgectl` invocation it generates for
the Call Agent (`--reader voice`); the Companion Channel's `/` grammar never
-does ([#302](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/302),
+does ([#302](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/302),
ADR 0016 as amended).
```json
@@ -220,7 +220,7 @@ not protocol 6's: that one was a second rendering of the roster, and this one
answers with Briefing's own text and adds only labels.
Protocol 10 adds the optional `reader` field described under **Request**
-([#302](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/302)). A
+([#302](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/302)). A
protocol-9 engine accepts the same request and ignores the field, answering a
page fitted to this wire alone — which codex then cuts on the way to the Voice,
silently and in the middle. So a surface has to be able to tell the two apart
@@ -229,7 +229,7 @@ at 9, and no gate on the field is added on the Python side: `bridgectl` ships
inside the engine's own package, so the two move together.
`launch` and `close` were the eighth and ninth until protocol 4. They are parked
-with the code behind them ([#72](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/72)):
+with the code behind them ([#72](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/72)):
v1.0 is a bridge over the Sessions the user starts, so nothing here brings one
into existence or ends one. Their two error codes, `launch_failed` and
`close_failed`, went with them. A surface still sending either action is
diff --git a/docs/diagrams/draw.py b/docs/diagrams/draw.py
new file mode 100644
index 0000000..66717b5
--- /dev/null
+++ b/docs/diagrams/draw.py
@@ -0,0 +1,374 @@
+"""Draw the README's diagrams as light and dark SVGs in docs/images/.
+
+ python3 docs/diagrams/draw.py
+
+The canvas is 880 wide, GitHub's README column, so a size written here is the size a reader
+sees. Every box and edge is read off the code; docs/github-promotion.local.md (sessions 6 and 7)
+records where each comes from. Dashes moving along an edge show which way data goes; they stop
+for readers who ask for reduced motion.
+"""
+
+from dataclasses import dataclass
+from pathlib import Path
+from xml.sax.saxutils import escape
+
+IMAGES = Path(__file__).resolve().parent.parent / "images"
+WIDTH = 880
+FONT = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
+
+
+@dataclass(frozen=True)
+class Palette:
+ background: str
+ dots: str
+ card: str
+ stroke: str
+ group: str
+ text: str
+ dim: str
+ line: str
+ accent: str
+ glow: float
+
+
+LIGHT = Palette(
+ background="#ffffff",
+ dots="#dfe3e8",
+ card="#f6f8fa",
+ stroke="#d0d7de",
+ group="#afb8c1",
+ text="#1f2328",
+ dim="#656d76",
+ line="#c4cad1",
+ accent="#c26a00",
+ glow=0.25,
+)
+DARK = Palette(
+ background="#0d1117",
+ dots="#1c232c",
+ card="#131a22",
+ stroke="#2d3540",
+ group="#3d4652",
+ text="#e6edf3",
+ dim="#7d8590",
+ line="#343c47",
+ accent="#f0a030",
+ glow=0.55,
+)
+CLAUDE = "#d97757"
+CODEX = "#8b95a5"
+
+
+class Canvas:
+ def __init__(self, height: int, title: str):
+ self.height = height
+ self.title = title
+ self.lines: list[str] = []
+ self.parts: list = []
+ self.keyframes: list[str] = []
+
+ def add(self, markup: str) -> None:
+ self.parts.append(markup)
+
+ def glow(self, x, y, w, h) -> None:
+ """A blurred accent halo behind the box at the same place, coloured per palette."""
+ self.parts.append((x, y, w, h))
+
+ def text(self, x, y, value, size=12, cls="text", anchor="middle", weight=400, spacing=0):
+ self.add(
+ f'{escape(value)}'
+ )
+
+ def card(self, x, y, w, h, title, sub=None, cls="card", dot=None):
+ self.add(f'')
+ cx = x + w / 2
+ if sub is None:
+ ty = y + h / 2 + 4.5
+ else:
+ ty = y + h / 2 - 3
+ if dot:
+ label_w = len(title) * 7.8
+ self.add(f'')
+ cx += 7
+ self.text(cx, ty, title, size=13, weight=600)
+ if sub is not None:
+ self.text(x + w / 2, ty + 17, sub, size=11, cls="dim")
+
+ def group(self, x, y, w, h, label, note=""):
+ self.add(f'')
+ self.add(
+ f''
+ )
+ self.text(x + 22, y + 4, label, size=12, cls="accent", anchor="start", weight=600)
+ if note:
+ self.text(
+ x + 22 + len(label) * 7.25 + 8, y + 4, note, size=11, cls="dim", anchor="start"
+ )
+
+ def edge(self, points, flow="forward", arrow_end=True, arrow_start=False, speed=1.6):
+ path = rounded(points)
+ markers = ""
+ if arrow_end:
+ markers += ' marker-end="url(#arrow)"'
+ if arrow_start:
+ markers += ' marker-start="url(#arrow-back)"'
+ self.lines.append(f'')
+ directions = {"forward": ["fwd"], "back": ["back"], "both": ["fwd", "back"]}[flow]
+ for d in directions:
+ self.lines.append(
+ f''
+ )
+
+ def pulse(self, path, length, index, count, period):
+ """A spark that runs once along `path` in its own slot of a repeating sequence."""
+ share = 100 / count
+ self.keyframes.append(
+ f"@keyframes pulse{index} {{ 0% {{ stroke-dashoffset: 10; opacity: 1; }} "
+ f"{share * 0.9:.2f}% {{ stroke-dashoffset: {-length}; opacity: 1; }} "
+ f"{share * 0.9 + 0.01:.2f}%, 100% {{ stroke-dashoffset: {-length}; opacity: 0; }} }}"
+ )
+ self.parts.append(
+ f''
+ )
+
+ @staticmethod
+ def halo(p: Palette, x, y, w, h) -> str:
+ return (
+ f''
+ )
+
+ def render(self, p: Palette) -> str:
+ style = f"""
+ text {{ font-family: {FONT}; }}
+ .bg {{ fill: {p.background}; }}
+ .text {{ fill: {p.text}; }}
+ .dim {{ fill: {p.dim}; }}
+ .accent {{ fill: {p.accent}; }}
+ .card {{ fill: {p.card}; stroke: {p.stroke}; stroke-width: 1; }}
+ .core {{ fill: {p.card}; stroke: {p.accent}; stroke-width: 1.5; }}
+ .group {{ fill: none; stroke: {p.group}; stroke-width: 1; stroke-dasharray: 4 4; }}
+ .line {{ fill: none; stroke: {p.line}; stroke-width: 1.5; }}
+ .flow {{ fill: none; stroke: {p.accent}; stroke-width: 1.5; stroke-linecap: round;
+ stroke-dasharray: 2 14; animation: flow linear infinite; }}
+ .flow.back {{ animation-name: flow-back; }}
+ .lifeline {{ stroke: {p.line}; stroke-width: 1; stroke-dasharray: 3 5; }}
+ @keyframes flow {{ from {{ stroke-dashoffset: 16; }} to {{ stroke-dashoffset: 0; }} }}
+ @keyframes flow-back {{ from {{ stroke-dashoffset: 0; }} to {{ stroke-dashoffset: 16; }} }}
+ .pulse {{ fill: none; stroke: {p.accent}; stroke-width: 3; stroke-linecap: round; }}
+ .label-bg {{ fill: {p.background}; }}
+ {" ".join(self.keyframes)}
+ @media (prefers-reduced-motion: reduce) {{ .flow, .pulse {{ animation: none; }} }}
+"""
+ defs = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ body = "\n ".join(
+ self.lines
+ + [part if isinstance(part, str) else self.halo(p, *part) for part in self.parts]
+ )
+ size = f'viewBox="0 0 {WIDTH} {self.height}" width="{WIDTH}" height="{self.height}"'
+ return f"""
+"""
+
+
+def rounded(points, radius=10):
+ """An orthogonal polyline with its corners rounded."""
+ d = f"M{points[0][0]},{points[0][1]}"
+ for i in range(1, len(points) - 1):
+ (x0, y0), (x1, y1), (x2, y2) = points[i - 1], points[i], points[i + 1]
+ r = min(radius, abs(x1 - x0 + y1 - y0) / 2, abs(x2 - x1 + y2 - y1) / 2)
+ ax = x1 - r * sign(x1 - x0)
+ ay = y1 - r * sign(y1 - y0)
+ bx = x1 + r * sign(x2 - x1)
+ by = y1 + r * sign(y2 - y1)
+ d += f" L{ax},{ay} Q{x1},{y1} {bx},{by}"
+ d += f" L{points[-1][0]},{points[-1][1]}"
+ return d
+
+
+def sign(v):
+ return (v > 0) - (v < 0)
+
+
+def architecture() -> Canvas:
+ c = Canvas(600, "GPT-VoiceCoding architecture")
+ c.text(40, 30, "gpt-voicecoding", size=11, cls="dim", anchor="start", spacing=1)
+ c.text(840, 30, "architecture", size=11, cls="dim", anchor="end", spacing=1)
+
+ # Your terminals, and the Codex daemon they join.
+ c.group(40, 62, 400, 96, "~/terminals", "started by you")
+ c.card(60, 94, 170, 44, "claude code", dot=CLAUDE)
+ c.card(250, 94, 170, 44, "codex", dot=CODEX)
+ c.card(520, 86, 320, 60, "codex app-server", "started by us, joined by your sessions")
+
+ # The engine.
+ c.group(40, 206, 800, 270, "engine", "python · asyncio")
+ c.card(60, 236, 170, 44, "agent adapters")
+ c.card(660, 236, 160, 44, "call adapter")
+ c.glow(300, 302, 280, 76)
+ c.card(300, 302, 280, 76, "BRIDGE CORE", "every policy · one source of truth", cls="core")
+ c.card(60, 414, 200, 40, "companion channel")
+ c.card(330, 414, 220, 40, "control plane", None)
+
+ # Outside the engine.
+ c.card(60, 520, 220, 56, "telegram", "reply from your phone")
+ c.card(300, 520, 200, 56, "menu-bar app", "duty lamp · control panel")
+ c.card(520, 520, 110, 56, "bridgectl", "cli")
+ c.card(650, 520, 190, 56, "live call", "voice speaks · agent acts")
+
+ # Edges, in the direction data moves.
+ c.edge([(145, 138), (145, 236)], flow="both", arrow_start=True)
+ c.text(155, 186, "hooks · roster · inbox socket", size=11, cls="dim", anchor="start")
+ c.edge([(420, 116), (520, 116)], flow="both", arrow_end=False)
+ c.edge([(580, 146), (580, 258), (230, 258)], flow="both")
+ c.edge([(740, 146), (740, 236)], speed=1.2)
+ c.text(730, 186, "realtime route", size=11, cls="dim", anchor="end")
+ c.edge([(145, 280), (145, 340), (300, 340)], flow="both", arrow_end=False)
+ c.edge([(580, 340), (620, 340), (620, 258), (660, 258)], flow="both", arrow_end=False)
+ c.edge([(360, 378), (360, 396), (160, 396), (160, 414)], flow="both", arrow_end=False)
+ c.edge([(440, 378), (440, 414)], flow="both", arrow_end=False)
+ c.edge([(160, 454), (160, 520)], flow="both", arrow_end=False)
+ c.edge([(400, 454), (400, 520)], flow="both", arrow_end=False)
+ c.edge([(500, 454), (500, 490), (575, 490), (575, 520)], flow="both", arrow_end=False)
+ c.edge([(740, 280), (740, 520)], flow="both", arrow_end=False)
+ return c
+
+
+ACTORS = ["session", "bridge core", "call keeper", "call agent", "voice", "you", "telegram"]
+COLUMN = {name: 80 + 120 * i for i, name in enumerate(ACTORS)}
+
+# What happens when a Session stops, read off core/bridge.py:_session_stopped: the reading goes
+# to the roster, the Stop Notice goes out as an Anchor, then the Call Keeper is woken, and it
+# briefs from the roster when it dials (ADR 0017). ("group", label, note) opens a box and
+# ("end", "", "") closes it.
+STOP = [
+ ("session", "bridge core", "stops, waiting on a decision"),
+ ("bridge core", "bridge core", "writes the reading to the roster"),
+ ("bridge core", "telegram", "stop notice, as an anchor you can reply to"),
+ ("bridge core", "call keeper", "wake"),
+ ("group", "on the call", ""),
+ ("call keeper", "voice", "dials, briefed from the roster as it stands"),
+ ("voice", "you", "speaks the session brief"),
+ ("you", "voice", '"use email login, and give me the steps"'),
+ ("voice", "call agent", "hands the job over"),
+ ("call agent", "bridge core", "relay"),
+ ("end", "", ""),
+ ("group", "or on telegram", "no call needed"),
+ ("telegram", "you", "stop notice"),
+ ("you", "telegram", "replies to it"),
+ ("telegram", "bridge core", "inbound text, naming the anchor's session"),
+ ("end", "", ""),
+ ("bridge core", "session", "answer relay"),
+]
+STEP = 40
+
+
+def sequence() -> Canvas:
+ rows = []
+ y = 136
+ for item in STOP:
+ if item[0] == "group":
+ y += 14
+ rows.append((y, item))
+ y += 34
+ elif item[0] == "end":
+ rows.append((y - 16, item))
+ y += 18
+ else:
+ rows.append((y, item))
+ y += STEP + (16 if item[0] == item[1] else 0)
+ height = y + 20
+ c = Canvas(height, "What happens when a session stops")
+ c.text(40, 30, "gpt-voicecoding", size=11, cls="dim", anchor="start", spacing=1)
+ c.text(840, 30, "a session stops", size=11, cls="dim", anchor="end", spacing=1)
+
+ for name, x in COLUMN.items():
+ c.lines.append(f'')
+ cls = "core" if name == "bridge core" else "card"
+ if name == "bridge core":
+ c.glow(x - 52, 56, 104, 34)
+ c.card(x - 52, 56, 104, 34, name, cls=cls)
+
+ messages = [row for row in rows if row[1][0] not in ("group", "end")]
+ period = len(messages) * 0.9
+ opened = None
+ for y, (source, target, label) in rows:
+ if source == "group":
+ opened = (y, target, label)
+ continue
+ if source == "end":
+ top, name, note = opened
+ left, right = COLUMN["bridge core"] - 30, 860
+ c.lines.append(
+ f''
+ )
+ label_w = (len(name) + len(note)) * 7 + 24
+ c.add(f'')
+ c.text(left + 20, top + 4, name, size=12, cls="accent", anchor="start", weight=600)
+ if note:
+ c.text(
+ left + 20 + len(name) * 7.25 + 8,
+ top + 4,
+ note,
+ size=11,
+ cls="dim",
+ anchor="start",
+ )
+ continue
+ x1, x2 = COLUMN[source], COLUMN[target]
+ index = messages.index((y, (source, target, label)))
+ if source == target:
+ path = rounded([(x1, y), (x1 + 34, y), (x1 + 34, y + 16), (x1 + 4, y + 16)], 6)
+ length = 34 + 16 + 30
+ c.add(f'')
+ c.text(x1 + 44, y + 12, label, size=11, cls="dim", anchor="start")
+ else:
+ end = x2 - 4 * sign(x2 - x1)
+ path = f"M{x1},{y} L{end},{y}"
+ length = abs(end - x1)
+ c.add(f'')
+ mid = (x1 + x2) / 2
+ width = len(label) * 6.7 + 12
+ c.add(
+ f''
+ )
+ c.text(mid, y - 8, label, size=11, cls="dim")
+ c.pulse(path, length, index, len(messages), period)
+ return c
+
+
+def write(name: str, canvas: Canvas) -> None:
+ for mode, palette in (("light", LIGHT), ("dark", DARK)):
+ (IMAGES / f"{name}-{mode}.svg").write_text(canvas.render(palette))
+
+
+if __name__ == "__main__":
+ write("architecture", architecture())
+ write("session-stops", sequence())
diff --git a/docs/images/architecture-dark.svg b/docs/images/architecture-dark.svg
new file mode 100644
index 0000000..f7cbc46
--- /dev/null
+++ b/docs/images/architecture-dark.svg
@@ -0,0 +1,122 @@
+
diff --git a/docs/images/architecture-light.svg b/docs/images/architecture-light.svg
new file mode 100644
index 0000000..7b3b2ce
--- /dev/null
+++ b/docs/images/architecture-light.svg
@@ -0,0 +1,122 @@
+
diff --git a/docs/images/session-stops-dark.svg b/docs/images/session-stops-dark.svg
new file mode 100644
index 0000000..617a73f
--- /dev/null
+++ b/docs/images/session-stops-dark.svg
@@ -0,0 +1,124 @@
+
diff --git a/docs/images/session-stops-light.svg b/docs/images/session-stops-light.svg
new file mode 100644
index 0000000..9b255f7
--- /dev/null
+++ b/docs/images/session-stops-light.svg
@@ -0,0 +1,124 @@
+
diff --git a/docs/images/showcase.png b/docs/images/showcase.png
new file mode 100644
index 0000000..5ef3f54
Binary files /dev/null and b/docs/images/showcase.png differ
diff --git a/pyproject.toml b/pyproject.toml
index 1937ea6..998a704 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ description = "Voice-controlling terminal coding agents through a realtime voice
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.12"
-authors = [{ name = "okqixiaobao727-design" }]
+authors = [{ name = "Simon-AI-coding" }]
keywords = ["voice", "coding-agent", "claude-code", "codex", "realtime"]
classifiers = [
"Development Status :: 1 - Planning",
@@ -46,8 +46,8 @@ voice = ["aiortc>=1.9", "av>=12", "sounddevice>=0.4",
acceptance = ["telethon>=1.41"]
[project.urls]
-Homepage = "https://github.com/okqixiaobao727-design/GPT-VoiceCoding"
-Issues = "https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues"
+Homepage = "https://github.com/Simon-AI-coding/GPT-VoiceCoding"
+Issues = "https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues"
[project.scripts]
bridgectl = "gpt_voicecoding.cli:main"
diff --git a/scripts/install.sh b/scripts/install.sh
index 9c11923..cf7f969 100644
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -16,7 +16,7 @@ curl -fsIL --connect-timeout 10 --max-time 30 https://github.com >/dev/null 2>&1
curl -fsIL --connect-timeout 10 --max-time 30 https://pypi.org/simple/ >/dev/null 2>&1 ||
fail 'Allow HTTPS access to PyPI, then run this command again.'
-repository=https://github.com/okqixiaobao727-design/GPT-VoiceCoding
+repository=https://github.com/Simon-AI-coding/GPT-VoiceCoding
printf '%s\n' 'Preparing the source checkout…'
product=${repository##*/}
source_directory="$HOME/Library/Application Support/$product/source"
diff --git a/scripts/prompts/stock-overlay/review.md b/scripts/prompts/stock-overlay/review.md
index 32ca49b..8786976 100644
--- a/scripts/prompts/stock-overlay/review.md
+++ b/scripts/prompts/stock-overlay/review.md
@@ -1,6 +1,6 @@
# Stock-plus-Overlay probe review
-Prepared for [the stock-plus-overlay tracer task](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/299). Simon approved the revised Call Agent text on 2026-09-08. The current revisions have not been live-tested.
+Prepared for [the stock-plus-overlay tracer task](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/299). Simon approved the revised Call Agent text on 2026-09-08. The current revisions have not been live-tested.
Source: `openai/codex`, `rust-v0.153.4`, commit `3d2ee51ca2d5db578f328aa75e20aa22c0197c9a`, `codex-rs/prompts/templates/realtime/`. These began as probe inputs; on 2026-09-08 Simon had them installed in the shipping catalogue (`core/instructions/{voice,agent}.py`), whose rendered text is the two files here plus the two Overlay paragraphs recorded under **Installed** below.
@@ -104,7 +104,7 @@ The two measured runs kept `realtime_start.md` whole, added the Relayed Instruct
One Engine tools section adds the runtime invocation and shared calling rules, followed by one entry per tool. Each entry has one usage form from the seam's `USAGE` table and its decision rules. The repeated command card and separate “How you run them” section are removed.
-- `brief` owns current status, newest content and current decision details. This preserves the behavioral distinction required by [the wrong-tool bug](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/277), while Simon's new instruction supersedes its old split between the command card and the usage paragraph.
+- `brief` owns current status, newest content and current decision details. This preserves the behavioral distinction required by [the wrong-tool bug](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/277), while Simon's new instruction supersedes its old split between the command card and the usage paragraph.
- `history` owns requesting a page and requesting older pages. The tool already limits page size and returns a concrete `--before` continuation in `control_plane/commands.py:_history_lines`; the prompt uses that returned value instead of restating page size and calculating a minimum ordinal. Fetching more pages remains conditional on the user's request.
- `relay` owns the single Relayed Instruction rule and the meaning of delivery receipts. The older overlapping “do not add a decision” paragraph is removed because that constraint is in the Relayed Instruction rule.
- `approve` and `live` each own their purpose and invocation. The hang-up requirement appears only with `live`.
@@ -132,7 +132,7 @@ The Voice set renders with codex's headings (ADR 0018 as amended); the Call Agen
## The kept and corrected rows are rules
-Since [#300](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/300), every row above marked `kept` or `corrected` — excluding blank lines and headings, which state no obligation — carries a rule in `core/instructions/catalogue.py`: id `voice.stock.*` or `agent.stock.*`, `source` the codex tag, template and line (`codex@rust-v0.153.4:backend_prompt.md:25`), `gist` the line exactly as the generator renders it, which for a corrected row is the correction. The reason stays here; the catalogue holds no reason column. `removed` rows gain no rule and stay rows here alone: the catalogue records what is owed, not what was declined.
+Since [#300](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/300), every row above marked `kept` or `corrected` — excluding blank lines and headings, which state no obligation — carries a rule in `core/instructions/catalogue.py`: id `voice.stock.*` or `agent.stock.*`, `source` the codex tag, template and line (`codex@rust-v0.153.4:backend_prompt.md:25`), `gist` the line exactly as the generator renders it, which for a corrected row is the correction. The reason stays here; the catalogue holds no reason column. `removed` rows gain no rule and stay rows here alone: the catalogue records what is owed, not what was declined.
The generators claim those ids on the blocks that say them, and `InstructionSet` refuses a set that claims a stock line without carrying it word for word — so a line deleted from a block fails generation even though the block still claims every id it ever did. The tag lives in exactly one place, `catalogue.STOCK_TEXT_VERSION`.
diff --git a/src/gpt_voicecoding/locations.py b/src/gpt_voicecoding/locations.py
index 4227ca8..15326b6 100644
--- a/src/gpt_voicecoding/locations.py
+++ b/src/gpt_voicecoding/locations.py
@@ -10,7 +10,7 @@
So it lives here: a leaf that imports nothing from this package and that every
layer may import. A second spelling of one of these paths is the shape of
-[#47](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/47), where
+[#47](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/47), where
the control-plane socket path is built independently in Swift and in Python with
no test holding the two together.
diff --git a/src/gpt_voicecoding/private_socket.py b/src/gpt_voicecoding/private_socket.py
index d221950..d56dae3 100644
--- a/src/gpt_voicecoding/private_socket.py
+++ b/src/gpt_voicecoding/private_socket.py
@@ -14,7 +14,7 @@
that way, and on a loaded CI runner the codex adapter — which polls for the
socket file and checks its mode the instant it appears — landed inside the
window and refused a socket that was about to be made private
-([#116](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/116)).
+([#116](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/116)).
The refusal was correct. The bind was not.
So the mode comes from the umask at creation time, and the socket is never
diff --git a/tests/conftest.py b/tests/conftest.py
index daa9971..32815ae 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -59,7 +59,7 @@
live in `launchd_fake.py`, because a test module that imports a `conftest` by
name is importing whichever `conftest` reached `sys.path` first — and with the
`[acceptance]` extra installed that is `tests/acceptance/conftest.py`, not this
-one ([#93](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/93)).
+one ([#93](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/93)).
`tests/test_layout.py` holds the rule so it cannot come back.
"""
diff --git a/tests/launchd_fake.py b/tests/launchd_fake.py
index 7c22c5a..9f1186d 100644
--- a/tests/launchd_fake.py
+++ b/tests/launchd_fake.py
@@ -6,7 +6,7 @@
unique, because `tests/acceptance/` has one too. With only `[dev]` installed the
acceptance conftest is `importorskip`ped, its directory never reaches `sys.path`,
and the bare name resolves here. Install `[acceptance]` and it resolves *there*
-instead, and the whole unit suite dies at collection ([#93](https://github.com/okqixiaobao727-design/GPT-VoiceCoding/issues/93)).
+instead, and the whole unit suite dies at collection ([#93](https://github.com/Simon-AI-coding/GPT-VoiceCoding/issues/93)).
So the rule this file exists to keep: **a test module never imports a
`conftest`.** Anything two test files share lives in a module with a name of its