Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7be9144
F1: enable swift test + coverage + pre-commit in CI (#20)
CloudbrokerAz Apr 24, 2026
877676a
F2: URLProtocolStub + HTTP fixture helpers (#21)
CloudbrokerAz Apr 24, 2026
69acee3
F3: SecureStore + KeychainSecureStore + InMemory fake (#22)
CloudbrokerAz Apr 24, 2026
a2c7ac9
F4: Swift Testing + tag scheme (.fast / .slow / .requiresHardware) (#23)
CloudbrokerAz Apr 24, 2026
977bd21
F6: Topic Router refactor of AGENTS.md + .claude/references/ (#25)
CloudbrokerAz Apr 24, 2026
5d4b1d6
feat(session): in-memory SessionStore for transcript + draft note lif…
CloudbrokerAz Apr 24, 2026
e8b1d3a
chore(workflow): Gemini Code Assist + Opus-subagent mandate + session…
CloudbrokerAz Apr 24, 2026
73f25f4
feat(ci): structured CI summary → $GITHUB_STEP_SUMMARY for fast PR tr…
CloudbrokerAz Apr 24, 2026
592cdb2
fix(test): widen PendingWritesCounter wait budget 200ms → 500ms (#42)
CloudbrokerAz Apr 24, 2026
64e7cb0
chore(concurrency): PermissionService deinit nonisolated copy + Audio…
CloudbrokerAz Apr 24, 2026
e0860ac
chore(concurrency): scope Core Audio pointer lifetimes via withUnsafe…
CloudbrokerAz Apr 24, 2026
b5f5de0
chore(concurrency): MainActor.assumeIsolated on main-queue / main-run…
CloudbrokerAz Apr 24, 2026
e47e18f
feat(taxonomy): placeholder manipulations JSON + ManipulationsReposit…
CloudbrokerAz Apr 24, 2026
3f67013
feat(prompt): ClinicalNotesPromptBuilder + RawLLMDraft + soap_v1 temp…
CloudbrokerAz Apr 24, 2026
fc16eb2
chore(ci): bump actions/* to Node.js 24 majors (#48)
CloudbrokerAz Apr 24, 2026
32a2a7f
chore(claude): drop unused skill dirs + tighten lint-config deny rule…
CloudbrokerAz Apr 24, 2026
cb8123f
chore(ci): bump runner image macos-14 → macos-15 (Sequoia) (#50)
CloudbrokerAz Apr 24, 2026
adc3db6
feat(llm): LLMProvider protocol + LLMOptions + MockLLMProvider fake (…
CloudbrokerAz Apr 24, 2026
7cbcb78
feat(notes): ClinicalNotesProcessor — transcript → StructuredNotes (r…
CloudbrokerAz Apr 25, 2026
26155ca
feat(cliniko): KeychainCredentialStore + Cliniko API key settings UI …
CloudbrokerAz Apr 25, 2026
cbf8927
feat(cliniko): Networking layer (URLSession actor + typed Cliniko err…
CloudbrokerAz Apr 25, 2026
495f80b
feat(cliniko): patient search + appointment picker (#9)
CloudbrokerAz Apr 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 86 additions & 5 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,108 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## 🚨 Read this first — every session

**Before responding to the user's first message in any new session, you MUST:**

1. **Read `AGENTS.md` at the project root** — `/Users/aarone/Documents/repos/mac-speech-to-text/AGENTS.md` on the host, `/workspace/AGENTS.md` in the devcontainer. It holds the Correctness Checklist (always/never rules), the Topic Router for `.claude/references/*.md`, and the tech-stack + commands reference. The `@../AGENTS.md` import below is defence-in-depth; if it resolves, great — **either way, read the file explicitly with the `Read` tool before any substantive work.**
2. **Read any `.claude/references/<topic>.md` files relevant to the task** — concurrency, testing conventions, PHI, Cliniko, MLX, menu-bar — per the Topic Router in AGENTS.md. Do not load all of them; load only what the task needs.
3. **Pick up the GitHub issue(s) you've been asked to work on** with `gh issue view <N> -R CloudbrokerAz/mac-speech-to-text`.

If the user asks "did you read AGENTS.md?" at any point in a session, the correct answer is "yes, I read it at session start" — not "let me check now." This checklist is the contract that makes that true.

@../AGENTS.md

**Project Type**: macOS native application for local speech-to-text capture
**Language**: Swift 6.x (compiler) with Swift 5.9 language mode (Package.swift)
**Platform**: macOS 14+ (minimum), macOS 26+ (development)

## Primary Reference
## Current initiative (last updated 2026-04-24)

**Clinical Notes Mode** — extend this app into a local-first clinical documentation assistant for chiropractors. Record consultation → local LLM (MLX Swift + Gemma 3 4B-IT v1; Gemma 4 E4B migration gated on ml-explore/mlx-swift#389) → structured SOAP notes → doctor review → Cliniko API export. 100% on-device. Session-only PHI. No cloud.

**Work is tracked as GitHub issues** in `CloudbrokerAz/mac-speech-to-text`. Always start a session by reading the open EPICs + any assigned issues:

```bash
gh issue list -R CloudbrokerAz/mac-speech-to-text --state open --label epic
gh issue view <N> --comments # for any specific issue you pick up
```

Please see the root `./AGENTS.md` in this same directory for the main project documentation and guidance.
**Two parallel EPICs**:
- **#19 — Testing + Workflow Framework** (children #20–#25). Must land first; unblocks feature work. Order: F1 → (F2, F3 parallel) → F4 → F6 → F5.
- **#1 — Clinical Notes Mode** (children #2–#18). Rides on top of #19 outputs.

### Operating rules (binding)

1. **Use Opus subagents liberally — and ALWAYS pass `model: "opus"` explicitly.** For any research/exploration spanning more than a couple of files, spawn parallel `Explore` / `general-purpose` agents with `model: "opus"` on every `Agent` tool call. Do not rely on the agent-definition default — several agents default to Sonnet and will silently downgrade if you omit the override. Review agents (`pr-review-toolkit:code-reviewer`, `pr-review-toolkit:comment-analyzer`, `pr-review-toolkit:silent-failure-hunter`, `pr-review-toolkit:type-design-analyzer`) run after substantive changes and **also** take `model: "opus"`. Keep the main thread for decisions and orchestration.
2. **Test everything.** Every new service gets a unit test; every new SwiftUI view gets a ViewInspector crash test; ReviewScreen + SafetyDisclaimer get snapshot tests. New pure-logic/async tests use **Swift Testing** (`@Test` / `#expect`) — see `Tests/SpeechToTextTests/Utilities/SwiftTestingExemplarTests.swift` for the canonical idiom. UI + ViewInspector stay on **XCTest**. Tag Swift Testing tests with `.fast` / `.slow` / `.requiresHardware` from `Tests/SpeechToTextTests/Utilities/TestTags.swift`; CI filters via `--skip-tag requiresHardware` where applicable. Acceptance criteria in every GH issue call out the test expectations.
3. **Talk to the tickets.** Three comment checkpoints per issue: (a) starting — plan + branch name, (b) PR opened — link + "awaiting CI", (c) **merged** — PR link + merge commit SHA + one-line summary. Link PRs with `Closes #N` so GitHub auto-closes. Also post an "unblocked by" comment on any downstream issue when its blocker merges. **Tick EPIC task-list checkboxes manually** when the child merges — GitHub only auto-ticks entries formatted as a bare `- [ ] #N`, which our EPICs usually aren't. **Always verify the post-merge main-branch workflow run** (`gh run list --branch main --limit 3`) before declaring a merge-batch done — the PR's CI and main's CI are separate runs. Keep discussion in GitHub, not in chat.
4. **Point to docs, don't duplicate.** In PRs, issue comments, and subagent prompts, reference `AGENTS.md` (and, once #25 lands, `.claude/references/*.md` — a topic-router split) instead of restating context inline.
5. **Security.** Never echo or reuse a GitHub PAT pasted in chat — `gh auth status` already has a valid token. Cliniko API keys live in Keychain only (#22 / #7); never logged, never in UserDefaults. PHI is in-memory only, plus the HTTPS body at the moment of POST to Cliniko — nowhere else (no logs, crash reports, audit files, or external tooling).
6. **Respect pre-commit.** `pre-commit run --all-files` must pass; SwiftLint is strict; gitleaks is on. The custom rules `observable_actor_existential_warning` and `nonisolated_unsafe_warning` stay honoured.
7. **Don't re-litigate locked decisions** (see below) without an explicit user ask.

### Locked technical decisions (2026-04-24)

| Area | Decision |
|---|---|
| LLM runtime | MLX Swift in-process (ml-explore/mlx-swift-examples) |
| LLM model v1 | Gemma 3 4B-IT (MLX 4-bit); swap to Gemma 4 E4B when mlx-swift#389 lands (#18) |
| Model delivery | Bundled in the .app (DMG distribution, not App Store) |
| Persistence | Session-only, cleared on export/quit — no on-disk PHI |
| Cliniko | API integration in v1, mirror patterns from [CloudbrokerAz/epc-letter-generation](https://github.com/CloudbrokerAz/epc-letter-generation/tree/main/Sources/Services) |
| Manipulations | Placeholder JSON v1 (#6); user supplies real Cliniko taxonomy later |
| UI entry | Settings toggle + "Generate Notes" action after recording |
| Review layout | Two-column (SOAP editor left, Manipulations + Excluded drawer right) — wireframe embedded in #13 |
| Safety | One-time "not a diagnostic tool" disclaimer, UserDefaults ack (#12) |
| Test frameworks | Mixed: Swift Testing (new) + XCTest (UI + ViewInspector) |
| HTTP mocking | Hand-rolled Sendable `URLProtocolStub` (#21) — zero deps |
| Keychain mocking | `SecureStore` protocol + `InMemorySecureStore` actor fake (#22) |
| LLM mocking | `MockLLMProvider` fast path; `RUN_MLX_GOLDEN=1` gated goldens nightly |
| Snapshot testing | `pointfreeco/swift-snapshot-testing` v1.17+ — scoped to ReviewScreen + Disclaimer only |
| Coverage | slather → `codecov-action@v5` on PR (#20) |
| CI gains (#20) | `swift test --parallel -enableCodeCoverage` + pre-commit/action; UI + hardware-dependent tests skipped in CI, run pre-push on remote Mac |

### Watch-list / blockers

- **ml-explore/mlx-swift#389** — Gemma 4 E4B architecture support. Migration tracked in #18.
- **Real Cliniko manipulations taxonomy** — user-supplied; placeholder in #6 for now.
- **Disclaimer copy legal review** — draft in #12; must be reviewed before ship.

### Reference projects

- FluidAudio SDK: https://github.com/FluidInference/FluidAudio
- Cliniko API: https://docs.api.cliniko.com/
- Cliniko integration reference: https://github.com/CloudbrokerAz/epc-letter-generation/tree/main/Sources/Services
- avdlee/swiftui-agent-skill (Topic Router pattern source): https://github.com/avdlee/swiftui-agent-skill

@/workspace/AGENTS.md
## Primary Reference

The root `AGENTS.md` (at the project root, one level up from this file) is the primary project documentation. The session-start checklist at the top of this file requires you to read it before doing any work — don't rely solely on the `@` import, as imports can silently fail when paths don't resolve (which is exactly what bit us when this file previously imported `@/workspace/AGENTS.md` from the host).

## Additional Component-Specific Guidance

For detailed module-specific implementation guides, also check for AGENTS.md files in subdirectories throughout the project.

These component-specific AGENTS.md files contain targeted guidance for working with those particular areas of the codebase.

## Important: Use Subagents Liberally
## Important: Use Subagents Liberally (and always Opus)

When performing any research, concurrent subagents can be used for performance and isolation.
Use parallel tool calls and tasks where possible.

**Mandatory:** every `Agent` tool call must pass `model: "opus"`. Several subagent definitions default to Sonnet and will silently downgrade if you omit the override. This applies to `Explore`, `general-purpose`, and every `pr-review-toolkit:*` reviewer.

## Code review pipeline (three layers)

For every non-trivial PR, exercise all three:

1. **Pre-PR (local):** spawn a `pr-review-toolkit:code-reviewer` subagent with `model: "opus"` over the diff before pushing. For PHI / concurrency / HTTP / Keychain work, also spawn `pr-review-toolkit:silent-failure-hunter` in parallel (and `pr-review-toolkit:type-design-analyzer` if new types are introduced). Apply blockers before pushing.
2. **Automated (on PR open):** **Gemini Code Assist** runs automatically via the GitHub App, driven by `.gemini/config.yaml` + `.gemini/styleguide.md` at the repo root. Address its inline comments as peer review. Re-trigger with a `/gemini review` comment if the PR materially changed. Gemini Code Assist docs: https://developers.google.com/gemini-code-assist/docs/review-github-code
3. **On-demand deep dive:** invoke the `/code-review` slash command (the `code-review:code-review` skill) for large, multi-subsystem, or comment-heavy PRs. It operates on the PR surface (including review comments) rather than the local diff.

"Non-trivial" = any diff touching PHI, concurrency, HTTP, Keychain, `@Observable`, actors, or >~30 lines across Sources/. Pure test additions and doc-only changes can skip layer 1.

## Quick Reference: Project Structure

```
Expand Down Expand Up @@ -229,7 +310,7 @@ For CI or when developing on non-macOS:

### Concurrency Safety Patterns

**CRITICAL**: Review `docs/CONCURRENCY_PATTERNS.md` before writing concurrency code.
**CRITICAL**: Review [`.claude/references/concurrency.md`](references/concurrency.md) before writing concurrency code. The legacy `docs/CONCURRENCY_PATTERNS.md` now redirects there.

#### 1. @Observable + Actor Existential (EXC_BAD_ACCESS)
```swift
Expand Down
136 changes: 136 additions & 0 deletions .claude/references/cliniko-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Cliniko API Reference

> **Load this when:** writing or reviewing the Cliniko client layer
> (issue #8), the patient/appointment picker (#9), `treatment_note`
> export (#10), or the credential-management surface (#7). Also relevant
> when touching `AuditStore`.

Design reference for the Cliniko integration, informed by the similar
work in
[`CloudbrokerAz/epc-letter-generation`](https://github.com/CloudbrokerAz/epc-letter-generation/tree/main/Sources/Services)
and the [official Cliniko API docs](https://docs.api.cliniko.com/).

---

## Authentication

- **API key** (secret): stored in Keychain via the `SecureStore` protocol.
- `SecureStore` service identifier: `"com.speechtotext.cliniko"`.
- Account: `"api_key"`.
- Accessibility: `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` (no iCloud sync).
- **Shard / subdomain** (non-secret): stored in `UserDefaults`.
- Example values: `au1`, `au2`, `au3`, `au4`, `uk1`, `uk2`, `ca1`, `us1`.
- Used to build the base URL: `https://api.{shard}.cliniko.com/v1`.
- **Auth scheme**: HTTP Basic with `"{api_key}:"` (the empty password is
deliberate — Cliniko uses the API key as the username).
- **Required headers** (per Cliniko docs):
- `User-Agent: mac-speech-to-text/<version> (contact@example.test)` —
Cliniko requires a contact email; plumb through a setting if needed.
- `Accept: application/json`.

---

## Error mapping

`ClinikoClient.send(…)` must surface typed errors, not raw `URLError`:

| Cliniko response | Swift error |
|---|---|
| `401` | `.unauthenticated` — API key invalid or revoked. Route the user to the Cliniko settings sheet. |
| `403` | `.forbidden` — key is valid but lacks the needed scope / the practitioner can't see that patient. |
| `404` | `.notFound` — typed `.notFound(resource: Resource)` where `Resource` identifies patient / appointment / treatment_note. |
| `422` | `.validation(fields: [String: [String]])` — parse the error body. Show field-level messages in the UI. |
| `429` | `.rateLimited(retryAfter: TimeInterval)` — parse `Retry-After` header. The client should auto-retry (see Retry). |
| `5xx` | `.server(status: Int)` — retry per policy below. |
| Network / DNS / TLS | `.transport(Error)` — wrap the underlying error, don't swallow it. |

---

## Retry policy

- **Idempotent reads** (GET): up to 2 retries on 5xx / transport, exponential backoff (1s, 2s).
- **Writes** (POST/PATCH on `treatment_notes`): **no auto-retry on 5xx** to avoid duplicate notes. Surface the error; let the user re-confirm.
- **429**: honour `Retry-After`, up to 2 retries. UI shows a countdown.
- Retries live inside the `ClinikoClient` actor so callers don't have to think about it. 2 retries max across the whole stack.

---

## Redaction rules (PHI)

Logging around Cliniko calls must follow the PHI rules from
[`phi-handling.md`](phi-handling.md). Summary for this client:

- **OK to log**: HTTP method, path template (`/patients/:id`, not `/patients/12345`), status, latency, typed error case.
- **NEVER log**: request body, response body, patient first/last name, DOB, `treatment_note` content, API key (not even obfuscated), subdomain/shard (low-sensitivity but not needed).
- `OSLog` privacy annotation: default to `privacy: .private` for anything the code doesn't strictly own. `privacy: .public` is reserved for structural values (status, method, path template, error case name).

---

## Endpoints in scope for v1

| Endpoint | Method | Purpose | Issue |
|---|---|---|---|
| `/users/me` | GET | "Test connection" in the Cliniko settings UI | #7 |
| `/patients?q={term}` | GET | Patient picker search (debounced 300 ms) | #9 |
| `/patients/{id}/appointments?from=…&to=…` | GET | List recent + today's appointments for the chosen patient | #9 |
| `/treatment_notes` | POST | Submit the generated SOAP note (+ optional `appointment_id`) | #10 |

Schema details belong in fixture files under
`Tests/SpeechToTextTests/Fixtures/cliniko/`, not this doc.

---

## Tenant template variability

Cliniko `treatment_notes` are template-driven. Different clinics may have
different field layouts (custom fields for "Manipulations used",
different section names). Two decisions:

1. **v1 approach**: post the SOAP note as a single markdown/HTML body
and let the clinic's template pull from it. This works for any
template without clinic-specific mapping code.
2. **Future**: a clinic-side configuration file maps our `StructuredNotes`
fields to a specific treatment-note template's custom fields. Deferred
until we have a real clinic to pilot with.

---

## Audit

Every successful export writes a metadata-only line to
`AuditStore` (Application Support, `audit.jsonl`):

```json
{
"timestamp": "2026-04-24T12:34:56Z",
"patient_id": "12345",
"appointment_id": "67890",
"note_id": "from-response",
"cliniko_status": 201,
"app_version": "0.x.y"
}
```

**No transcript, no SOAP body, no patient name.** The test matrix for
`AuditStore` must assert no such field ever leaks (see #10's acceptance).

---

## Reference implementations

- Networking layer mirrors patterns from
[`epc-letter-generation/Sources/Services/Networking/`](https://github.com/CloudbrokerAz/epc-letter-generation/tree/main/Sources/Services/Networking).
- Keychain credentials mirror
[`epc-letter-generation/Sources/Services/KeychainCredentialStore.swift`](https://github.com/CloudbrokerAz/epc-letter-generation/blob/main/Sources/Services/KeychainCredentialStore.swift).
- Audit log mirrors
[`epc-letter-generation/Sources/Services/AuditStore.swift`](https://github.com/CloudbrokerAz/epc-letter-generation/blob/main/Sources/Services/AuditStore.swift).

---

## Related files (once implemented)

- `Sources/Services/Cliniko/ClinikoClient.swift` — actor + URLSession.
- `Sources/Services/Cliniko/ClinikoEndpoint.swift` — endpoint enum.
- `Sources/Services/Cliniko/ClinikoError.swift` — typed errors.
- `Sources/Services/AuditStore.swift` — metadata-only audit log.
- `Tests/SpeechToTextTests/Fixtures/cliniko/` — request + response goldens.
Loading