Skip to content

Writing phase 2: autocomplete runs inside Transcripted (behind WritingDebugEnabled) - #1869

Merged
r3dbars merged 28 commits into
mainfrom
claude/writing-phase-2
Sep 25, 2026
Merged

r3dbars merged 28 commits into
mainfrom
claude/writing-phase-2

Conversation

@r3dbars

@r3dbars r3dbars commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Phase 2 of docs/writing-plan.md, stacked on #1868. There's still no UI. Autocomplete runs when a debug default is on.

What's in it

  • Runtime port: Tilde's app runtime is ported to Sources/TranscriptedWriting/Runtime/ (28 files): the model manager, llama-server host and restart policy, the authenticated socket server, Screen Memory (Accessibility tree first, then OCR; memory only), personal history, stats, and the keyboard installer. Parity against Tilde f36f6562 differs only by the documented strips and deviations (see the ledger's new "Deviations from Tilde" list).
  • App bridge: Sources/Writing/WritingController.swift, started from TranscriptedAppState only when WritingDebugEnabled is on. It has Tilde's startup and stop order and adds a wake hook, which Tilde never had.
  • Behavior changes:
    • adopts a hash-verified Tilde model via clonefile
    • Qwen needs at least 16 GiB of memory
    • no self-relaunch: a model switch restarts only the helper and cancels a superseded download
    • TISEnableInputSource
    • quiet-quit only on a real user quit
    • the keyboard never reopens an already-running app
    • the owner seal check runs once per launch
    • the keyboard gets an integer build number
  • Tests: Tilde's app tests are ported (263), plus bridge tests. DiagnosticsLog never writes under tests.

Verification (local)

  • build.sh, codesign --verify --deep --strict (app and keyboard) and the beta DMG build all pass.
  • run-tests.sh: 18450/18450.
  • swift test: 588 XCTest + 731 Swift Testing.
  • Also passing: integration smoke, QA tests, CLI packaging, source lists, source pins, duplicate declarations, parity, and grep checks for relaunch code and for untouched real user logs.
  • linux-checks.sh: all pass except the VM self-tests.
  • Independent review (Fable 5.1): ship. Its three pre-phase-4 items are fixed here.

How to try it (Justin)

  1. Quit Tilde and the running Transcripted.
  2. Build with a Developer ID (bash build.sh --no-open); ad hoc builds can't authenticate the keyboard.
  3. defaults write com.justinbetker.draft WritingDebugEnabled -bool YES
  4. Add the built Transcripted under System Settings › Privacy & Security › Screen & System Audio Recording. Nothing prompts in this phase, and the keyboard stays silent without it.
  5. Open the built app. It adopts Tilde's Gemma model if it's there, installs, enables and selects the "Transcripted" keyboard, and starts the helper.
  6. Type in Slack, Mail, Messages, Notes, Chrome and VS Code. Tab takes one word, ~ takes all of it, Esc hides it.
  7. Undo: defaults delete com.justinbetker.draft WritingDebugEnabled, then switch input sources back.

🤖 Generated with Claude Code

r3dbars and others added 28 commits September 25, 2026 13:53
Transcripted gets a third capture feature, Writing, next to Meetings and
Dictations: Tilde's local autocomplete keyboard ported with exact
behavior, plus optional writing capture saved as plain Markdown in the
capture library. The plan records the decisions made with Justin, the
approved Writing tab copy, a parity checklist against Tilde f36f6562,
where the code lands, build and release changes, storage, agent tools,
analytics, six phases with exit criteria, and risks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six project subagents for the Writing port: writing-porter (Opus 5.5,
xhigh), writing-porter-fable (Fable 5.1, xhigh) for the keyboard, socket
auth and Screen Memory, writing-builder (Opus 5.5, high), writing-worker
(Opus 5.5, medium), writing-reviewer (Fable 5.1, max, read-only) and
writing-scout (Opus 5.5, low). They share one set of ground rules.

docs/writing-port-ledger.md maps every shipping Tilde file and test at
f36f6562 to its Transcripted destination, phase and status (107 source
rows, 92 test rows), plus the identity rename table and the pinned
llama-server code hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-deps.sh fetches Tilde 0.1.0 beta 1's release zip, checks its hash,
lifts Contents/Helpers/llama-server out, strips Tilde's signature and checks
the code-bytes hash before installing deps-tools/llama-server. Either
mismatch stops the build. build.sh and build-beta.sh require the helper,
copy it to Contents/Helpers/, and sign it in the existing Helpers loop.

Both sign steps now sign any Contents/Library/Input Methods/*.app before
the outer app (hardened runtime, timestamp and the new, empty
keyboard.plist entitlements in the beta build). The loop does nothing
until a keyboard bundle exists. The app compile skips
Sources/TranscriptedKeyboard/.

PackagedAppSmoke checks the helper is present and passes codesign.
Adds llama.cpp's MIT license and routes the Writing, keyboard and
entitlements paths in the test matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The static fixture ships a llama-server helper so the passing case stays
green, and two new cases fail on a missing helper and a bad signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports the 46 `todo` Sources/TildeCore rows of docs/writing-port-ledger.md
verbatim into Sources/TranscriptedWriting/Core, same subfolders and file
names, and marks them `ported`. The five not-ported rows (PreviewModelAsset,
PsychicReplay, ReplayEval, H01BlockRandomization, PreviewModelChoice) are
not copied.

44 files have no parity drift. The two that do:
- TildeProductProfile: the ledger renames (bundle IDs, connection name,
  Transcripted/writing support dir, "Transcripted Keyboard.app",
  display name, llama port 17891, keychain service
  com.justinbetker.draft.writing.personal-history), and the ledger's
  "drop preview-build profiles" strip: .preview26B and .modelPreview go,
  .production and .preview9B (the Qwen behaviour profile) stay.
- SuggestionArbiter: PersonalReplayEval.normalizeWord inlined as a
  private helper, since ReplayEval isn't ported.

Package.swift gains a dependency-free TranscriptedWritingCore target at
Sources/TranscriptedWriting/Core. build.sh already compiles these files
into the app module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports 45 of Tilde's 46 todo core test files to Tests/TranscriptedWritingTests/Core
with the ledger renames, and adds a TranscriptedWritingTests Swift Testing
target in Package.swift.

Trimmed only what tests dropped types: the .preview26B/.modelPreview
profiles (SceneEchoPolicyTests, TildeProductProfileTests) and
PreviewModelChoice (TildeProductProfileTests).

RedactionCorpusSanityTests stays todo: its jsonl corpus fixture is not in
the f36f6562 export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Straight port of Tilde f36f6562's IMKit keyboard (7 Swift files plus Info.plist) into Sources/TranscriptedKeyboard with the ledger's identity renames. Key handling is unchanged: Tab adds a word, the keyCode-50 backtick/tilde key accepts all, Esc is swallowed only with a ghost visible; type-through, reveal delays, drawing, secure input, tickets and staleness rules are verbatim.

Stripped per the plan: the H01 randomization call sites (experimentArm now travels as nil, variant falls back to the ledger's champion default) and GhostOutcomeLedger's plaintext word diary, so the keyboard writes only text-free v3 events while the 5 s / 30 s / segment-close kept-or-edited checks keep running from memory. main.swift had no dev flags at this commit. Three DispatchQueue labels move from bar.r3d/com.tilde to the Transcripted namespace.

Package.swift gains a TranscriptedKeyboard executable target on TranscriptedWritingCore so later tests can build it; the ledger rows are marked ported.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…of the app binary

scripts/entrypoints/lib/bundle-input-method.sh compiles Sources/TranscriptedWriting/Core and Sources/TranscriptedKeyboard as one Swift 5 module with swiftc -O -framework InputMethodKit into Contents/Library/Input Methods/Transcripted Keyboard.app, copies the checked-in Info.plist with both version keys stamped from the root Info.plist, checks the controller class the plist names is really in the binary, and replaces the previous bundle whole so it is safe to re-run. It never signs; the build scripts' signing loop owns that, and build.sh/build-beta.sh are not wired yet.

swiftc-app-args.sh now excludes Sources/TranscriptedKeyboard/ the way it excludes TranscriptedCore: without it build.sh compiled main.swift's top-level IMKServer code under -parse-as-library and failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hase-1

# Conflicts:
#	Package.swift
#	scripts/entrypoints/lib/swiftc-app-args.sh
build.sh and build-beta.sh now call bundle_transcripted_input_method
right after the CLI helper, before the nested-code signing step, so
Contents/Library/Input Methods/Transcripted Keyboard.app is signed
inside-out with the rest of the app. PackagedAppSmoke checks the bundle
exists and passes codesign --verify --strict, with fixture tests for a
missing and an unsigned keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ction fixtures

bundle-input-method.sh checked for the controller class with
`nm | grep -q`. Under the build scripts' `set -o pipefail`, grep exits on
the first match and nm takes SIGPIPE, so the check failed on a good
binary and build.sh stopped. Read nm into a variable first.

The nightly secret scan flagged the fake keys in Tilde's SecretRules and
RawContinuationPrompt tests, which exist to prove those shapes are
scrubbed. Allowlist exactly those two files, like the existing sanitizer
test allowlist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngRuntime

Straight port of TildeApp minus UI and lifecycle into
Sources/TranscriptedWriting/Runtime: the socket server and peer auth, the
llama-server host, completion engine and scaffold prewarmer, the model
manager, Screen Memory, personal history, the outcome-ledger readers and
stats, TildeSettings, and the keyboard installer. Identity renames per the
ledger's rename table. Dev-only hooks stripped: H01, the preview models and
PreviewModelSelection, TILDE_* overrides (except the DEBUG unsigned-peer
flag, renamed), release-proof paths, the local OCR evaluation store and the
incremental-OCR flag.

Allowed changes: the keyboard installer reads
Contents/Library/Input Methods/<profile.inputMethodInstalledBundleName>,
and the diagnostics log writes to Transcripted's logs directory. The
runtime.lock flock stays; a duplicate instance makes start() return false.

Adds the TranscriptedWritingRuntime SPM target (Core dependency, system
frameworks only). build.sh compiles the same files into the app module, so
the Core import is guarded with canImport. Nothing starts the runtime yet;
the Sources/Writing bridge (P2-B) owns lifecycle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- KeyboardIdentityTests pins the keyboard Info.plist (bundle ID,
  TISInputSourceID, connection name, controller class, executable) to
  TildeProductProfile.production, so a drifted connection name can't
  ship a keyboard that installs and never gets a session.
- Ledger records why .preview9B stays: Qwen's completion behavior reads
  it. Collapse it in phase 2 cleanup.
- repo-layout, Sources/CLAUDE.md and Tests/README.md name the new
  Writing folders and test target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the TildeAppTests rows whose types now live in
Sources/TranscriptedWriting/Runtime/ or Sources/TranscriptedKeyboard/
to Tests/TranscriptedWritingTests/Runtime/ with the ledger renames.
Trim only the cases that exercise stripped or replaced code (H01,
preview profiles, OCR evaluation, incremental OCR, StatusMenuHost,
the settings view model, YourTildeView, AppDelegate launch modes).
Mark the tests for replaced or not-ported code in the ledger.

The TranscriptedWritingTests target now depends on the runtime and
keyboard targets too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DiagnosticsLog.shared wrote every runtime test's events into the real
~/Library/Application Support/Transcripted/logs/writing-diagnostics.log.
It now follows FileLogger's rule: no file writes under XCTest or Swift
Testing, or with TRANSCRIPTED_DISABLE_FILE_LOGGER=1. Tests pin both the
detection and that this test process never writes. The ledger gains a
follow-ups list (Tilde-branded strings, .preview9B, the llama pin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…abled

WritingController (Sources/Writing) wires Tilde's runtime in its startup
order minus the dev-only steps: socket server, Screen Memory observers
(activation observer + 1 s window poll), prewarmer, model preparation,
keyboard installer. Settings use the keyboard's own defaults domain for
keyboard keys and com.justinbetker.draft.writing for app keys; models live
in Application Support/Transcripted/models/writing.

TranscriptedAppState owns it, starts it from initialize() only when
WritingDebugEnabled is set, checks the helper on wake, and stops it in
shutdown() in Tilde's order. A graceful stop sets the keyboard's quiet-quit
flag; start clears it.

New Runtime pieces, covered by SPM tests with fakes and temp fixtures:
- Tilde-model adoption: size + SHA-256 verified off the main thread, then
  cloned from the same descriptor into Transcripted's model folder
- Qwen needs 16 GiB; a persisted ineligible Qwen runs Gemma
- model switch without relaunch: stop helper, persist, rebuild, prepare,
  restart helper
- suggestions gate seam (Tilde's Screen Memory rule plus on/paused)
- wake policy for the helper
- TISEnableInputSource on first setup, then select

No Accessibility or Screen Recording prompt at launch; no relaunch code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cache the app seal check

- bundle-input-method.sh stamps an integer CFBundleVersion (1.1.66 ->
  1001066). The ported installer upgrades only on a larger integer, so a
  dotted build made installOrUpdateIfNeeded() fail and nothing installed.
- The keyboard only summons Transcripted when it isn't running. A running
  app means the model or helper is still loading, and a second open would
  deliver a reopen event that shows the window up to once a minute.
- KeyboardInstaller validates this app's own code seal once per launch
  instead of on every call (about 0.2 s on the main thread each time).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons)

- Quiet-quit is set only when the user quits. applicationShouldTerminate
  records whether the quit Apple event carries a system quit reason
  (logout, restart, shutdown). Those leave the flag clear, so after a
  reboot without launch at login the keyboard can still wake Transcripted.
- ModelManager.cancel() stops a superseded download on a model switch;
  waitUntilSettled returns when cancelled instead of spinning.
- The ledger records every Transcripted deviation from Tilde in one list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@r3dbars
r3dbars changed the base branch from claude/writing-phase-1 to main September 25, 2026 22:45
@r3dbars
r3dbars merged commit b4e0347 into main Sep 25, 2026
8 checks passed
r3dbars added a commit that referenced this pull request Sep 25, 2026
…ide Transcripted (#1868, #1869, #1870)

Writing phase 3: Save my writing, one app-scope rule, and agent tools read writing
@r3dbars
r3dbars deleted the claude/writing-phase-2 branch September 25, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant