Compare commits

...
3 Commits
Author SHA1 Message Date
snowspeeder 9bdaea2408 Document engineering learnings and refresh handoff guidance 2026-09-19 14:40:24 -04:00
snowspeeder 85169d651c project: regenerate to include asset catalog resource phase 2026-09-12 17:27:00 -04:00
snowspeeder 7ac8a1c2fa brand: add Hope app icon (Lightwell aperture)
- 1024px icon from web/brand/hope-icon.svg (forced-dark, grid-tuned)
- fix xcodegen: xcassets was never in the generated project (no
  resources phase) — move it into sources with buildPhase: resources
- fix iconset manifest: idiom universal (universal-platform rejected
  by actool), single 1024 universal/ios entry
2026-09-12 17:26:57 -04:00
11 changed files with 162 additions and 13 deletions

No files matched your search

+6
View File
@@ -3,3 +3,9 @@
- On startup, read `docs/PROJECT_STATUS.md` before planning or changing this project. It is the current handoff report and records decisions, completed work, verification, deployment dependencies, and remaining tasks. - On startup, read `docs/PROJECT_STATUS.md` before planning or changing this project. It is the current handoff report and records decisions, completed work, verification, deployment dependencies, and remaining tasks.
- Keep `docs/PROJECT_STATUS.md` updated as work progresses, especially after implementation, testing, deployment, or changes to blockers and user decisions. Distinguish verified results from assumptions, and update it before handing off or committing material changes. - Keep `docs/PROJECT_STATUS.md` updated as work progresses, especially after implementation, testing, deployment, or changes to blockers and user decisions. Distinguish verified results from assumptions, and update it before handing off or committing material changes.
- If using XcodeBuildMCP, use the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools. - If using XcodeBuildMCP, use the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools.
- Read `docs/ENGINEERING_LEARNINGS.md` before changing session lifecycle, transcripts, Settings, tests, signing, or deployment. Preserve its behavioral invariants unless the user explicitly changes the requirement.
- Follow the latest explicit user decisions: keep Qwen 2.5; the embedded music player was removed. Do not restore canceled features from historical goals, plans, or test reports.
- Treat `project.yml` as the project-generation source. Update it and regenerate with XcodeGen when adding/removing sources, tests, or assets; verify resources are in the generated build phase.
- Keep offline UI tests isolated from normal preferences and the NAS. Run live tests only deliberately, and distinguish unit/simulator evidence from physical audio qualification.
- Date verification claims and record their scope/artifacts. A healthy container, successful token request, review PASS, or installed app does not prove a working voice conversation.
- Before deploying or pushing backend changes, inspect the sibling repository and its automation. An iOS commit does not publish backend patches; preserve existing NAS data, certificates, dirty checkouts, and rollback options. Never put credentials in logs, reports, or commits.
+1
View File
@@ -72,6 +72,7 @@ fail with a pin mismatch (not a network error).
## Docs ## Docs
- [docs/PROJECT_STATUS.md](docs/PROJECT_STATUS.md) — current handoff, decisions, and remaining work; read first. - [docs/PROJECT_STATUS.md](docs/PROJECT_STATUS.md) — current handoff, decisions, and remaining work; read first.
- [docs/ENGINEERING_LEARNINGS.md](docs/ENGINEERING_LEARNINGS.md) — regression invariants, diagnostic lessons, and deployment safeguards.
- [docs/VERIFICATION.md](docs/VERIFICATION.md) — test evidence and remaining qualification. - [docs/VERIFICATION.md](docs/VERIFICATION.md) — test evidence and remaining qualification.
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — live NAS configuration and rollback. - [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — live NAS configuration and rollback.
- [PLAN.md](PLAN.md) — phase-by-phase progress, verified facts, open items. - [PLAN.md](PLAN.md) — phase-by-phase progress, verified facts, open items.
Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

@@ -1,7 +1,8 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal-platform", "filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"
} }
+23
View File
@@ -16,6 +16,7 @@
64291BBC8D0DAD47E07E94E0 /* ConversationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C23305A6C55A83F8617D9D /* ConversationState.swift */; }; 64291BBC8D0DAD47E07E94E0 /* ConversationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C23305A6C55A83F8617D9D /* ConversationState.swift */; };
70E4B860147494D9657E8C01 /* VoiceLiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F6A697985B36B3A1A3F2AD5 /* VoiceLiveTests.swift */; }; 70E4B860147494D9657E8C01 /* VoiceLiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F6A697985B36B3A1A3F2AD5 /* VoiceLiveTests.swift */; };
720C26EC163AE46713B02DD8 /* TLSPinning.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B40076934C28BA4B92D808 /* TLSPinning.swift */; }; 720C26EC163AE46713B02DD8 /* TLSPinning.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B40076934C28BA4B92D808 /* TLSPinning.swift */; };
7A8CC686530A5920338EDF5A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 26922A8D6467BD51ED45B63B /* Assets.xcassets */; };
89B47754E5C39418BFD839E6 /* TranscriptHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A45228EF59186CFC219F2E /* TranscriptHistory.swift */; }; 89B47754E5C39418BFD839E6 /* TranscriptHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A45228EF59186CFC219F2E /* TranscriptHistory.swift */; };
8DE1363CC0119B5AF9E4E117 /* ConnectionTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4B9073EE8EAB51F8C55760 /* ConnectionTestView.swift */; }; 8DE1363CC0119B5AF9E4E117 /* ConnectionTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4B9073EE8EAB51F8C55760 /* ConnectionTestView.swift */; };
943BCE971A01F220FAE9E250 /* TaskPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85BACA267ADD462306C9920 /* TaskPanelView.swift */; }; 943BCE971A01F220FAE9E250 /* TaskPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85BACA267ADD462306C9920 /* TaskPanelView.swift */; };
@@ -55,6 +56,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0FC167F6658CFEC9546A7E9F /* AgentMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentMonitor.swift; sourceTree = "<group>"; }; 0FC167F6658CFEC9546A7E9F /* AgentMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentMonitor.swift; sourceTree = "<group>"; };
20A82F8996DEA166C94A7956 /* ServerReplacementCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerReplacementCoordinatorTests.swift; sourceTree = "<group>"; }; 20A82F8996DEA166C94A7956 /* ServerReplacementCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerReplacementCoordinatorTests.swift; sourceTree = "<group>"; };
26922A8D6467BD51ED45B63B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2A4C3CE2CA6FF828452803D1 /* Voice.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Voice.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2A4C3CE2CA6FF828452803D1 /* Voice.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Voice.app; sourceTree = BUILT_PRODUCTS_DIR; };
2E5DF207EDB787F63CB3A64D /* Voices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Voices.swift; sourceTree = "<group>"; }; 2E5DF207EDB787F63CB3A64D /* Voices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Voices.swift; sourceTree = "<group>"; };
2FED8976B60FBC13BC980FCB /* VoiceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceApp.swift; sourceTree = "<group>"; }; 2FED8976B60FBC13BC980FCB /* VoiceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceApp.swift; sourceTree = "<group>"; };
@@ -170,6 +172,7 @@
B3D0379BEBE1E5F4AA58DA2D = { B3D0379BEBE1E5F4AA58DA2D = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EB99F8CAC2A1FB46873C1E48 /* Resources */,
FCC61E9990B71498171F9D34 /* Sources */, FCC61E9990B71498171F9D34 /* Sources */,
DC8495F2B27E1770E6620A05 /* Tests */, DC8495F2B27E1770E6620A05 /* Tests */,
6735DFBC89D9A939C44D763A /* UITests */, 6735DFBC89D9A939C44D763A /* UITests */,
@@ -198,6 +201,14 @@
path = Tests; path = Tests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EB99F8CAC2A1FB46873C1E48 /* Resources */ = {
isa = PBXGroup;
children = (
26922A8D6467BD51ED45B63B /* Assets.xcassets */,
);
path = Resources;
sourceTree = "<group>";
};
F168B25F8B3C6F36B6E88228 /* Tasks */ = { F168B25F8B3C6F36B6E88228 /* Tasks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -261,6 +272,7 @@
buildConfigurationList = 67CE95EFE28F976D1CF1A59A /* Build configuration list for PBXNativeTarget "Voice_iOS" */; buildConfigurationList = 67CE95EFE28F976D1CF1A59A /* Build configuration list for PBXNativeTarget "Voice_iOS" */;
buildPhases = ( buildPhases = (
548DD373FE3F2A49C30AA9CB /* Sources */, 548DD373FE3F2A49C30AA9CB /* Sources */,
AD64D238C9844386DB9B2B44 /* Resources */,
F79CBCE3DCE9D63A26B167D9 /* Frameworks */, F79CBCE3DCE9D63A26B167D9 /* Frameworks */,
); );
buildRules = ( buildRules = (
@@ -317,6 +329,17 @@
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
AD64D238C9844386DB9B2B44 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7A8CC686530A5920338EDF5A /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
548DD373FE3F2A49C30AA9CB /* Sources */ = { 548DD373FE3F2A49C30AA9CB /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
+8 -2
View File
@@ -1,5 +1,9 @@
# NAS deployment — 2026-09-08 # NAS deployment — 2026-09-08
Historical deployment record, not a current health assertion. The iOS music player
was removed September 12; the media/AAC details below concern the retained backend,
not a current iOS feature. Reinspect server state before making deployment changes.
The `voice` container on `192.168.86.2` hosts LiveKit, the voice agent, The `voice` container on `192.168.86.2` hosts LiveKit, the voice agent,
token service, music API and nginx. The app connects to token service, music API and nginx. The app connects to
`https://192.168.86.2:9178`. Docker uses host networking and an `https://192.168.86.2:9178`. Docker uses host networking and an
@@ -34,5 +38,7 @@ containers or the music library; it does not include iOS AAC conversion.
`scripts/deploy-backend.sh` records the launch configuration and refuses to `scripts/deploy-backend.sh` records the launch configuration and refuses to
replace an existing `voice` container. Run it on the NAS only after building replace an existing `voice` container. Run it on the NAS only after building
the named image and explicitly handling the existing container. It reads the named image and explicitly handling the existing container. It reads
credentials on the NAS and never prints them. No source changes were pushed; credentials on the NAS and never prints them. The backend compatibility source was
future CI deployments must include the compatibility patch to preserve it. not pushed during that deployment; recheck the sibling repository before acting.
An iOS push does not publish it. Future backend CI deployments must include the
compatibility patch to preserve it.
+12 -5
View File
@@ -6,8 +6,15 @@ iPhone on the LAN** (`https://192.168.86.2:9178`) because mic input, WebRTC capt
and audio session behavior don't work in this simulator environment (CoreAudio `-4010`, and audio session behavior don't work in this simulator environment (CoreAudio `-4010`,
0 input channels). 0 input channels).
Setup: build with Xcode for a physical device (or `xcodebuild ... -destination`), install, Setup: recheck device availability and signing, then build/install with Xcode or
ensure the server cert pin is current (see README — re-export after any server restart). the installed XcodeBuildMCP skill workflow. The phone was paired and the Nearlight
build launched successfully on September 12; that does not prove today's availability
or a still-valid provisioning profile. Verify the server certificate pin after an
actual certificate rotation, not automatically after every container restart.
For each check, record date, source revision, device/iOS version, backend/model,
observed result and supporting logs. Leave unchecked items unqualified; installing
the app or passing a simulator state test is not equivalent to hearing a reply.
## 1. Mic input with real speech ## 1. Mic input with real speech
@@ -21,7 +28,7 @@ ensure the server cert pin is current (see README — re-export after any server
- [ ] While the agent is speaking, start talking → conversation continues without a - [ ] While the agent is speaking, start talking → conversation continues without a
"barge-in" glitch; state returns to listening/thinking as expected. "barge-in" glitch; state returns to listening/thinking as expected.
- [ ] Text mode: barge-in hint line visible while in text mode. - [ ] Switching to Type leaves microphone capture off and text messaging usable.
## 3. Wake lock (screen off) ## 3. Wake lock (screen off)
@@ -39,8 +46,8 @@ ensure the server cert pin is current (see README — re-export after any server
## 5. Mic permission denied fallback ## 5. Mic permission denied fallback
- [ ] Deny microphone permission in iOS Settings for the app, relaunch. - [ ] Deny microphone permission in iOS Settings for the app, relaunch.
- [ ] Start → session reaches **ready** despite mic failure (non-fatal); orange error - [ ] Start → session reaches **ready** despite mic failure (non-fatal); a visible
banner shows the mic error; text mode remains fully usable. error explains the problem and offers iPhone Settings recovery; Type remains usable.
## 6. General device sanity ## 6. General device sanity
+89
View File
@@ -0,0 +1,89 @@
# Engineering learnings
Updated 2026-09-19 from repository source, recorded test evidence, and prior debugging.
This is guidance, not a claim that hardware or server checks were rerun today.
Read [PROJECT_STATUS.md](PROJECT_STATUS.md) for the current product scope.
## Preserve behavior across UI changes
- Observe nested observable objects directly where their data is rendered. Watching
only the parent session does not guarantee transcript/state/task updates redraw.
- Keep one conversation session across tabs. Server replacement must remain
latest-wins and reject late callbacks from a torn-down session.
- Keep Settings presentation outside the replaced session's identity. Otherwise
saving a changed server can destroy the navigation stack and its success feedback.
- Treat normalization as a programmatic edit: trimming a saved URL must not clear
its own confirmation. Clear confirmation on the next genuine user edit instead.
- A restored transcript is reading history, not a request to jump to the bottom.
User scrolling, text selection, and VoiceOver row focus suspend auto-follow;
explicit **Latest** restores it. Test the reading position, not just row existence.
- Gate Send on transport readiness, reconnect state and pending sends. Retain an
unsent draft on failure. Type mode must not enable microphone capture.
- A ready room can still have a microphone failure. Show the actual error and its
recovery action without disabling the usable text path.
These are regression invariants of the current Nearlight implementation, not a
reason to restore the removed music UI or the canceled ChatGPT migration.
## Match evidence to the claim
- A token/TLS check is not an agent-join test; Docker health is not a Qwen-response
test; app installation is not proof of microphone capture or audible output.
- Offline UI fixtures must use isolated preferences and deterministic local failure
endpoints. Never reset production preferences to make a UI test pass.
- Keep live tests opt-in. Record which model/server was used, what was actually
asserted, skips, result bundles, and any cleanup of test-created state.
- Test repeated lifecycle sequences, not just one successful launch. Repeat runs
exposed an intermittent playback timeout that a single green run missed.
- After an interrupted tool call, inspect the existing process/result before
restarting. A lost tool handle does not imply the underlying test stopped.
- When UI snapshots expose no targets, cross-check screenshots and XCTest results;
this occurred despite a rendered app and was not sufficient evidence of a crash.
- Keep failed-run evidence. A later pass does not explain the earlier failure.
Source review and automated large-text checks also do not replace manual VoiceOver
or real-device speech/interruption testing.
## Device connection and signing
Diagnose the layers separately: USB detection → pairing/trust → developer/signing
readiness → install/launch → live voice behavior. An unlocked phone may still be
unpaired. Once USB detects it, investigate pairing rather than repeatedly blaming
the cable. Query fresh device details; do not reuse an old availability report.
The phone was eventually paired and the final Nearlight build installed/launched
on September 12. That supersedes the earlier Finder/pairing blocker. Recheck its
current availability and provisioning before the next run; the recorded personal
provisioning was short-lived. Do not reset privacy settings or erase a device as a
routine diagnostic step.
## Project generation and assets
`project.yml` is the source for `Voice.xcodeproj`; the scheme is `Voice_iOS`, bundle
ID `dev.shane.voice`. Adding an icon file alone is insufficient: the asset catalog
must also be included in the resource build phase. The September 12 follow-up
commits added the Lightwell icon and regenerated the project to include it.
Regenerate deliberately and review generated diffs alongside source changes.
## NAS deployment and historical audio findings
- Inspect actual containers, images, mounts and health before recreating anything.
A user concern about deletion is a reason to check, not proof of deletion.
- Preserve the persistent certificate. A container restart need not rotate it;
update the app pin only after verifying an actual certificate change.
- Stage clean deployment source separately from a dirty remote checkout. Retain
library/configuration mounts and a recoverable previous container.
- Record the deployed image and source patch. Cross-repository changes must be
published separately, with any push-triggered deployment reviewed first.
- Do not print environment files, API keys, or full credential-bearing configuration.
Historical only—the embedded player no longer exists in this app:
- WebM/Opus library downloads needed cached AAC for the iOS playback path tested.
Preserve originals and verify byte ranges/cache behavior separately from conversion.
- AVFoundation's resource-loader content type required a UTI, not an HTTP MIME type.
- Buffering an entire 6 MB response before returning audio reproduced an AVPlayer
timeout. Bounded 256 KB range delivery passed three subsequent live repetitions.
These findings do not establish support for every radio format or indefinite stream.
See [DEPLOYMENT.md](DEPLOYMENT.md) and [VERIFICATION.md](VERIFICATION.md) for recorded
deployment and test history. Do not reintroduce the client player to preserve history.
+12 -2
View File
@@ -1,6 +1,10 @@
# Project status and handoff # Project status and handoff
Last updated: 2026-09-12. Last updated: 2026-09-19 (documentation review; no new runtime qualification).
Reusable implementation, testing, device and deployment lessons are in
[ENGINEERING_LEARNINGS.md](ENGINEERING_LEARNINGS.md). September 12 test/install
results below are historical evidence, not a September 19 health check.
## Current direction ## Current direction
@@ -63,6 +67,10 @@ Hardware audio and interruption qualification remains open.
### Tests and tooling ### Tests and tooling
- Follow-up commits added the Lightwell app icon (`7ac8a1c`) and regenerated the
project to include its asset-catalog resource phase (`85169d6`). The qualification
results below predate those follow-ups; do not describe them as a fresh full run
of the current HEAD.
- Installed XcodeBuildMCP and its skills; used it for simulator build/run/test work. - Installed XcodeBuildMCP and its skills; used it for simulator build/run/test work.
- The current combined simulator run passed **50 tests, 0 failures**, with the - The current combined simulator run passed **50 tests, 0 failures**, with the
opt-in live suite skipped once. Five offline UI scenarios cover recovery, opt-in live suite skipped once. Five offline UI scenarios cover recovery,
@@ -97,7 +105,9 @@ SSH host: `[email protected]` (Unraid). App URL: `https://192.168.86.2:9178`.
**Important:** the backend patch and its test are in a different local repository: **Important:** the backend patch and its test are in a different local repository:
`/Users/shane/dev/hope-voice-api/web/media_server.py` and `/Users/shane/dev/hope-voice-api/web/media_server.py` and
`/Users/shane/dev/hope-voice-api/tests/test_ios_audio.py`. They were still uncommitted `/Users/shane/dev/hope-voice-api/tests/test_ios_audio.py`. They were still uncommitted
at this handoff. Committing/pushing `voice-ios` does **not** publish those files. at the earlier deployment handoff; their current sibling-repository status has not
been rechecked in this documentation update. Committing/pushing the iOS repository
does **not** publish those files.
The deployed image contains the patch, but a future backend CI rebuild can lose it The deployed image contains the patch, but a future backend CI rebuild can lose it
unless it is committed and deployed through that repository's workflow. Inspect unless it is committed and deployed through that repository's workflow. Inspect
that workflow before pushing: it may replace the live container. that workflow before pushing: it may replace the live container.
+7 -1
View File
@@ -1,5 +1,9 @@
# Verification — 2026-09-12 # Verification — 2026-09-12
Documentation reviewed 2026-09-19; no tests rerun for that update. These results
predate the subsequent app-icon and generated-resource-phase commits. See
[ENGINEERING_LEARNINGS.md](ENGINEERING_LEARNINGS.md) for reusable lessons.
## Scope ## Scope
Qwen 2.5 remains configured; the canceled subscription-login migration was not Qwen 2.5 remains configured; the canceled subscription-login migration was not
@@ -78,7 +82,9 @@ published by this iOS branch.
Type/layout combination, or strict conformance to every Nearlight design detail. Type/layout combination, or strict conformance to every Nearlight design detail.
Astra explicitly accepted the remaining visual/adaptive/presentation debt for Astra explicitly accepted the remaining visual/adaptive/presentation debt for
this personal release. this personal release.
- The final corrected binary on the physical iPhone while it remains unavailable. - A fresh build/install and regression run after the subsequent app-icon/resource
commits. The September 12 Nearlight install succeeded; current phone availability
and provisioning have not been rechecked here.
Follow [DEVICE_TESTING.md](DEVICE_TESTING.md) for hardware qualification and Follow [DEVICE_TESTING.md](DEVICE_TESTING.md) for hardware qualification and
[PROJECT_STATUS.md](PROJECT_STATUS.md) for current priorities and deployment [PROJECT_STATUS.md](PROJECT_STATUS.md) for current priorities and deployment
+2 -2
View File
@@ -14,11 +14,11 @@ targets:
platform: [iOS] platform: [iOS]
sources: sources:
- Sources - Sources
- path: Resources/Assets.xcassets
buildPhase: resources
dependencies: dependencies:
- package: LiveKit - package: LiveKit
product: LiveKit product: LiveKit
resources:
- path: Resources/Assets.xcassets
info: info:
path: Resources/Info.plist path: Resources/Info.plist
properties: properties: