@zerwiz/ymir 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/config/app-repos.yaml +38 -0
- package/.claude/settings.json +31 -0
- package/.codex/hooks.json +43 -0
- package/.cursor/hooks.json +33 -0
- package/.pi/extensions/README.md +39 -0
- package/.pi/extensions/gna-pi-watch.ts +15 -0
- package/.pi/extensions/lib/ro-assistant-layout.ts +96 -0
- package/.pi/extensions/lib/ro-operational-user-layout.ts +145 -0
- package/.pi/extensions/lib/ro-visibility.ts +99 -0
- package/.pi/extensions/lib/ro-working-ship.ts +243 -0
- package/.pi/extensions/lib/rodd-operational-input.ts +62 -0
- package/.pi/extensions/lib/skuld-branch-dispatch.ts +252 -0
- package/.pi/extensions/lib/skuld-branch-model-picker.ts +77 -0
- package/.pi/extensions/lib/vordr-sessionstart-supervisor.mjs +48 -0
- package/.pi/extensions/ro.ts +11 -0
- package/.pi/extensions/skuld-branch-supervision.ts +11 -0
- package/.pi/extensions/syn-turnend-guard.ts +11 -0
- package/.pi/mcp.json +16 -0
- package/.pi/mcp.json.example +11 -0
- package/.pi/npm/.gitignore +2 -0
- package/.pi/settings.json +8 -0
- package/.pi/shared/extensions/gna-pi-watch.ts +750 -0
- package/.pi/shared/extensions/herdr-agent-state.ts +257 -0
- package/.pi/shared/extensions/open-editor.ts +509 -0
- package/.pi/shared/extensions/ro.ts +504 -0
- package/.pi/shared/extensions/skuld-branch-supervision.ts +1535 -0
- package/.pi/shared/extensions/syn-turnend-guard.ts +683 -0
- package/.pi/shared/extensions/todo.ts +133 -0
- package/.pi/shared/extensions/ymir-subagents.ts +287 -0
- package/.pi/shared/extensions/ymir-well.ts +130 -0
- package/CHANGELOG.md +23 -0
- package/assets/Ymir.md +4654 -0
- package/assets/healer.html +717 -0
- package/assets/icon-family/family-sheet.png +0 -0
- package/assets/icon-family/hlidskjalf.svg +1 -0
- package/assets/icon-family/sessrumnir.svg +1 -0
- package/assets/icon-family/smidja.svg +1 -0
- package/assets/icon-family/ymir.svg +1 -0
- package/assets/mimirswell.html +880 -0
- package/assets/mock/README.md +16 -0
- package/assets/mock/feeds.ts +175 -0
- package/assets/mock/mock.ts +408 -0
- package/assets/reference/docs/agent-control.md +122 -0
- package/assets/reference/docs/apodex/Appdex_as_Orchestrator_Planner_Reviewer_Assessment.md +176 -0
- package/assets/reference/docs/apodex/Final Report(1).md +198 -0
- package/assets/reference/docs/apodex/Final Report(2).md +54 -0
- package/assets/reference/docs/apodex/Final Report(3).md +40 -0
- package/assets/reference/docs/apodex/Final Report(4).md +234 -0
- package/assets/reference/docs/apodex/Final Report(5).md +13 -0
- package/assets/reference/docs/apodex/Final Report.md +82 -0
- package/assets/reference/docs/architecture.md +392 -0
- package/assets/reference/docs/arm-pretool-check.md +247 -0
- package/assets/reference/docs/calm-mode-feasibility.md +541 -0
- package/assets/reference/docs/calm.md +59 -0
- package/assets/reference/docs/captain-hold-lifecycle.md +96 -0
- package/assets/reference/docs/cd-guard.md +165 -0
- package/assets/reference/docs/cmux-backend.md +133 -0
- package/assets/reference/docs/codex-app-backend.md +57 -0
- package/assets/reference/docs/configuration.md +924 -0
- package/assets/reference/docs/documentation-audiences.json +468 -0
- package/assets/reference/docs/documentation-audiences.md +28 -0
- package/assets/reference/docs/examples/crew-dispatch.json +26 -0
- package/assets/reference/docs/examples/process-event-extension/file-signal.mjs +96 -0
- package/assets/reference/docs/examples/process-event-extension/firstmate-extension.json +15 -0
- package/assets/reference/docs/examples/watched-tools.json +24 -0
- package/assets/reference/docs/examples/wedge-alarm +22 -0
- package/assets/reference/docs/extension-bindings.md +237 -0
- package/assets/reference/docs/fm-test-isolation-proof.json +37 -0
- package/assets/reference/docs/fm-test-isolation-proof.md +149 -0
- package/assets/reference/docs/fm-test-portable-shards.md +118 -0
- package/assets/reference/docs/gitlab-merge-watch.md +275 -0
- package/assets/reference/docs/herdr-backend.md +346 -0
- package/assets/reference/docs/orca-backend.md +84 -0
- package/assets/reference/docs/pi-supervision-branch-poster.svg +125 -0
- package/assets/reference/docs/pi-supervision-branch.md +96 -0
- package/assets/reference/docs/plans/agentic-engineering-workflow.md +395 -0
- package/assets/reference/docs/plans/agentic-factory-implementation-plan.md +615 -0
- package/assets/reference/docs/plans/command-factory-enhancement-plan.md +420 -0
- package/assets/reference/docs/plans/phase-1-layer3-program-design.md +134 -0
- package/assets/reference/docs/plans/phase-2-vertical-slices.md +100 -0
- package/assets/reference/docs/plans/phase-3-measurable-goals.md +106 -0
- package/assets/reference/docs/plans/phase-4-context-budget.md +106 -0
- package/assets/reference/docs/plans/phase-5-pre-mortem-confidence.md +100 -0
- package/assets/reference/docs/plans/phase-6-incident-to-agent.md +109 -0
- package/assets/reference/docs/remote-secondmates.md +268 -0
- package/assets/reference/docs/scripts.md +146 -0
- package/assets/reference/docs/sessionstart-nudge.md +115 -0
- package/assets/reference/docs/subagent-guard.md +383 -0
- package/assets/reference/docs/supervision-protocols/claude.md +27 -0
- package/assets/reference/docs/supervision-protocols/codex.md +16 -0
- package/assets/reference/docs/supervision-protocols/cursor.md +31 -0
- package/assets/reference/docs/supervision-protocols/grok.md +39 -0
- package/assets/reference/docs/supervision-protocols/opencode.md +17 -0
- package/assets/reference/docs/supervision-protocols/pi.md +33 -0
- package/assets/reference/docs/supervision-protocols/unknown.md +13 -0
- package/assets/reference/docs/tmux-backend.md +115 -0
- package/assets/reference/docs/trace-context.md +117 -0
- package/assets/reference/docs/turnend-guard.md +170 -0
- package/assets/reference/docs/verification/dispatch-auth.md +204 -0
- package/assets/reference/docs/verification/muse.md +221 -0
- package/assets/reference/docs/verification/process-event-sources.md +208 -0
- package/assets/reference/docs/verification/public-followup.md +119 -0
- package/assets/reference/docs/verification/runtime-backends.md +1022 -0
- package/assets/reference/docs/verification/stow-memory.md +53 -0
- package/assets/reference/docs/verification/supervision.md +534 -0
- package/assets/reference/docs/verification/trace-context.md +31 -0
- package/assets/reference/docs/voice-relay.md +295 -0
- package/assets/reference/docs/watcher-continuity.md +119 -0
- package/assets/reference/docs/wedge-alarm.md +39 -0
- package/assets/reference/docs/zellij-backend.md +114 -0
- package/assets/reference/index.html +1172 -0
- package/assets/reference/skills/afk/SKILL.md +241 -0
- package/assets/reference/skills/ahoy/SKILL.md +56 -0
- package/assets/reference/skills/ask-user-authority/SKILL.md +56 -0
- package/assets/reference/skills/bearings/SKILL.md +161 -0
- package/assets/reference/skills/bearings/assets/board-template.html +731 -0
- package/assets/reference/skills/bootstrap-diagnostics/SKILL.md +68 -0
- package/assets/reference/skills/captain-hold-lifecycle/SKILL.md +54 -0
- package/assets/reference/skills/decision-hold-lifecycle/SKILL.md +15 -0
- package/assets/reference/skills/diagnostic-reasoning/SKILL.md +53 -0
- package/assets/reference/skills/firstmate-codexapp/SKILL.md +110 -0
- package/assets/reference/skills/firstmate-codexapp/agents/openai.yaml +4 -0
- package/assets/reference/skills/firstmate-coding-guidelines/SKILL.md +127 -0
- package/assets/reference/skills/firstmate-orca/SKILL.md +92 -0
- package/assets/reference/skills/fmx-respond/SKILL.md +283 -0
- package/assets/reference/skills/harness-adapters/SKILL.md +95 -0
- package/assets/reference/skills/harness-adapters/references/common/control-and-recovery.md +37 -0
- package/assets/reference/skills/harness-adapters/references/common/dispatch.md +32 -0
- package/assets/reference/skills/harness-adapters/references/common/model-and-effort.md +42 -0
- package/assets/reference/skills/harness-adapters/references/common/primary-hooks.md +40 -0
- package/assets/reference/skills/harness-adapters/references/harness/claude.md +55 -0
- package/assets/reference/skills/harness-adapters/references/harness/codex.md +43 -0
- package/assets/reference/skills/harness-adapters/references/harness/cursor.md +75 -0
- package/assets/reference/skills/harness-adapters/references/harness/grok.md +69 -0
- package/assets/reference/skills/harness-adapters/references/harness/kimi.md +51 -0
- package/assets/reference/skills/harness-adapters/references/harness/muse.md +70 -0
- package/assets/reference/skills/harness-adapters/references/harness/opencode.md +42 -0
- package/assets/reference/skills/harness-adapters/references/harness/pi.md +56 -0
- package/assets/reference/skills/process-event-sources/SKILL.md +137 -0
- package/assets/reference/skills/project-management/SKILL.md +92 -0
- package/assets/reference/skills/quota-array-dispatch/SKILL.md +131 -0
- package/assets/reference/skills/secondmate-provisioning/SKILL.md +254 -0
- package/assets/reference/skills/stow/SKILL.md +309 -0
- package/assets/reference/skills/stuck-crewmate-recovery/SKILL.md +54 -0
- package/assets/reference/skills/updatefirstmate/SKILL.md +64 -0
- package/assets/skills/README.md +5 -0
- package/assets/yggdrasil.html +649 -0
- package/assets/ymir-banner-01.png +0 -0
- package/assets/ymir-banner-02.png +0 -0
- package/assets/ymir-banner-03.png +0 -0
- package/assets/ymir-banner-04.png +0 -0
- package/assets/ymir-banner-05.png +0 -0
- package/assets/ymir-banner-06.png +0 -0
- package/assets/ymir-emblem-darksteel.svg +63 -0
- package/assets/ymir-emblem-forged.svg +55 -0
- package/assets/ymir-emblem-slate.svg +55 -0
- package/assets/ymir-mark-algiz-anvil.svg +9 -0
- package/assets/ymir-stave.svg +53 -0
- package/assets/ymir.html +563 -0
- package/bin/ymir-install.sh +6 -1
- package/data/learnings.md +33 -0
- package/data/local-models.md +34 -0
- package/data/operator.md +15 -0
- package/data/projects.md +28 -0
- package/data/realm.md +1 -0
- package/deploy/Containerfile +27 -0
- package/deploy/README.md +104 -0
- package/deploy/compose/compose.yaml +45 -0
- package/deploy/env.example +30 -0
- package/deploy/quadlet/ymir.container +57 -0
- package/package.json +10 -8
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# The spoken interface
|
|
2
|
+
|
|
3
|
+
Talk to a voice agent that sits in front of the first mate. It answers questions
|
|
4
|
+
about what is happening from the first mate's own records, and when you ask for
|
|
5
|
+
real work it says so out loud and queues the request rather than pretending to
|
|
6
|
+
do it.
|
|
7
|
+
|
|
8
|
+
This is step one of three: a spoken round trip that works. Interrupting the agent
|
|
9
|
+
mid-sentence and carrying context from one question to the next are step three,
|
|
10
|
+
and [what this build does not do](#what-this-build-does-not-do) is explicit about
|
|
11
|
+
where the edge is.
|
|
12
|
+
|
|
13
|
+
## The shape
|
|
14
|
+
|
|
15
|
+
Your laptop captures the audio and plays the reply. This desktop holds the
|
|
16
|
+
conversation with the model. Nothing in between needs AWS credentials on the
|
|
17
|
+
laptop, which is the whole reason for this shape.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
laptop this desktop AWS
|
|
21
|
+
------ ------------ ---
|
|
22
|
+
microphone --> fm-voice-client.py --(ssh)--> fm-voice-relay.py --> Nova Sonic 2
|
|
23
|
+
speaker <------------------------------------------------- (your region)
|
|
24
|
+
|
|
|
25
|
+
+--> the first mate's records (read)
|
|
26
|
+
+--> fm-inbox.sh note (queue real work)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The two ends share one bidirectional byte stream over an SSH exec channel, so
|
|
30
|
+
audio and control travel together and need framing. `bin/fm_voice_frame.py` is
|
|
31
|
+
the owner of that format and is the only file both machines run.
|
|
32
|
+
|
|
33
|
+
The relay reads records and queues work. It never changes a project, and the
|
|
34
|
+
queueing half is `bin/fm-inbox.sh note`, the same surface the captain's own
|
|
35
|
+
out-of-band capture already uses, rather than a second queue.
|
|
36
|
+
|
|
37
|
+
## What it costs in time
|
|
38
|
+
|
|
39
|
+
Measured on 2026-08-21 against the reviewed relay code, `amazon.nova-2-sonic-v1:0` in `eu-north-1`, on a spoken question that makes the agent read the records before it can answer, which is the slowest ordinary case.
|
|
40
|
+
Six runs each, all six answered each way.
|
|
41
|
+
|
|
42
|
+
| Path | First audio out, seconds | Median |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| Direct from this desktop, no relay | 1.165 1.190 1.215 1.250 1.281 1.352 | 1.232 |
|
|
45
|
+
| Over the relay, real client and framing | 1.138 1.165 1.171 1.174 1.177 1.283 | 1.172 |
|
|
46
|
+
|
|
47
|
+
The clock starts the instant the captain stops speaking and stops when the first byte of reply audio arrives.
|
|
48
|
+
An earlier measurement of the same question, on the same model and region and also reading the records, put the direct path at 1.164 seconds median over five runs, and this control reproduces it to within the noise floor below.
|
|
49
|
+
That measurement is not published here, so read it as corroboration rather than as something to open: the direct column stands as a control on its own, because it was taken in the same pass, on the same clip, model, region and read scope, with only the relay removed.
|
|
50
|
+
|
|
51
|
+
**The relay's own cost is smaller than this measurement can resolve.**
|
|
52
|
+
The relay median lands below the direct control, which does not mean the relay is faster: two direct-control passes twenty minutes apart differ by 0.070 seconds of median, so that is the floor, and framing and the extra process hop are both under it.
|
|
53
|
+
The earlier measurement above independently agrees on that floor, spreading 0.087 seconds across its own five runs, and two measurements agreeing on the noise are worth more than one asserting it.
|
|
54
|
+
Read the two rows as the same number.
|
|
55
|
+
|
|
56
|
+
The first pass, on the relay as first written, put it 0.22 seconds behind the control, and that gap read as framing, the process hop and the per-turn reconnect.
|
|
57
|
+
It was none of them, and the difference is worth keeping, because a wrong number invites a re-measurement while a wrong cause invites a fix to the wrong part of the relay.
|
|
58
|
+
Each relay run is six turns in one session, so a per-turn defect shows up as a step: that pass stepped from 1.229 on turn one to a 1.447 median across turns two to six, and the same step appeared independently on the talk-end-to-tool-request mark, 0.599 rising to 0.730.
|
|
59
|
+
The re-measured passes are flat, stepping 0.009 and 0.021.
|
|
60
|
+
The 0.22 seconds was the relay resolving AWS credentials again for every turn's session, which review found and fixed: `Credentials` in `bin/fm-voice-relay.py` resolves once, and every later session reuses that answer, so a reconnect costs a reconnect.
|
|
61
|
+
This is the second time credential resolution has dominated a voice path's latency on a host like this one, because earlier prototype work measured the local credential helper at about a second per call and found that fixed per-call overhead exceeded the model's own cost.
|
|
62
|
+
So it is the first thing to suspect when a spoken path is slower than the model, and it is worth checking that anything new doing per-turn work resolves credentials once rather than once per session.
|
|
63
|
+
|
|
64
|
+
What the relay figure does NOT include, and could not be measured from here:
|
|
65
|
+
|
|
66
|
+
- **The SSH hop itself.**
|
|
67
|
+
These runs drove the relay as a local child process, which is the identical relay command with only the `ssh -T <host>` prefix omitted, so the client, the framing, the uplink ordering, the relay, the records read and the handover are all real and only the SSH subprocess is absent.
|
|
68
|
+
Two facts bound what its absence can be hiding.
|
|
69
|
+
A constant transport cost cannot produce the turn-by-turn step that the credential defect produced, and the first pass, which did run over `ssh localhost`, put its own first turn 0.009 seconds above its own direct control.
|
|
70
|
+
Neither of those is a measurement of the SSH path on this code, and neither is offered as one.
|
|
71
|
+
- **Your laptop's round trip to this desktop.**
|
|
72
|
+
Add roughly your own round trip time: the audio goes up and the reply comes back, so it lands about once.
|
|
73
|
+
- **Microphone capture and speaker output latency.**
|
|
74
|
+
This desktop has no microphone and no speaker, so every measurement used audio files.
|
|
75
|
+
The client reports both device figures in its own output, so your first live run measures them rather than guessing.
|
|
76
|
+
|
|
77
|
+
So your number is about 1.15 to 1.3 seconds plus your round trip time plus your audio devices.
|
|
78
|
+
It is worth saying plainly that this came in under the bottom of the 1.5 to 2.5 second estimate the relay shape was given before it was built.
|
|
79
|
+
The safer shape, with no credentials on the laptop, is not the slower one.
|
|
80
|
+
|
|
81
|
+
## Setting up this desktop
|
|
82
|
+
|
|
83
|
+
The model is only reachable over HTTP/2 bidirectional streaming, which the AWS
|
|
84
|
+
CLI cannot drive and `boto3` cannot either. It needs the experimental SDK, in a
|
|
85
|
+
virtual environment of its own:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
python3 -m venv ~/.fm-voice-venv
|
|
89
|
+
~/.fm-voice-venv/bin/pip install aws-sdk-bedrock-runtime
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then tell this home which account and model to use.
|
|
93
|
+
The relay carries no default for any of these, because a region, a model id and an AWS profile name somebody's account and somebody's choices, and inheriting those from whoever wrote the code is not a sensible way to start talking to a paid API.
|
|
94
|
+
Each value is one line in your gitignored `config/` directory, and each has an environment variable that overrides it for a single run.
|
|
95
|
+
|
|
96
|
+
| File | Environment | Holds |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `config/voice-region` | `FM_VOICE_REGION` | The Bedrock region to open the session in, required. |
|
|
99
|
+
| `config/voice-model` | `FM_VOICE_MODEL` | The Nova Sonic model id, required. |
|
|
100
|
+
| `config/voice-profile` | `FM_VOICE_PROFILE` | The AWS profile to export credentials from, optional: with no profile the relay uses only credentials that are already in its environment. |
|
|
101
|
+
| `config/voice-id` | `FM_VOICE_ID` | The output voice, optional and `matthew` when unset. |
|
|
102
|
+
|
|
103
|
+
A missing required value refuses with the path to write, so an unconfigured home cannot start the relay by accident, and that configuration is the whole opt-in.
|
|
104
|
+
`docs/configuration.md` is the registry for these files.
|
|
105
|
+
|
|
106
|
+
Check it end to end without a microphone, using a recorded question:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
cd <your firstmate home>
|
|
110
|
+
~/.fm-voice-venv/bin/python bin/fm-voice-relay.py --self-test <clip.pcm>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The clip is headerless 16000 Hz mono signed 16-bit little-endian PCM and must end
|
|
114
|
+
on speech, not silence. It prints one JSON line: what it heard, what it said, how
|
|
115
|
+
long each stage took, whether it answered at all, and, in `relay_error`, what
|
|
116
|
+
broke when a turn broke rather than merely going unanswered, so an
|
|
117
|
+
infrastructure failure is not read as a slow answer. Feed it a clip that
|
|
118
|
+
already ends in silence and it will tell you the timings are measured from the
|
|
119
|
+
wrong instant rather than printing a number that looks fast.
|
|
120
|
+
|
|
121
|
+
## Setting up the laptop
|
|
122
|
+
|
|
123
|
+
**The audio devices are not verified.** No worker can reach the captain's laptop, so neither the microphone nor the speaker has ever been opened.
|
|
124
|
+
Treat the first live run as their test, and expect the device setup to be where it fails.
|
|
125
|
+
Everything around them is exercised with files.
|
|
126
|
+
That includes the speaker's own byte accounting, the arithmetic deciding which turn a chunk of reply audio is credited to and whose first-audio clock it stamps, which runs against a stub stream in the test suite.
|
|
127
|
+
Covering that arithmetic says nothing about how a real output device behaves.
|
|
128
|
+
|
|
129
|
+
Copy the two files the laptop needs, and install the one dependency:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
scp <desktop>:<firstmate home>/bin/fm-voice-client.py .
|
|
133
|
+
scp <desktop>:<firstmate home>/bin/fm_voice_frame.py .
|
|
134
|
+
python3 -m pip install sounddevice
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`sounddevice` needs PortAudio, which on macOS is `brew install portaudio`. macOS
|
|
138
|
+
will ask for microphone permission for whichever terminal you run this from, once.
|
|
139
|
+
|
|
140
|
+
Then talk:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
python3 fm-voice-client.py --host <desktop> \
|
|
144
|
+
--relay <firstmate home>/bin/fm-voice-relay.py \
|
|
145
|
+
--relay-python ~/.fm-voice-venv/bin/python
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The client has no built-in idea of where the relay lives on your desktop, so `--relay` is required and `FM_VOICE_RELAY` sets it once for a shell.
|
|
149
|
+
|
|
150
|
+
Press Enter to start talking, press Enter again when you have finished. It prints
|
|
151
|
+
the timings for each turn as JSON on stdout and everything human on stderr, so
|
|
152
|
+
`--runs 5 > runs.jsonl` gives you your own spread to compare against the table
|
|
153
|
+
above.
|
|
154
|
+
|
|
155
|
+
Every record carries `relay_error`, which is null when nothing broke and otherwise names what did.
|
|
156
|
+
Where this end is left to infer what happened, it tells the two mid-turn failures apart, because they are not the same fault: a turn that got no reply audio at all says the connection ended, or was lost, or the relay stopped, or the session ended, before that turn was answered, while a turn whose answer had already started playing says the same thing happened before the reply finished.
|
|
157
|
+
The second still reads `answered: true`, because sound did reach you and `first_audio_s` is a real measurement of when.
|
|
158
|
+
Two other shapes carry neither clause, so do not read the pair above as the whole list: a fault the relay names itself arrives as the relay's own words, which point at the desktop and are kept unaltered because it knows what this end can only guess at.
|
|
159
|
+
A reason opening `this end could not handle the relay's reply` is the one that points at your laptop instead, so a healthy relay is not where to look for it.
|
|
160
|
+
|
|
161
|
+
The exit code is non-zero if any turn went unanswered, if any record carries a `relay_error`, or if the session stopped before it had taken the runs you asked for.
|
|
162
|
+
A truncated answer therefore fails the run rather than passing it, so a spread computed from `runs.jsonl` cannot quietly average an infrastructure failure into a latency figure.
|
|
163
|
+
|
|
164
|
+
If the audio devices are not the ones you want, `--input-device` and `--output-device` take a name or an index.
|
|
165
|
+
Neither the client nor this guide can yet tell you which device it resolved, so an unexpected device is diagnosed by trying the other name or index rather than by reading a log line.
|
|
166
|
+
If it fails before any audio, add `--verbose` and look for the handshake: a chatty login shell on the desktop printing to stdout is the one failure that looks like a protocol error and is not.
|
|
167
|
+
|
|
168
|
+
## What it may read
|
|
169
|
+
|
|
170
|
+
An unconfigured home gets the narrow scope: counts of what is in flight, what is waiting on the captain and what is open for review, with no identifier, title or link assembled at all.
|
|
171
|
+
Widening that is one line the captain of those records writes into `config/voice-read-scope` themselves.
|
|
172
|
+
Two whole classes of record are excluded at every scope, and excluded by construction rather than filtered on the way out:
|
|
173
|
+
|
|
174
|
+
- **Finished work in the backlog's done history**, because a spoken "what is
|
|
175
|
+
happening" answer is about open work, and old engagements accumulate there.
|
|
176
|
+
- **Free-form note bodies**, because they are written for someone with the whole
|
|
177
|
+
file in front of them, and they are where commercial detail gets quoted.
|
|
178
|
+
|
|
179
|
+
Only open work and this home's own runtime records are ever assembled.
|
|
180
|
+
A task keeps its runtime record until teardown, so the count of workers on deck
|
|
181
|
+
and the states beside it still include one whose item is already done; both are a
|
|
182
|
+
number and a state word, never anything written in a record.
|
|
183
|
+
Verified against the captain's live records on 2026-08-21: every occurrence of
|
|
184
|
+
the one customer identifier those records contain sits in finished work or a note
|
|
185
|
+
body, so nothing a status answer can say names a customer.
|
|
186
|
+
`tests/fm-voice-relay.test.sh` holds that boundary as an executable check, so
|
|
187
|
+
widening the reader later fails a test instead of quietly widening what is sent.
|
|
188
|
+
|
|
189
|
+
Two settings control it, both optional and both in `config/`:
|
|
190
|
+
|
|
191
|
+
| File | Effect |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `voice-read-scope` | `counts` (the default, and what an absent file means) sends counts only, with no record free text assembled at all. `full` sends counts plus the names, titles and pull request links of open work. |
|
|
194
|
+
| `voice-read-deny` | One plain case-insensitive substring per line; `#` comments. Each open item is matched once, against its identifier, its title, its tag values and its pull request link together, and a match is withheld from every list it could have appeared in and reduced to a count, so the agent still says how much is waiting without saying what it is. An absent file means an empty list. |
|
|
195
|
+
|
|
196
|
+
`voice-read-deny` exists so that one future open item carrying a customer name
|
|
197
|
+
can be excluded in a single line rather than by turning the feature off.
|
|
198
|
+
|
|
199
|
+
The wider scope is not free. Measured on 2026-08-21 on the same question, on the
|
|
200
|
+
relay as first written, so compare the two sides with each other rather than with
|
|
201
|
+
the table above: the wide answer is 2872 bytes against 445, and it costs both time
|
|
202
|
+
and consistency, at 1.348, 1.866 and 2.273 seconds against 1.351, 1.299 and 1.376.
|
|
203
|
+
If the spoken answer only ever needs to be "three jobs running, two decisions
|
|
204
|
+
waiting", `counts` is faster and steadier as well as narrower.
|
|
205
|
+
|
|
206
|
+
An unreadable or misspelled `voice-read-scope` refuses rather than falling back
|
|
207
|
+
to the wider setting, because falling back would widen what is sent on the
|
|
208
|
+
strength of a typo.
|
|
209
|
+
|
|
210
|
+
## Push to talk, and the setting that refuses
|
|
211
|
+
|
|
212
|
+
Push to talk is the default: the microphone is closed until you ask for it. That
|
|
213
|
+
is `$0.0101` per minute against `$0.0151` for an open microphone, and it is the
|
|
214
|
+
setting nobody has decided yet, so this build does not choose the expensive one
|
|
215
|
+
on the captain's behalf.
|
|
216
|
+
|
|
217
|
+
`--listen open-mic` exists as a setting and refuses at startup today.
|
|
218
|
+
An open microphone needs something to decide when you stopped speaking, and the client has no end-of-speech detection, so the mode would open a turn, stream audio forever and never mark a boundary, which leaves the relay appending to a session that has already answered.
|
|
219
|
+
That detection belongs with carrying context across turns, which is step three, so the flag refuses before it opens an SSH connection or spends anything rather than half working.
|
|
220
|
+
The setting stays where it is so that turning it on later is a small change rather than a new flag.
|
|
221
|
+
|
|
222
|
+
## One turn per session, and what that gives up
|
|
223
|
+
|
|
224
|
+
The relay reconnects to the model at the start of each turn. That is not
|
|
225
|
+
tidiness, it is a measured requirement.
|
|
226
|
+
|
|
227
|
+
A second question inside a session that has already answered one is treated as an
|
|
228
|
+
interruption, unconditionally: the model raises it the instant the audio block
|
|
229
|
+
opens. Waiting does not help. Six consecutive turns were tried with no wait, with
|
|
230
|
+
a wait until all the reply audio had arrived, and with a wait of the reply's full
|
|
231
|
+
spoken duration on top of that. Every one interrupted every second turn. Worse,
|
|
232
|
+
an interrupted turn that needs to read the records is lost outright: the model
|
|
233
|
+
asks for the records, takes them, and then never answers at all.
|
|
234
|
+
|
|
235
|
+
Reconnecting costs 0.02 seconds and happens while the captain is pressing the
|
|
236
|
+
talk key rather than while they are waiting for a reply, so it is invisible. With
|
|
237
|
+
it, six turns in a row all answered.
|
|
238
|
+
|
|
239
|
+
The same path covers a session the model ends on its own, mid-conversation: that
|
|
240
|
+
costs the turn it was in and not the relay, and the next talk key builds a
|
|
241
|
+
replacement. Either way the client hears about it at once rather than waiting out
|
|
242
|
+
the whole reply timeout in silence.
|
|
243
|
+
A turn still waiting for its answer when either happens names why in its own `relay_error`, and [setting up the laptop](#setting-up-the-laptop) describes those reasons.
|
|
244
|
+
|
|
245
|
+
**What it gives up is memory.** Every question starts fresh, so "and what about
|
|
246
|
+
that one" will not work. Carrying context across turns means handling
|
|
247
|
+
interruption properly, which is step three.
|
|
248
|
+
|
|
249
|
+
## Two traps worth keeping
|
|
250
|
+
|
|
251
|
+
Both cost real time to find the first time. The code comments own the detail;
|
|
252
|
+
these are the shapes.
|
|
253
|
+
|
|
254
|
+
1. **The end of a reply is not the event that says the reply ended.** The obvious
|
|
255
|
+
completion event never arrives on its own. The real end is the content-end
|
|
256
|
+
event carrying an end-of-turn reason.
|
|
257
|
+
2. **A clip with no trailing silence is never answered.** The model truncates it
|
|
258
|
+
and waits forever. The relay appends 400 ms of silence. Measured, this is a
|
|
259
|
+
content requirement and not a timing one: 0 ms and 100 ms were never answered,
|
|
260
|
+
while 200, 300, 400 and 800 ms all answered inside the same spread, because the
|
|
261
|
+
padding is sent as fast as the socket takes it. 400 ms is free margin above the
|
|
262
|
+
floor where answers start.
|
|
263
|
+
|
|
264
|
+
## What this build does not do
|
|
265
|
+
|
|
266
|
+
- **Interrupting the agent mid-sentence.** Nova Sonic supports it, measured, on
|
|
267
|
+
both model versions, so the capability is there when it is wanted. The concrete
|
|
268
|
+
thing step three has to solve is the interruption finding above: today any
|
|
269
|
+
second question in a session is treated as an interruption, and an interrupted
|
|
270
|
+
turn that reads the records produces no answer at all.
|
|
271
|
+
- **Remembering the last question.** See above.
|
|
272
|
+
- **Doing any project work.** Real work is queued for the first mate and the
|
|
273
|
+
agent says so out loud. It has no tool that changes a project.
|
|
274
|
+
|
|
275
|
+
## Cost
|
|
276
|
+
|
|
277
|
+
`$0.00293` per exchange, derived from the first pass's token counts and session seconds, which is roughly a dollar for three hundred and forty questions.
|
|
278
|
+
The re-measured exchange is about a quarter of a second shorter, worth about `$0.00004` at the session rate below, so the figure is unchanged at the precision it is quoted to.
|
|
279
|
+
Push to talk is `$0.0101` per minute of session against `$0.0151` with an open microphone.
|
|
280
|
+
|
|
281
|
+
Text in and out is materially dearer on this model version than the one it
|
|
282
|
+
replaces, so a long system prompt or a large record answer is a real cost as well
|
|
283
|
+
as a real delay. That is the second reason the reader caps its lists rather than
|
|
284
|
+
sending every row.
|
|
285
|
+
|
|
286
|
+
## Owners
|
|
287
|
+
|
|
288
|
+
| Concern | Owner |
|
|
289
|
+
| --- | --- |
|
|
290
|
+
| Wire format between the two machines | `bin/fm_voice_frame.py` |
|
|
291
|
+
| The relay, the model session, the tools | `bin/fm-voice-relay.py` |
|
|
292
|
+
| The laptop end, capture and playback | `bin/fm-voice-client.py` |
|
|
293
|
+
| What may be read, and queueing real work | `bin/fm_voice_records.py` |
|
|
294
|
+
| The queue the handover writes to | `bin/fm-inbox.sh` |
|
|
295
|
+
| The boundary as an executable check | `tests/fm-voice-relay.test.sh` |
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Watcher continuity
|
|
2
|
+
|
|
3
|
+
The watcher remains intentionally one-shot: one actionable reason closes one watcher cycle.
|
|
4
|
+
Must-work continuity now lives above that process boundary instead of depending on the model remembering a re-arm step.
|
|
5
|
+
|
|
6
|
+
## Ownership
|
|
7
|
+
|
|
8
|
+
Pi's `.pi/extensions/fm-primary-pi-watch.ts` and OpenCode's `.opencode/plugins/fm-primary-watch-arm.js` own continuous re-arm after an actionable child close.
|
|
9
|
+
Each adapter starts the next arm before delivering the wake prompt, checks current session-lock ownership at launch, preserves one child or scheduled retry at a time, and applies bounded exponential retry after an unexpected or failed close.
|
|
10
|
+
A failed follow-up never cancels continuity restoration.
|
|
11
|
+
Pi same-process session replacement follows the generation-owner contract in `.pi/extensions/fm-primary-pi-watch.ts`.
|
|
12
|
+
Cursor's `.cursor/hooks.json` `stop` hook (`bin/fm-turnend-guard-cursor.sh`) owns routine tokenless re-arm for a Cursor primary by parking that awaited hook on `bin/fm-watch-arm.sh` and returning an actionable close as one follow-up; [`turnend-guard.md`](turnend-guard.md#harness-integrations) owns its Pi-host stand-down, loop bounds, and supersession baton.
|
|
13
|
+
Claude's `.claude/settings.json` Stop `asyncRewake` hook (`bin/fm-claude-stop-autoarm.sh`) owns routine tokenless re-arm.
|
|
14
|
+
The hook fires on every Stop, and an eligible primary with supervision need admits one home-scoped owner that foregrounds `bin/fm-watch-arm.sh` inside the hook-owned process tree.
|
|
15
|
+
A numeric session-lock owner that fails the shared `fm_harness_pid_alive` predicate is reclaimed through `bin/fm-lock.sh` before auto-arm state changes, while a live owner, absent lock, or malformed lock keeps the competing hook inert.
|
|
16
|
+
The stale-owner claim occurs only after the existing AFK and supervision-need gates pass.
|
|
17
|
+
After each non-actionable arm close, the hook rechecks the identity-matched watcher lock and fresh beacon before retrying a bounded number of times.
|
|
18
|
+
A cycle-end failure is benign when that live-watcher predicate is true, and the hook suppresses the arm output and continues silently.
|
|
19
|
+
Only an exhausted failure with no verified watcher commits one last-resort notice for the continuous failure episode; a refused notice commit stays silent for a later retry, and after a successful notice later Stop cycles exit 2 without repeating it until the turn-end guard consumes the attended fail-open.
|
|
20
|
+
The Claude turn-end guard owns that notice commit contract, the monotonic failure progression, one-time attended fail-open, post-alarm continuation suppression, and positive recovery reset described in [`turnend-guard.md`](turnend-guard.md#harness-integrations).
|
|
21
|
+
While supervision is still needed and away mode remains inactive, an actionable close wakes the idle session through exit 2.
|
|
22
|
+
|
|
23
|
+
## Actionable wake ordering
|
|
24
|
+
|
|
25
|
+
After an actionable Pi or OpenCode child close, the adapter starts and verifies one singleton successor before it delivers the original wake.
|
|
26
|
+
It confirms the handling handoff against that successor before scheduling the follow-up, retries once against the current generation and successor, and treats a failed confirmation as a restoration failure: it classifies the error, retires a successor that is no longer alive, and surfaces exactly one typed message.
|
|
27
|
+
A failed confirmation is never swallowed.
|
|
28
|
+
It waits at most one readiness timeout per attempt, then sends TERM and waits a bounded retirement confirmation before the next lock-verified exponential retry.
|
|
29
|
+
If the unready arm does not retire within that bound, the adapter keeps ownership, starts no overlapping retry, and delivers the typed fallback immediately.
|
|
30
|
+
When that retained arm later closes, its actual close is classified as a new supervised event without replaying the earlier fallback.
|
|
31
|
+
After the configured retry bound is exhausted, it delivers the original wake with a typed continuity-restoration failure even if every successor arm hung without reporting readiness.
|
|
32
|
+
This is deliberate Option B ordering: the fleet is protected before the model handles the wake whenever restoration succeeds, but the model is never left blind when it does not.
|
|
33
|
+
|
|
34
|
+
Claude's Stop hook starts the successor arm at the next Stop after the handling turn, rather than before notification as Pi and OpenCode do.
|
|
35
|
+
The durable wake queue preserves actionable events during the residual active-turn window, and the bounded turn-end guard enforces recovery at Stop when no watcher is live and no open generation claim is still deciding, so a finished, hung, or identity-mismatched claim cannot suppress it ([`turnend-guard.md`](turnend-guard.md#harness-integrations) owns that boundary).
|
|
36
|
+
The recovery-episode contract below owns once-per-generation announcement.
|
|
37
|
+
A handling successor does not re-announce; it enters its poll loop immediately and keeps scanning signals, stale panes, and checks.
|
|
38
|
+
The model no longer re-arms after ordinary wakes.
|
|
39
|
+
No PreToolUse hook denies fleet commands based on watcher status.
|
|
40
|
+
A genuine auto-arm failure describes the automatic mechanism as broken and never directs a routine manual background arm.
|
|
41
|
+
Terminal arm-output classification (`started`, `attached`, or `FAILED`) remains defense in depth for the manual recovery path.
|
|
42
|
+
Codex retains its bounded foreground checkpoint protocol.
|
|
43
|
+
Grok retains its tracked background-task notification protocol.
|
|
44
|
+
No adapter starts a replacement with shell `&`.
|
|
45
|
+
|
|
46
|
+
The turn-end guard remains the final backstop rather than the normal continuity mechanism and cooperates with the auto-arm in its `--claude` mode.
|
|
47
|
+
|
|
48
|
+
## Recovery episode acknowledgement
|
|
49
|
+
|
|
50
|
+
A recovery episode is one generation of `state/.watcher-down`, and it is retired only by the generation-bound acknowledgement the drain prints as `WAKE_ACK_REQUIRED`.
|
|
51
|
+
An unacknowledged downtime generation is announced at most once: the first recovery marks that generation announced, and later arms wait until a new down stretch mints a new generation.
|
|
52
|
+
A non-successor watcher start after an announced-but-unacked episode is a new down stretch and mints a fresh generation so buried decisions still resurface once.
|
|
53
|
+
Every watcher close and every durable queue append publishes downtime, so a downtime republication of any pending episode reuses its generation instead of minting a new one, and an already-announced generation stays announced.
|
|
54
|
+
That reuse keeps a watcher close inside the handling window from orphaning the acknowledgement already presented and trapping later arms in repeated recovery presentation.
|
|
55
|
+
An acknowledgement carries two separable facts: queue-row consumption is bound to the monotonic `--ack-through` sequence (further scoped per actor - see "Per-actor acknowledgement" below), while only retiring the episode is bound to `--recovery-generation`.
|
|
56
|
+
A generation mismatch therefore does not block consumption of rows through that sequence; it is a non-fatal result that names its own remedy - re-drain, then acknowledge the newer episode.
|
|
57
|
+
The acknowledgement retires the marker only when no rows remain after sequence-bound consumption.
|
|
58
|
+
A concurrently appended wake has a higher sequence, remains queued, and keeps the episode pending for presentation.
|
|
59
|
+
Consequently, an empty-queue downtime publication during handling can be retired by the outstanding acknowledgement without a dedicated recovery turn.
|
|
60
|
+
An acknowledged episode does not freeze the generation, because the next downtime after it opens an episode of its own.
|
|
61
|
+
|
|
62
|
+
## Per-actor acknowledgement
|
|
63
|
+
|
|
64
|
+
`bin/fm-wake-drain.sh` consumes the queue per actor, not per whole-queue cutoff, using `bin/fm-lease-lib.sh`'s existing `fm_lease_actor` identity (`FM_SUPERVISION_ACTOR`, unset or `main` for every non-Pi harness and Pi's own main session; `branch` only inside the Pi supervision branch's own bash tool calls, injected deterministically by the extension - never agent memory).
|
|
65
|
+
Every presented row is claimed to exactly one actor under the durable queue lock.
|
|
66
|
+
Main records its presented set in `state/.main-eligible-rows`.
|
|
67
|
+
A branch grant is published through `bin/fm-wake-grant.sh` under that same lock in `state/.branch-eligible-rows`, bound to the live branch process and extension generation recorded in `state/.branch-eligible-owner`, and publication is refused if main already claimed any requested row.
|
|
68
|
+
A main drain validates that owner evidence under the queue lock and reclaims the grant when its process is gone or its identity no longer matches.
|
|
69
|
+
A main drain claims every currently unclaimed row and excludes an active branch grant from both presentation and acknowledgement.
|
|
70
|
+
Its `--ack-through <SEQ>` deletes only claimed main rows at or below the cutoff, while a branch acknowledgement deletes only claimed branch rows at or below its cutoff.
|
|
71
|
+
Every settled branch prompt releases any residual grant, so an omitted or failed acknowledgement leaves the durable row available to a later main drain; a successful acknowledgement has already removed it.
|
|
72
|
+
If a branch offer loses the claim race to main, it falls back to a main follow-up rather than assuming the earlier main delivery is still live.
|
|
73
|
+
[`pi-supervision-branch.md`](pi-supervision-branch.md#components-and-their-owners) owns branch eligibility, mixed-queue dispatch, the pre-drain recheck, and heartbeat's all-or-nothing rule.
|
|
74
|
+
A check-kind row is main-owned in every mode, including a heartbeat review, so it is never part of a branch claim and never defers one; main is woken for it on that check's own triggering close.
|
|
75
|
+
`fm-wake-drain.sh` never reclassifies a row itself: it filters the queue to the current actor's opaque claim before same-key deduplication, then presents and acknowledges only that actor-local view.
|
|
76
|
+
A missing or empty branch snapshot is refused loudly rather than read as "nothing eligible", because reaching the drain without the non-empty handoff promised by the extension is a wiring bug.
|
|
77
|
+
Because branch claims contain no check-kind rows, a branch acknowledgement skips check-specific receipt scans.
|
|
78
|
+
`tests/fm-wake-queue.test.sh`'s mixed-queue actor tests drive both directions against the real scripts: branch acknowledgement cannot swallow a main row, and a concurrent main turn cannot present or acknowledge an active branch grant.
|
|
79
|
+
`tests/fm-pi-branch-extension.test.sh` pins extension-side classification, claim publication and release, and the pre-drain recheck.
|
|
80
|
+
|
|
81
|
+
## Arm-layer cycle contract
|
|
82
|
+
|
|
83
|
+
`bin/fm-watch-arm.sh` never returns a clean empty success.
|
|
84
|
+
An actionable child output returns that reason normally.
|
|
85
|
+
A zero/empty child return rechecks the home lock and beacon, attaches to a verified healthy successor when one exists, or resolves the close against the watcher's bounded terminal-delivery ledger.
|
|
86
|
+
An attached arm follows verified identity-matched successors and resolves the same way when that chain ends without one, because it holds no handle on the watcher's stdout and cannot read the reason line itself.
|
|
87
|
+
Before releasing its singleton lock after printing an actionable reason, the watcher records that reason with its PID and process identity in `state/.watch-deliveries.log`.
|
|
88
|
+
A matching PID and identity lets an attached arm report the delivered reason and exit zero even after its durable wake was handled and acknowledged, while an unrelated queue producer or a recycled PID cannot satisfy the match.
|
|
89
|
+
Only a cycle with no matching delivery record emits `watcher: FAILED - cycle ended without an actionable reason` and exits nonzero.
|
|
90
|
+
|
|
91
|
+
The arm layer appends one tab-separated record per observed cycle to `state/.watch-cycle-exits.log`.
|
|
92
|
+
Each record includes arm and watcher PIDs, start and end timestamps, exit code and signal, classified reason, beacon age, lock identity before and after close, and successor disposition.
|
|
93
|
+
The file is size-capped through `FM_WATCH_CYCLE_LOG_MAX_BYTES` and `FM_WATCH_CYCLE_LOG_KEEP_LINES`.
|
|
94
|
+
`state/.watch-triage.log` remains only the watcher's bounded absorbed-wake debug log and carries no lifecycle semantics.
|
|
95
|
+
|
|
96
|
+
The default 300-second grace is unchanged.
|
|
97
|
+
Only the watcher process touches `state/.last-watcher-beat`; no helper process can make a wedged watcher appear healthy.
|
|
98
|
+
|
|
99
|
+
## Regression coverage
|
|
100
|
+
|
|
101
|
+
`tests/fm-pi-watch-extension.test.sh` checks Pi's first-cycle-or-explicit-repair tool metadata and ownership-based redundant-call no-ops, then simulates actionable and empty child closes against the actual Pi and OpenCode close handlers, blocks prompt delivery to prove the successor launches first, verifies single-flight behavior, changes the session lock before close to prove ownership is rechecked, and hangs each successor arm to prove bounded fallback delivery includes the typed restoration failure.
|
|
102
|
+
The same suite covers ordinary same-process session replacement for `/new`, `/resume`, and `/fork`, same-instance shutdown-plus-start, stale prior-generation callbacks, repeated transitions with exactly one live cycle, disappearance of the shutting-down refusal after a valid replacement activates, and terminal quit still refusing late rearm.
|
|
103
|
+
`tests/fm-watch-arm.test.sh` covers durable queue replay, real remote parent-replies ingestion into the authoritative status log, decision-only OPEN DECISIONS recovery, interrupted handling replay, generation-bound acknowledgement, a persistent live successor after recovery, a watcher close inside the handling window that must leave the printed acknowledgement valid, and the self-healing moved-generation acknowledgement that consumes its handled rows and names its remedy.
|
|
104
|
+
`tests/fm-watch-recovery-loop.test.sh` covers the once-per-generation announcement bound with the real Pi extension against a refused handling handshake, and a handling successor that must surface a real crew event instead of going blind.
|
|
105
|
+
`tests/fm-watcher-lock.test.sh` covers verified-successor attach, recovery publication before stale-lock removal, the typed self-eviction failure, bounded and successor-linked lifecycle rows, and a SIGSTOP counterfactual that distinguishes a live PID from a stale beacon before classifying termination.
|
|
106
|
+
`tests/fm-subagent-pretool-check.test.sh` proves Claude retains only the non-status Bash seatbelts.
|
|
107
|
+
`tests/fm-claude-stop-autoarm.test.sh` covers the auto-arm's scope, stale and live session owners, unchanged AFK and need boundaries, single-flight, bounded failure retries, benign live-watcher cycle ends, one-notice failure episodes, and exit-2 translation.
|
|
108
|
+
It also covers generation-claim single-flight, stuck-claim supersession, superseded-owner silence, notice-marker refusal and retry, ownership-atomic episode reset, and the legacy upgrade shim; [`turnend-guard.md`](turnend-guard.md) owns those behavior contracts.
|
|
109
|
+
`FM_CLAUDE_LIVE_E2E=1 tests/fm-claude-stop-autoarm-live-e2e.test.sh` starts with the reproduced stale-lock state, runs session start first, completes two tokenless cycles, and checks the competing-live-owner negative control.
|
|
110
|
+
`tests/fm-turnend-guard.test.sh` covers the cooperative `--claude` guard, including monotonic failed-epoch progression, the integrated bounded fail-open, post-alarm continuation suppression, and positive recovery reset; [`turnend-guard.md`](turnend-guard.md#regression-coverage) lists that suite's full generation and legacy claim coverage.
|
|
111
|
+
|
|
112
|
+
## Active limits and verification
|
|
113
|
+
|
|
114
|
+
The goal is continuity without a Pi or OpenCode model-memory re-arm step.
|
|
115
|
+
No zero-latency guarantee is claimed because lock verification, watcher startup, and bounded retry delays remain deliberate safety work.
|
|
116
|
+
OpenCode support targets persistent TUI sessions rather than headless `opencode run`.
|
|
117
|
+
Claude depends on the Stop `asyncRewake` rewake, Cursor depends on its awaited stop-hook park, Grok retains native background-completion notifications, and Codex retains bounded foreground checkpoints.
|
|
118
|
+
|
|
119
|
+
[`verification/supervision.md`](verification/supervision.md#watcher-continuity) records the current five-harness live evidence, the 2026-07-24 Stop-owned Claude auto-arm results, and exact opt-in commands.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Away-mode injection wedge alarm
|
|
2
|
+
|
|
3
|
+
The away-mode sub-supervisor (`bin/fm-supervise-daemon.sh`) buffers escalations and injects them into Firstmate's own pane.
|
|
4
|
+
When injection cannot confirm a submit past `FM_MAX_DEFER_SECS`, `inject_wedge_alarm` raises a loud, rate-limited alarm so the stall never stays invisible.
|
|
5
|
+
The active alert is pane-independent because a tmux status-line flash has no cross-backend equivalent and cannot reach an unattended captain reliably.
|
|
6
|
+
The durable marker and tmux flash remain as additional signals.
|
|
7
|
+
|
|
8
|
+
## Channels
|
|
9
|
+
|
|
10
|
+
`config/wedge-alarm` is local and gitignored.
|
|
11
|
+
It lists channel directives, one per non-empty, non-comment line, and every listed non-`off` channel fires best-effort.
|
|
12
|
+
`FM_WEDGE_ALARM_CHANNEL` overrides the file with one directive for focused testing.
|
|
13
|
+
|
|
14
|
+
- `off` disables every active alert while retaining the durable marker and tmux flash.
|
|
15
|
+
- `auto` or `default` resolves to `osascript` on macOS.
|
|
16
|
+
Other platforms have no built-in OS channel, so configure `command:` when a durable marker alone is insufficient.
|
|
17
|
+
- `osascript` posts a macOS Notification Center banner outside the terminal pane.
|
|
18
|
+
- `herdr` calls `herdr notification show` outside the supervised pane.
|
|
19
|
+
- `command:<cmd>` runs `<cmd>` through `sh -c` with the alarm summary as `$1` and on stdin, allowing delivery to a phone or pager service.
|
|
20
|
+
|
|
21
|
+
An absent `config/wedge-alarm` behaves as `auto`, which is default-on on macOS.
|
|
22
|
+
This is deliberate because the alarm fires only after a genuine max-defer wedge and is rate-limited to at most once per max-defer window.
|
|
23
|
+
|
|
24
|
+
Each channel is best-effort.
|
|
25
|
+
A missing binary or non-zero exit logs a warning and continues to the next channel without crashing the daemon loop.
|
|
26
|
+
Every invocation is process-group bounded by `FM_WEDGE_ALARM_TIMEOUT_SECS`, which defaults to 10 seconds, including `command:`, `osascript`, `herdr`, and the test seam.
|
|
27
|
+
On timeout or daemon shutdown, the notifier process group is terminated and the next configured channel may run.
|
|
28
|
+
AppleScript receives the summary as an argv item rather than interpolated source, so summary text cannot alter the script.
|
|
29
|
+
See [`examples/wedge-alarm`](examples/wedge-alarm) for a copyable config.
|
|
30
|
+
|
|
31
|
+
## Test safety
|
|
32
|
+
|
|
33
|
+
Every notifier routes through `FM_WEDGE_ALARM_EXEC` in `wedge_alarm_emit`.
|
|
34
|
+
When the daemon is sourced as a library, that seam defaults to `discard`, so a test cannot accidentally post a real notification.
|
|
35
|
+
`tests/wake-helpers.sh` replaces it with a recorder when a suite needs to assert channel selection and summary propagation.
|
|
36
|
+
Production leaves the seam unset and uses the configured real channels.
|
|
37
|
+
|
|
38
|
+
`tests/fm-daemon.test.sh` covers directive parsing, rate limiting, timeout and process-group cleanup, argv-safe dispatch, channel fallback, and safe `command:` summary delivery.
|
|
39
|
+
[`verification/supervision.md`](verification/supervision.md#wedge-alarm-channels) records the bounded manual macOS and Herdr channel proof.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Zellij runtime backend
|
|
2
|
+
|
|
3
|
+
Zellij is an experimental explicit-only session backend.
|
|
4
|
+
It provides the terminal session while Treehouse continues to provide task worktrees.
|
|
5
|
+
[`configuration.md`](configuration.md#runtime-backend-configbackend--fm_backend) owns shared selection and metadata semantics.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Pick Zellij when you already use it as a terminal multiplexer and accept its current focus, liveness, and polling limits.
|
|
10
|
+
|
|
11
|
+
Prerequisites:
|
|
12
|
+
|
|
13
|
+
- Zellij 0.44 or newer.
|
|
14
|
+
- `jq` for JSON responses.
|
|
15
|
+
- The universal harness and toolchain requirements in [`configuration.md`](configuration.md#toolchain).
|
|
16
|
+
|
|
17
|
+
Select it with local `config/backend` containing `zellij`, `FM_BACKEND=zellij` for one launch, or an explicit request to Firstmate.
|
|
18
|
+
It is never auto-detected.
|
|
19
|
+
A spawn stops before creating a session or acquiring a worktree when Zellij or `jq` is missing or Zellij is below 0.44.
|
|
20
|
+
|
|
21
|
+
Firstmate uses one shared session named `firstmate` by default.
|
|
22
|
+
`FM_ZELLIJ_SESSION` can select another name for isolated verification.
|
|
23
|
+
Attach with:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
zellij attach <session-name>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Routine supervision does not require attachment.
|
|
30
|
+
Use `bin/fm-peek.sh <id>` and `FM_HOME=<home> bin/fm-send.sh <id> '<text>'` against the metadata-routed endpoint.
|
|
31
|
+
|
|
32
|
+
Verify setup by spawning a small task and confirming metadata contains `backend=zellij`, `zellij_session=`, `zellij_tab_id=`, and `zellij_pane_id=`.
|
|
33
|
+
|
|
34
|
+
## Task shape and home isolation
|
|
35
|
+
|
|
36
|
+
Every task receives one tab in the shared Zellij session.
|
|
37
|
+
The caller-facing label remains `fm-<id>`, while the visible title is home-scoped as `fm-<home-label>-<id>`.
|
|
38
|
+
The home label is `firstmate` or `2ndmate-<id>` plus a short stable hash of the resolved Firstmate root.
|
|
39
|
+
This prevents task-id collisions between a primary, secondmates, and separate Firstmate installations sharing one session.
|
|
40
|
+
|
|
41
|
+
Zellij does not enforce tab-name uniqueness, so the adapter performs its own duplicate check against the scoped title.
|
|
42
|
+
Create, recover, list, and cleanup paths all use the same scoped title owner in `bin/fm-backend-hometag-lib.sh`.
|
|
43
|
+
Moving a Firstmate installation changes its path hash and leaves old titles unmatched, consistent with worktree paths also becoming stale after a move.
|
|
44
|
+
|
|
45
|
+
A pre-home-tag task remains reachable through its recorded metadata only when exactly one live tab has the old unscoped title.
|
|
46
|
+
Multiple old tabs with the same title cause a refusal rather than a guess.
|
|
47
|
+
Bulk recovery never adopts unscoped legacy tabs because it has no safe home identity for them.
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
backend=zellij
|
|
51
|
+
window=<session>:<pane-id>
|
|
52
|
+
zellij_session=<session>
|
|
53
|
+
zellij_tab_id=<tab-id>
|
|
54
|
+
zellij_pane_id=<pane-id>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Recorded pane ids are numeric and are never trusted alone after a session recreation.
|
|
58
|
+
Metadata-routed operations also verify the owning tab's expected scoped or unambiguous legacy title.
|
|
59
|
+
An explicit raw `session:pane` target remains a pane-existence-only operator escape hatch.
|
|
60
|
+
|
|
61
|
+
## Current operation and safety
|
|
62
|
+
|
|
63
|
+
Zellij's CLI action commands return exit 0 even for missing sessions or panes.
|
|
64
|
+
The adapter therefore verifies session, terminal pane, and expected title before an operation and validates JSON or integer response shapes afterward.
|
|
65
|
+
A pane can still disappear between verification and the operation; downstream submit, worktree-discovery, and stale detection report that narrow race rather than treating exit 0 as success.
|
|
66
|
+
|
|
67
|
+
Every pane operation passes an explicit `--pane-id` because a new session can focus its release-notes plugin pane, whose numeric plugin id is in a separate namespace from terminal pane ids.
|
|
68
|
+
|
|
69
|
+
`pane_cwd` follows a top-level shell `cd` but not the foreground subshell opened by `treehouse get`.
|
|
70
|
+
Worktree discovery therefore sends begin and end markers around `pwd`, captures the marked block, and joins wrapped path lines.
|
|
71
|
+
This active probe is scoped to spawn-time worktree discovery and is not advertised as a general live-cwd API.
|
|
72
|
+
|
|
73
|
+
`new-tab` has no no-focus flag and temporarily focuses the created tab in attached clients.
|
|
74
|
+
The adapter records the previously active tab and immediately restores it with `go-to-tab-by-id`.
|
|
75
|
+
There is a narrow visible race between those calls that no current Zellij flag can remove.
|
|
76
|
+
|
|
77
|
+
An ordinary metadata-routed `fm-send.sh` text steer becomes a durable steering-inbox record, and only its best-effort constant doorbell passes through Zellij's submit machinery.
|
|
78
|
+
On the typed plane, literal send uses bracketed paste followed by a separate explicit Enter.
|
|
79
|
+
Before sending Enter, the adapter proves that the selected composer's normalized content changed by exactly the pasted text; an unreadable composer, a paste that lands elsewhere, or unrelated pane output fails without submitting.
|
|
80
|
+
The adapter supports `Enter`, `Esc`, and the one-argument key expression `Ctrl c` through the shared key vocabulary.
|
|
81
|
+
Zellij exposes no cursor-row or native agent-state signal, but `dump-screen --ansi` (verified at 0.44.0) preserves styling, so the composer is read through the same fleet-wide classifier as tmux and herdr (`bin/fm-composer-lib.sh`), with ghost and placeholder text stripped before the verdict.
|
|
82
|
+
Submit acknowledgement requires a positively classified empty composer.
|
|
83
|
+
The retired content-delta acknowledgement could report a message delivered whenever the pane changed for any reason - a spinner, streaming output, a clock - which could silently close a decision record for a message the crew never received; a pane that merely changed no longer confirms anything.
|
|
84
|
+
A dead pane still fails safe: Zellij's unconditional-exit-0 actions dump nothing, and an empty dump classifies `unknown`, never a confirmation.
|
|
85
|
+
|
|
86
|
+
Viewport capture has no line-bound option.
|
|
87
|
+
Routine reads use `dump-screen` and larger peeks use `dump-screen --full`, followed by local trimming.
|
|
88
|
+
A short viewport may expose fewer lines than requested.
|
|
89
|
+
|
|
90
|
+
Closing a pane leaves an empty tab.
|
|
91
|
+
Cleanup resolves and verifies the owning tab, then uses `close-tab-by-id` so both the task pane and tab disappear.
|
|
92
|
+
Real test cleanup uses only an isolated non-`firstmate` session and the guard in `tests/zellij-test-safety.sh`; it never calls all-session deletion commands.
|
|
93
|
+
|
|
94
|
+
## Active limits
|
|
95
|
+
|
|
96
|
+
- Zellij is experimental and explicit-only.
|
|
97
|
+
- All homes share one session and tab bar; scoped titles prevent cross-home identity collisions but do not create per-home visual containers.
|
|
98
|
+
- There is no native busy or push-event signal, so supervision uses capture/hash polling for screen changes and each harness adapter's semantic lifecycle for worker state.
|
|
99
|
+
Grok alone retains its isolated rendered-tail fallback.
|
|
100
|
+
- There is no verified agent-process liveness signal, so a dead Zellij secondmate is reported inconclusive rather than auto-respawned.
|
|
101
|
+
- New-tab focus restoration has a narrow visible race.
|
|
102
|
+
- CLI exit status is not meaningful; a target can still disappear after structural readiness checks.
|
|
103
|
+
- Worktree cwd discovery requires the spawn-time marker probe.
|
|
104
|
+
- An ambiguous unscoped legacy title requires manual cleanup and respawn.
|
|
105
|
+
|
|
106
|
+
## Regression entry points
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
tests/fm-backend-zellij.test.sh
|
|
110
|
+
tests/fm-backend-zellij-smoke.test.sh
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The real smoke test uses a unique session and guarded deletion.
|
|
114
|
+
[`verification/runtime-backends.md`](verification/runtime-backends.md#zellij) records the active CLI matrix and lifecycle evidence.
|