Imported from thehumanworks/pasta (
AGENTS.md). Install upstream withnpx skills add thehumanworks/pasta. Copyright stays with the author.
Pasta Agent Instructions
These project-local instructions apply to this repository.
Product Boundary
- The app name is Pasta. CLI/package references should use
pasta. - Transport is central-service only: Cloudflare Worker over HTTPS plus one Durable Object per clipboard space.
- P2P, LAN discovery, SSH, tailnets, STUN/TURN, and WebRTC traversal are out of scope. Do not reintroduce them as a fallback or future MVP path.
- Devices own interactions: copy publishes ciphertext, paste pulls latest/history, pairing approval wraps keys, reset starts a new encrypted space.
- Cloudflare must never receive clipboard plaintext or raw group keys.
- Cloudflare auth products are not part of MVP auth. Use app-owned device keys and signed requests.
Execution Entry Point
- Read
GOAL.md. - Read
docs/ORCHESTRATION.md. - Work the goal files in
docs/goals/using the local GDD workflow. - Before changing a goal, run
gdd_status.pyon it and preserve its DoD/task coverage. - Record evidence in the task's
Evidenceblock before marking any task or DoD complete.
Toolchain And Secrets
- Use
miseas the tool manager. Prefer repo-configured tools throughmise exec -- <command>when a tool is not already onPATH. - Use
bunas the TypeScript runtime and package manager. Preferbun install,bun run,bun test, andbunx --bun; do not introduce npm, yarn, pnpm, or their lockfiles unless explicitly requested. - Use
fnoxfor secrets. Run secret-gated commands asmise exec -- fnox exec -- <command>so secrets are injected fromfnox.toml. fnoxis already configured to fetchCLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_KEY, andCLOUDFLARE_ACCOUNT_EMAILfrom Doppler. Do not replace this with.envfiles or hardcoded credentials.- Never print, commit, or paste secret values. Secret checks should prove names/configuration, not reveal values.
- For iOS builds in parallel worktrees, use an isolated
-derivedDataPath. Inspect generatedios/Package.resolvedchurn and drop it unless dependency pins intentionally changed. Do not usestatusas a zsh variable name; it collides with shell state.
Scope Discipline
- Text MVP comes before images/files.
- Shell/keybinding integration comes before global OS hotkeys or OS services.
- Keep implementation changes narrow to the active goal.
- Do not store secrets in config files, logs, fixtures, or docs.
iOS Custom Keyboard (KeyboardKit)
The Pasta keyboard is additive: KeyboardKit owns the keys, autocomplete
band, surface, and all input handling; Pasta only adds compact side actions
around KeyboardKit's standard autocomplete toolbar. History refresh happens
automatically when Full Access and pairing state allow it, and the Paste action
opens a history menu. Keep the keys and input as native as possible. The recurring bug
here has been turning the action row into a bolt-on strip glued to the keyboard's
top edge — do not repeat it. Source of truth: ios/Keyboard/KeyboardViewController.swift,
ADR docs/adrs/0002-keyboardkit-keyboard-rendering.md, and
docs-site/content/native-ios.md.
Working pattern:
- Put the Pasta row in KeyboardKit's native
toolbar:slot and keep the stock autocomplete view:toolbar: { params in PastaKeyboardToolbar(autocompleteToolbar: params.view, ...) }. KeyboardKit already composes the keyboard asVStack { toolbar; keys }; replacingparams.viewwith a customPublish/Pasteshelf removes autocorrect. - Do not set an explicit full-keyboard surface with
.keyboardViewStyle(.init(background: .color(.keyboardBackground))), do not repaintview/inputView/hosting-controller backgrounds, and do not hand-pick a gray fallback. Keep those views clear so the keyboard host and KeyboardKit key chrome own the surface. The last visible mismatch was the app painting the whole keyboard, not just the toolbar. - Keep Pasta toolbar controls compact and transparent. Prefer icon-only side actions around autocomplete, with accessibility labels and no chip/shelf fill.
- Do not: render the row as a sibling above
KeyboardView; passEmptyView()into the slot with a zero-height autocomplete toolbar; use.ignoresSafeArea(.top); or hand-paint a background behind a sibling band. That stack is exactly what produces the cropped, detached strip. - Let KeyboardKit own the layout. Do not strip
.nextKeyboard; on iPhone the standard layout adds the globe only whenneedsInputModeSwitchKeyis true, and on iPad it adds it unconditionally — either way, removing it strands the user with no keyboard switch (a HIG / App Review risk). - Observe
KeyboardContextso the view re-evaluates; keyKeyboardView's.idonly on structural changes (keyboard type, orientation, size, device class). Never key.idonkeyboardCase— it tears the keyboard down on every auto-capitalization flip mid-typing and cancels in-flight gestures. - Privacy stays intact: never publish ordinary keystrokes; read the pasteboard only behind an explicit user-tapped action.
- Never present UIKit modals or host text fields in the extension. A custom
keyboard may draw only inside its input view controller's primary view, and
alerts are unavailable to
com.apple.keyboard-service, soUIAlertController/UIAlertViewand anypresent(_:animated:)call silently fail.UITextField/UITextViewinside the extension hijack the responder chain and invalidate the hosttextDocumentProxy, which leaves the keyboard unable to type at all. This is what broke the first passkey prompt. - For input that Pasta itself must capture (passkeys, search), render Pasta views
inside the existing toolbar band and set
KeyboardContext.textInputProxyto a Pasta-ownedUITextDocumentProxy. KeyboardKit routes every insert and delete through the context proxy, so the native keys stay intact; clear the proxy to return input to the host, disable autocomplete while it is active, and insert host text throughoriginalTextDocumentProxy. textInputProxyrouting is not exclusively Pasta's. KeyboardKit's keyboard-switch (globe) button detaches the proxy on touch and restores it 0.5s later (Keyboard+NextKeyboardButton.swift), so any Pasta capture must observe$textInputProxy: close the capture UI when the proxy is detached so keystrokes cannot land in the host document under Pasta's own prompt, and make a reattached Pasta proxy forward to the host instead of dropping key presses. Dropping them leaves the keyboard unable to type until the extension restarts.
Proof: swift test --package-path ios exercises the shared Swift package, not
PastaKeyboard.appex; keyboard-extension changes require a simulator
xcodebuild with an isolated -derivedDataPath. Host swift -e is not valid
UIKit or keyboard-extension proof; use simulator-targeted xcodebuild or an
executable built against the simulator SDK. A green xcodebuild
simulator build and PluginKit registration prove it
compiles and installs, not that the chrome looks right. Keyboard-extension
chrome (top strip, safe area, globe, height) only renders correctly in the real
extension host — the last strip survived a green simulator build and a TestFlight
build and was caught only by a device screenshot. Require a device/TestFlight
screenshot before claiming the visual is fixed. When unsure, read the pinned
KeyboardKit sources in-repo (ios/build/DerivedData/SourcePackages/checkouts/KeyboardKit/,
especially _Keyboard/KeyboardView.swift, _Keyboard/Views/Keyboard+Toolbar.swift,
and Demo/Keyboard/DemoKeyboardView.swift).
Delivery
- Every task in this repository ends with the verified changes committed on
mainand pushed toorigin/mainunless the user explicitly asks not to publish. - Before committing, run the strongest practical verification for the touched surface and include any blocker in the final response.
- If a task changes Worker routes, Durable Object behavior, D1 schema, migrations, or documented remote API behavior, publishing code or creating a tag is not enough. Apply required remote D1 migrations with
mise exec -- fnox exec -- wrangler d1 migrations apply DB --remote, deploy withmise exec -- fnox exec -- wrangler deploy, and run a non-leaking remote smoke againsthttps://pasta.nothuman.workfor the changed path unless the user explicitly says not to deploy. Record the migration/deploy/smoke evidence before finalizing.