diff --git a/Casks/transcripted.rb b/Casks/transcripted.rb index c89284576..9e3d8a464 100644 --- a/Casks/transcripted.rb +++ b/Casks/transcripted.rb @@ -21,6 +21,9 @@ zap trash: [ "~/Library/Application Support/Transcripted", "~/Library/Caches/com.justinbetker.draft", + "~/Library/Input Methods/Transcripted Keyboard.app", "~/Library/Preferences/com.justinbetker.draft.plist", + "~/Library/Preferences/com.justinbetker.draft.inputmethod.Transcripted.plist", + "~/Library/Preferences/com.justinbetker.draft.writing.plist", ] end diff --git a/README.md b/README.md index cc69f210e..1472b55f1 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ Markdown file on your Mac. Then you can ask Claude, or any AI, things like you were typing. - **Knows who's talking.** It splits the transcript by speaker. Name someone once and it'll suggest their name next time it hears them. +- **Helps you write.** Autocomplete finishes your sentences, right where + you're typing. Press `Tab` to take the next word. It can also save what you + wrote, so your AI sees your notes and replies next to your meetings. Both + are optional, and you pick the apps. Everything becomes a text file with timestamps and speaker names. You can also drop in an audio or video file you already have. @@ -100,6 +104,8 @@ Files are saved here by default. You can pick any folder in Settings. ~/Library/Application Support/Transcripted/captures/ ``` +Meetings, dictations, and writing each get their own folder in there. + ## Install You need an Apple Silicon Mac on macOS 26 or later. @@ -114,6 +120,10 @@ brew tap r3dbars/transcripted https://github.com/r3dbars/transcripted brew install --cask transcripted ``` +Autocomplete downloads a model the first time you turn it on. The default, +Gemma, needs about 3.4 GB of disk. The optional Qwen model takes 5.6 GB and +needs a Mac with 16 GB of memory. + ## Privacy - Your audio and transcripts never leave your Mac. @@ -121,6 +131,9 @@ brew install --cask transcripted - The app sends anonymous crash reports and usage stats. They never include audio, transcripts, names, or file paths. You can turn both off in **Settings → Privacy**. +- What you write stays on your Mac. The only thing Writing sends is anonymous + suggestion counts, never text, and the same usage stats switch turns them + off. Where everything is stored: [docs/storage-paths.md](docs/storage-paths.md). @@ -137,6 +150,24 @@ Nothing. No account, no subscription. **Is there a command-line tool?** Yes. See the [CLI instructions](Tools/TranscriptedCLI/README.md). +## Uninstall + +1. Quit Transcripted from the menu bar. +2. If you set up Writing, go to **System Settings → Keyboard → Input + Sources** and remove Transcripted. +3. Drag Transcripted out of Applications, or run + `brew uninstall --cask transcripted`. +4. Delete the keyboard, if it's there: + `~/Library/Input Methods/Transcripted Keyboard.app` + +Your meetings, dictations, and writing stay where they are. To also delete the +Writing model and Writing's app data, remove these folders: + +```text +~/Library/Application Support/Transcripted/models/writing/ +~/Library/Application Support/Transcripted/writing/ +``` + ## For contributors It's a native Swift app. Build and test: diff --git a/Sources/UI/Settings/AgentConnectionSettingsPage.swift b/Sources/UI/Settings/AgentConnectionSettingsPage.swift index 1defc403d..af27d8d00 100644 --- a/Sources/UI/Settings/AgentConnectionSettingsPage.swift +++ b/Sources/UI/Settings/AgentConnectionSettingsPage.swift @@ -20,6 +20,7 @@ struct AgentConnectionSettingsPage: View { private let meetingsFolderURL = AgentConnectionGuide.meetingsFolder private let dictationsFolderURL = AgentConnectionGuide.dictationsFolder + private let writingFolderURL = AgentConnectionGuide.writingFolder @State private var detectedAgents: Set = [] @State private var connectedAgents: Set = [] @State private var rowPhases: [AgentMCPAgent: RowPhase] = [:] @@ -309,6 +310,15 @@ struct AgentConnectionSettingsPage: View { reveal(dictationsFolderURL) } + AgentFolderRow( + name: "Writing", + detail: "Writing Markdown files.", + path: writingFolderURL.path, + isAvailable: folderExists(writingFolderURL) + ) { + reveal(writingFolderURL) + } + SettingsInlineActionButton( title: copiedFolderPaths ? "Copied" : "Copy Paths", symbolName: "folder", diff --git a/Sources/UI/Shared/AgentConnectionGuide.swift b/Sources/UI/Shared/AgentConnectionGuide.swift index f916c93c0..c79c6c2a8 100644 --- a/Sources/UI/Shared/AgentConnectionGuide.swift +++ b/Sources/UI/Shared/AgentConnectionGuide.swift @@ -30,6 +30,12 @@ enum AgentConnectionGuide { DictationStoragePaths.transcriptsFolder } + /// `/writing/`. The pure path form, so building a prompt + /// never creates the folder for people who don't use Writing. + static var writingFolder: URL { + FileManager.writingDirectory(in: FileManager.default.transcriptedCaptureLibraryDir) + } + static var codexInboxFolder: URL { FileManager.default.transcriptedAppSupportDir .appendingPathComponent("CodexInbox", isDirectory: true) @@ -115,6 +121,9 @@ enum AgentConnectionGuide { Dictations: - \(dictationsFolder.path) + Writing: + - \(writingFolder.path) + Use these files as the source of truth. Good first asks: @@ -122,7 +131,7 @@ enum AgentConnectionGuide { - Review yesterday. Use recent context or a recap to tell me what I promised, what changed, and what I should follow up on today. Rules: - - Prefer Transcripted direct tools when available; otherwise search meetings and dictations together from files. + - Prefer Transcripted direct tools when available; otherwise search meetings, dictations, and writing together from files. - Cite filenames, dates, speakers, and timestamps when useful. - For relative dates like today or yesterday, state the exact dates searched. - If a direct tool fails, fall back to the folders. @@ -202,6 +211,7 @@ enum AgentConnectionGuide { - Setup prompt file: \(setupPath) - Meetings: \(meetingsFolder.path) - Dictations: \(dictationsFolder.path) + - Writing: \(writingFolder.path) - State file: \(inboxPath)/state.json - Pending folder: \(inboxPath)/pending - Processed folder: \(inboxPath)/processed @@ -298,6 +308,9 @@ enum AgentConnectionGuide { Dictations: \(dictationsFolder.path) + + Writing: + \(writingFolder.path) """ } @@ -335,6 +348,7 @@ enum AgentConnectionGuide { Use Transcripted's local Markdown meetings as the source of truth: - Meetings: \(meetingsFolder.path) - Dictations: \(dictationsFolder.path) + - Writing: \(writingFolder.path) Rules: - Process only new unprocessed meetings unless the user asks for backfill. diff --git a/Tests/AgentConnectionGuideTests.swift b/Tests/AgentConnectionGuideTests.swift index dbfc64800..66d70c41b 100644 --- a/Tests/AgentConnectionGuideTests.swift +++ b/Tests/AgentConnectionGuideTests.swift @@ -70,7 +70,7 @@ func testAgentConnectionGuide() { "local agent prompt should not include web/Cowork routing" ) assertTrue( - prompt.contains("Prefer Transcripted direct tools when available; otherwise search meetings and dictations together from files."), + prompt.contains("Prefer Transcripted direct tools when available; otherwise search meetings, dictations, and writing together from files."), "prompt should support direct-tool retrieval and folder fallback" ) assertTrue( @@ -105,6 +105,15 @@ func testAgentConnectionGuide() { prompt.contains("Dictations:\n- \(AgentConnectionGuide.dictationsFolder.path)"), "prompt should include the dictations folder" ) + assertTrue( + prompt.contains("Writing:\n- \(AgentConnectionGuide.writingFolder.path)"), + "prompt should include the writing folder" + ) + assertEqual( + AgentConnectionGuide.writingFolder.lastPathComponent, + "writing", + "writing folder should be the capture library's writing/ folder" + ) assertTrue( prompt.contains("If helpful, start with this meeting:\nPlanning Sync.md"), "meeting-specific prompt should preserve the selected filename" @@ -139,6 +148,10 @@ func testAgentConnectionGuide() { assertTrue(readme.contains("Transcripted Codex Inbox"), "README should name the Codex Inbox") assertTrue(agents.contains("Process only new unprocessed meetings"), "AGENTS should tell Codex not to backfill by default") + assertTrue( + agents.contains("- Writing: \(AgentConnectionGuide.writingFolder.path)"), + "AGENTS should list the writing folder next to meetings and dictations" + ) assertTrue(setup.contains("transcripted-inbox-watch"), "setup prompt should name the heartbeat automation") assertTrue(state.contains("\"processedMeetings\": []"), "state should start with no processed meetings") assertTrue(state.contains("\"installedAt\""), "state should create a setup baseline") @@ -151,6 +164,10 @@ func testAgentConnectionGuide() { assertTrue(prompt.contains("Transcripted Codex Inbox Setup"), "prompt should title the setup clearly") assertTrue(prompt.contains(inboxURL.path), "prompt should include the inbox path") assertTrue(prompt.contains(AgentConnectionGuide.meetingsFolder.path), "prompt should include the meetings folder") + assertTrue( + prompt.contains("- Writing: \(AgentConnectionGuide.writingFolder.path)"), + "prompt should include the writing folder" + ) assertTrue(prompt.contains("Monday through Friday"), "prompt should ask for a weekday schedule") assertTrue(prompt.contains(":05 and :35"), "prompt should ask for checks after the hour and half-hour") assertTrue(prompt.contains("stay quiet"), "prompt should keep empty checks silent") @@ -194,6 +211,7 @@ func testAgentConnectionGuide() { ) assertTrue(folderText.contains(AgentConnectionGuide.meetingsFolder.path), "folder copy should include current meetings path") assertTrue(folderText.contains(AgentConnectionGuide.dictationsFolder.path), "folder copy should include current dictations path") + assertTrue(folderText.contains(AgentConnectionGuide.writingFolder.path), "folder copy should include current writing path") } runSuite("AgentConnectionGuide bundled skills — files and manifest are versioned") { diff --git a/Tests/Benchmarks/HomeRecentCaptureBenchmark.swift b/Tests/Benchmarks/HomeRecentCaptureBenchmark.swift index caf0e50a3..ee4214538 100644 --- a/Tests/Benchmarks/HomeRecentCaptureBenchmark.swift +++ b/Tests/Benchmarks/HomeRecentCaptureBenchmark.swift @@ -8,6 +8,19 @@ struct HomeRecentCaptureBenchmark { let runRoot = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true) .appendingPathComponent("build/home-recent-capture-benchmark", isDirectory: true) .appendingPathComponent(UUID().uuidString, isDirectory: true) + + // Harnesses must not touch real user state. Loading resolves the capture + // library through `transcriptedCaptureLibraryDir`, which rewrites + // mcp-directories.json, and the loader's default metadata cache is a + // `static let` whose path is fixed at first access. Point the whole + // app-owned container at this run's scratch folder before anything + // touches storage, so neither lands in ~/Library/Application Support/Transcripted. + let containerRoot = runRoot.appendingPathComponent("container", isDirectory: true).standardizedFileURL + setenv("TRANSCRIPTED_CONTAINER_DIR", containerRoot.path, 1) + guard fileManager.transcriptedMCPDirectoriesManifestURL.standardizedFileURL.path.hasPrefix(containerRoot.path + "/") else { + throw BenchmarkError.configuration("TRANSCRIPTED_CONTAINER_DIR override did not take effect; refusing to touch the real app-support container") + } + let captureRoot = runRoot.appendingPathComponent("captures", isDirectory: true) let meetingsRoot = captureRoot.appendingPathComponent("meetings", isDirectory: true) let dictationsRoot = captureRoot.appendingPathComponent("dictations", isDirectory: true) diff --git a/Tests/TranscriptedStoragePathsTests.swift b/Tests/TranscriptedStoragePathsTests.swift index 1e705a970..0b795c58f 100644 --- a/Tests/TranscriptedStoragePathsTests.swift +++ b/Tests/TranscriptedStoragePathsTests.swift @@ -14,19 +14,58 @@ func testTranscriptedStoragePaths() { return attributes?[.posixPermissions] as? NSNumber } - let originalManifestURL = FileManager.default.transcriptedMCPDirectoriesManifestURL - let originalManifestExists = FileManager.default.fileExists(atPath: originalManifestURL.path) - let originalManifestData = try? Data(contentsOf: originalManifestURL) - defer { - if originalManifestExists, let originalManifestData { - try? FileManager.default.createPrivateDirectory(at: originalManifestURL.deletingLastPathComponent()) - try? originalManifestData.write(to: originalManifestURL, options: [.atomic]) - FileManager.default.restrictFileToOwnerOnly(at: originalManifestURL) - } else { - try? FileManager.default.removeItem(at: originalManifestURL) + /// Modification time and size, never the contents: the real manifest holds + /// the user's capture-library paths. + func fingerprint(of url: URL) -> String? { + guard let attributes = try? FileManager.default.attributesOfItem(atPath: url.path) else { + return nil } + let modified = (attributes[.modificationDate] as? Date)?.timeIntervalSinceReferenceDate + let size = (attributes[.size] as? NSNumber)?.intValue + return "mtime=\(modified.map { String($0) } ?? "?") size=\(size.map { String($0) } ?? "?")" } + // Harnesses must not touch real user state. The suites that go through + // `setCaptureLibraryURL` or the `transcriptedCaptureLibraryDir` getter rewrite + // mcp-directories.json at `transcriptedMCPDirectoriesManifestURL`, which follows + // the `TRANSCRIPTED_CONTAINER_DIR` container override that run-tests.sh exports + // as a throwaway folder. Suites that call the writer directly pass their own + // temp `manifestURL`. If the override is missing, stop here rather than write + // (and "restore") ~/Library/Application Support/Transcripted/mcp-directories.json. + let realManifestURL = ( + FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + ?? URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Application Support", isDirectory: true) + ) + .appendingPathComponent("Transcripted", isDirectory: true) + .appendingPathComponent("mcp-directories.json", isDirectory: false) + .standardizedFileURL + let realManifestBefore = fingerprint(of: realManifestURL) + let isolatedManifestURL = FileManager.default.transcriptedMCPDirectoriesManifestURL.standardizedFileURL + + var manifestWritesAreIsolated = false + runSuite("Transcripted MCP directory manifest — test writes land in the test container, not the real one") { + guard let containerRaw = ProcessInfo.processInfo.environment["TRANSCRIPTED_CONTAINER_DIR"], + !containerRaw.isEmpty else { + assertTrue( + false, + "TRANSCRIPTED_CONTAINER_DIR must be exported by the test harness so manifest writes stay out of the real app-support container" + ) + return + } + // Standardize so a `TMPDIR` trailing slash (→ `//`) can't break the prefix. + let container = URL(fileURLWithPath: containerRaw, isDirectory: true).standardizedFileURL.path + let underContainer = isolatedManifestURL.path.hasPrefix(container + "/") + let isRealManifest = isolatedManifestURL.path == realManifestURL.path + + assertTrue( + underContainer, + "the manifest the storage helpers write should live under the test container, got \(isolatedManifestURL.path)" + ) + assertFalse(isRealManifest, "the manifest the storage helpers write must not be the real one") + manifestWritesAreIsolated = underContainer && !isRealManifest + } + guard manifestWritesAreIsolated else { return } + runSuite("FileManager.createPrivateDirectory — tightens existing directories to owner-only") { let directory = FileManager.default.temporaryDirectory .appendingPathComponent("TranscriptedStoragePathsTests-existing-\(UUID().uuidString)", isDirectory: true) @@ -142,8 +181,8 @@ func testTranscriptedStoragePaths() { let persisted = TranscriptedStoragePreferences.setCaptureLibraryURL(customRoot) assertTrue(persisted, "test setup should persist a safe custom capture-library root") - let manifestURL = FileManager.default.transcriptedMCPDirectoriesManifestURL - guard let data = try? Data(contentsOf: manifestURL), + // The container-scoped manifest checked at the top of this file. + guard let data = try? Data(contentsOf: isolatedManifestURL), let manifest = try? JSONDecoder().decode(TranscriptedMCPDirectoriesManifest.self, from: data) else { assertTrue(false, "manifest should be readable JSON after setting a custom capture library") return @@ -591,4 +630,12 @@ func testTranscriptedStoragePaths() { "app writer output for the fixture's capture-library input should match the golden fixture exactly" ) } + + runSuite("Transcripted MCP directory manifest — the suites above left the real manifest untouched") { + assertEqual( + fingerprint(of: realManifestURL), + realManifestBefore, + "the real ~/Library/Application Support/Transcripted/mcp-directories.json should keep its mtime and size (nil = absent before and after)" + ) + } } diff --git a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Models.swift b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Models.swift index c9d84537b..b9710426f 100644 --- a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Models.swift +++ b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Models.swift @@ -103,7 +103,7 @@ struct AgentDictationEntry: Codable { } } -/// A parsed writing day file (`Writing_.md`): what the user typed +/// A parsed writing day file (`Writing_.md`): what the user wrote /// with the Transcripted keyboard, one entry per app/idle-gap segment. struct AgentWritingDay: Codable { let version: String diff --git a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers+Writing.swift b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers+Writing.swift index 7582a22e6..254ef8ab4 100644 --- a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers+Writing.swift +++ b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers+Writing.swift @@ -2,7 +2,7 @@ import Foundation import MCP import TranscriptedCaptureKit -// Writing day files (`Writing_.md`): what the user typed with the +// Writing day files (`Writing_.md`): what the user wrote with the // Transcripted keyboard. These mirror list_dictations / read_dictation so an // agent that knows one knows the other. diff --git a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers.swift b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers.swift index e2978f46b..261146e65 100644 --- a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers.swift +++ b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/ToolHandlers.swift @@ -446,7 +446,7 @@ func registerToolHandlers(server: Server, index: TranscriptIndex, directories: T ), Tool( name: "list_writing", - description: "List saved writing days: what the user typed with the Transcripted keyboard, one Writing_ file per day, with entry counts, source apps, and recent titles. Use read_writing with a returned filename for the text.", + description: "List saved writing days: what the user wrote with the Transcripted keyboard, one Writing_ file per day, with entry counts, source apps, and recent titles. Use read_writing with a returned filename for the text.", inputSchema: .object([ "type": .string("object"), "properties": .object([ @@ -499,7 +499,7 @@ func registerToolHandlers(server: Server, index: TranscriptIndex, directories: T ), Tool( name: "search_context", - description: "Search across saved meetings, dictations, writing, or all of them. Defaults to hybrid (full-text + on-device semantic), so paraphrases match, not just exact wording; writing is matched by full text only. Great for finding everything you captured about a topic, whether it came from a meeting, a quick dictated note, or something you typed.", + description: "Search across saved meetings, dictations, writing, or all of them. Defaults to hybrid (full-text + on-device semantic), so paraphrases match, not just exact wording; writing is matched by full text only. Great for finding everything you captured about a topic, whether it came from a meeting, a quick dictated note, or something you wrote.", inputSchema: .object([ "type": .string("object"), "properties": .object([ diff --git a/docs/agent-connect.md b/docs/agent-connect.md index 6ae43c48a..a08781548 100644 --- a/docs/agent-connect.md +++ b/docs/agent-connect.md @@ -67,6 +67,8 @@ Current `transcripted-mcp` capabilities: - `read_meeting` - `list_dictations` - `read_dictation` +- `list_writing` +- `read_writing` - `search` - `who_is` - `recap` @@ -78,15 +80,23 @@ The last three roll up structured summary fields (decisions, action items, open questions) across saved meetings. They only return rows for meetings that have a saved summary; see `docs/cross-meeting-tools.md`. -These tools are read-only, but they are not redacted. `read_meeting` and -`read_dictation` can return local transcript text to the agent you connected. +Agents can read what you wrote, too, if Save my writing is on. +`list_writing` and `read_writing` open the daily `Writing_.md` files. +`search_context` and `recent_context` include writing by default; pass +`kind: "writing"` to get only writing. Writing search is full text only, not +semantic. + +These tools are read-only, but they are not redacted. `read_meeting`, +`read_dictation`, and `read_writing` can return local transcript or writing +text to the agent you connected. ### Anonymous Usage Ping Like the app, the MCP helper can send one anonymous analytics event per successful tool call (`agent_capture_query_observed`) so we can tell whether the agent connection gets used. It carries only bucketed metadata: which kind -of tool ran, meeting vs. dictation, rough capture age, and rough source count. +of tool ran, which kind of capture (meeting, dictation, or writing), rough +capture age, and rough source count. It never includes transcript text, queries, titles, speaker names, file paths, or audio, and your captures still never leave your Mac. @@ -118,6 +128,7 @@ connected, otherwise read the saved Markdown folders: ```text ~/Library/Application Support/Transcripted/captures/meetings ~/Library/Application Support/Transcripted/captures/dictations +~/Library/Application Support/Transcripted/captures/writing ``` ## Transcribe Files From an Agent @@ -208,7 +219,7 @@ Notes: - `transcripted-mcp` communicates over stdio, not HTTP. - `--self-test` verifies directory resolution, creates missing local data/index directories, and exits without starting the MCP stdio server. - By default it follows the capture library chosen in Transcripted Settings, then also reads legacy Draft or `~/Documents/Transcripted/` layouts when those folders still contain capture Markdown. -- `TRANSCRIPTED_DATA_DIR` can point at a shared root with `meetings/` and `dictations/` subfolders. For `transcripted-mcp`, that shared root also becomes the default SQLite index location unless `TRANSCRIPTED_INDEX_DIR` is set. +- `TRANSCRIPTED_DATA_DIR` can point at a shared root with `meetings/`, `dictations/`, and `writing/` subfolders. For `transcripted-mcp`, that shared root also becomes the default SQLite index location unless `TRANSCRIPTED_INDEX_DIR` is set. - If needed, override paths with `TRANSCRIPTED_DATA_DIR`, - `TRANSCRIPTED_MEETINGS_DIR`, `TRANSCRIPTED_DICTATIONS_DIR`, and - `TRANSCRIPTED_INDEX_DIR`. + `TRANSCRIPTED_MEETINGS_DIR`, `TRANSCRIPTED_DICTATIONS_DIR`, + `TRANSCRIPTED_WRITING_DIR`, and `TRANSCRIPTED_INDEX_DIR`. diff --git a/docs/llama-server-provenance.md b/docs/llama-server-provenance.md new file mode 100644 index 000000000..0d59ed3e9 --- /dev/null +++ b/docs/llama-server-provenance.md @@ -0,0 +1,24 @@ +# llama-server provenance + +Writing's inference helper (`Contents/Helpers/llama-server`) is Tilde 0.1.0 beta 1's helper. + +- **Where build-deps gets it:** `build-deps.sh` fetches it from `Tilde.zip` on the r3dbars/tilde v0.1.0-beta.1 release (zip SHA-256 `12b7f14ae31abea7d5cecf236d2e4de3b0facad89fa580877dec391336b26a50`). +- **How it's pinned:** by its code bytes with the signature removed, SHA-256 `3f6895ab8d077b02803761fb8cc254073d2c7b4006fbacbef4c844879333fffc`. +- **Signing:** the build re-signs it with Transcripted's identity. + +## Verified from source (2026-09-25) + +The pinned bytes were rebuilt independently and matched exactly, twice, from fresh clones: + +- **Source:** `ggml-org/llama.cpp` at `2115b73d8ebdbd659075cce66c609506863bc826` (2026-08-22, "model : support DSpark for bailingmoe3 (#27508)"). A shallow clone without tags gives `version: 0.2.0-dev (build 1, commit 2115b73)`. +- **Toolchain:** Command Line Tools 26.6 (AppleClang 21.0.0.21000101, ld-1267, macOS SDK 26.5). Xcode 27 produces different bytes. +- **Configure:** `cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DGGML_NATIVE=OFF -DGGML_METAL=ON -DGGML_METAL_EMBED_LIBRARY=ON -DLLAMA_OPENSSL=OFF -DCMAKE_OSX_DEPLOYMENT_TARGET=26.0`, with `-ffile-prefix-map` mapping the source path onto Tilde's original temporary build path (235 absolute paths are embedded). +- **Build:** `cmake --build build --target llama-server`, then `strip -S -x`. +- **Web UI:** the embedded web UI assets (70 gzipped files) came from Hugging Face's `ggml-org/llama-ui` "latest" at build time (built 2026-08-24, likely release b10612). They aren't pinned by the llama.cpp commit. +- **Hash comparison:** `codesign --remove-signature` leaves `__LINKEDIT` sized for the removed signature. To compare, ad-hoc sign with `--digest-algorithm=sha1,sha256`, then remove the signature again. That yields `3f6895ab…`. + +Conclusion: the shipped helper is upstream llama.cpp `2115b73` plus that web UI snapshot, and nothing else. + +## Recommendation + +Keep pinning Tilde's binary. A from-source step in `build-deps.sh` would break on the next Command Line Tools update, on any machine without CLT 26.6, and whenever the web UI source can't be pinned. If Transcripted ever builds its own helper, build with `LLAMA_BUILD_UI=OFF` and pin its own hash of the stripped output instead of chasing this one. diff --git a/docs/writing-plan.md b/docs/writing-plan.md index 85aae455a..dfecb8c43 100644 --- a/docs/writing-plan.md +++ b/docs/writing-plan.md @@ -47,8 +47,8 @@ It is not a code source for this port. ships. It's Justin's testing tool, not a product. 9. `llama-server`: re-sign the exact binary Tilde 0.1.0 beta 1 shipped. Write down a reproducible llama.cpp build recipe before public rollout. It was built - from llama.cpp commit `2115b73` (AppleClang 21, arm64, static); see the - ledger's Follow-ups for the recipe. + from llama.cpp commit `2115b73` (AppleClang 21, arm64, static), verified + by an exact from-source rebuild; see `docs/llama-server-provenance.md`. 10. Saved-text fidelity for v1: keyboard capture only, plus Backspace tracking so deleted text isn't saved. Look at real files before deciding whether an Accessibility read of the final field text is worth adding. diff --git a/docs/writing-port-ledger.md b/docs/writing-port-ledger.md index 4117d0167..3d3e44e83 100644 --- a/docs/writing-port-ledger.md +++ b/docs/writing-port-ledger.md @@ -162,10 +162,8 @@ python3 ~/tilde-port/parity-diff.py --ledger docs/writing-port-ledger.md --repo - Check with real files: Return, Tab, arrows and shortcuts break segments, so each Slack message or Notes paragraph becomes its own writing entry. Decide at the phase 3 checkpoint whether consecutive segments in one app should merge. - Narrowing the app scope doesn't retroactively remove already-stored encrypted personal history; replay filters by the exclusion list only. -- Phase 6: `AgentConnectionGuide`'s file-fallback prompt lists the meetings and dictations folders only. Add the writing folder there (four places) and in its pinned tests. -- Done in phase 4: user-visible runtime strings that said "Tilde" (outcome-ledger and runtime status text such as "reinstall Tilde", "Tilde held back…"). Rename them to Transcripted/Writing copy when the Writing tab lands, and update the tests that assert them. - Phase 2 cleanup: collapse `.preview9B` into a Qwen completion profile once nothing reads its preview identities. -- Before rollout: the `llama-server` pin depends on `codesign --remove-signature` output staying byte-stable across toolchains (it fails closed). Provenance found 2026-09-25: `llama-server --version` reports `version: 0.2.0-dev (build 1, commit 2115b73)`, built with AppleClang 21.0.0 for Darwin arm64, static with system-only dependencies. Reproducible recipe: build `ggml-org/llama.cpp` at `2115b73` as a static Release with the Metal library embedded, then `strip -S -x`, and compare the unsigned code hash `3f6895ab…`. The exact CMake flags still need confirming. Once it matches, build-deps can build instead of fetching Tilde's zip. +- Done: `llama-server` provenance is verified. A from-source rebuild of llama.cpp `2115b73` matches the pinned code hash exactly; see [llama-server-provenance.md](llama-server-provenance.md). The pin fails closed if a toolchain ever changes how `codesign --remove-signature` lays out bytes. ## Tests