Skip to content

Latest commit

 

History

History
315 lines (212 loc) · 21.3 KB

File metadata and controls

315 lines (212 loc) · 21.3 KB

Contributing

Practical contributor and maintainer guide for working on SpeakSwiftlyServer, including local setup, validation, live end-to-end coverage, pull request workflow, and release handoff.

Table of Contents

Overview

Who This Guide Is For

This guide is for contributors and maintainers making source, docs, test, release, or operator-surface changes in the standalone SpeakSwiftlyServer repository.

Before You Start

  • Treat Package.swift as the source of truth for package structure, dependencies, resources, and deployment targets.
  • Start with AGENTS.md for the repo's package, architecture, and workflow rules.
  • Keep package graph changes together in one pass, including Package.swift, Package.resolved, tests, and matching docs.
  • Keep transport-local shaping at the HTTP and MCP edges. If SpeakSwiftly or TextForSpeech can express a concept directly, prefer deleting server-local inference instead of adding another translation layer.
  • Preserve the current standalone package baseline on macOS 15 while keeping the host and state model friendly to the near-future iOS reuse path.
  • Use Xcode's selected Swift toolchain through xcrun; this package declares Swift tools version 6.3 and macOS 15.0 in Package.swift.
  • Expect the ordinary package lane to work without secrets, without changing the installed LaunchAgent, and without a live service. Live end-to-end coverage is opt-in and documented separately below.

Contribution Workflow

Choosing Work

Choose work from the current repo state rather than from stale assumptions. Use README.md for the product and agent-facing story, ROADMAP.md for planned work, the active maintainer docs under docs/maintainers when the task touches current architecture or workflow, the historical release records under docs/releases when you need prior release context, and docs/investigations when you need past incident or debugging context.

For substantial changes, work on a feature branch instead of local main. When a task already has active release or bug-fix context on a branch, keep the follow-on work stacked on that line unless maintainers explicitly want a separate branch.

A feature branch is the local work surface, not an automatic publishing step. Push the branch or open a pull request only when the maintainer asks for remote review, asks for a push, asks for a release, or intentionally runs the repo-owned release workflow. For ordinary local implementation or documentation passes, finish the coherent local change, run the relevant local validation, inspect the diff, and report the status before starting any GitHub handoff.

Making Changes

Use the xcrun SwiftPM path intentionally in this repository:

xcrun swift build
xcrun swift test

This repo documents the xcrun form because the standalone Swiftly-selected Swift 6.3 toolchain currently reproduces a transitive _NumericsShims module-loading failure that does not appear when SwiftPM runs through Xcode's selected toolchain.

Keep work bounded and coherent:

  • keep transport concerns at the HTTP and MCP edges
  • keep host ownership narrow instead of adding new coordinator layers casually
  • update docs in the same pass when HTTP, MCP, LaunchAgent, release, or source-layout behavior changes
  • prefer the repo-maintenance scripts over one-off release or validation command chains when the repo already owns a workflow

Asking For Review

Before asking for review, make sure the affected docs are in sync, the local validation path for the change has run, and any release-relevant or operator-facing behavior changes are called out plainly. Do not open a pull request just because local edits exist; treat pull requests as explicit review or release handoffs. For release work, use the documented branch-to-main split instead of trying to tag directly from a feature branch.

Local Setup

Runtime Config

The concrete runtime config surfaces in this repo are:

  • server.yaml for local config examples
  • the bundled default-server.yaml resource in the SpeakSwiftlyServer target
  • the persisted ~/Library/Application Support/SpeakSwiftlyServer/server.yaml config seeded from that resource
  • APP_NAME
  • APP_ENVIRONMENT
  • APP_DEFAULT_VOICE_PROFILE_NAME
  • APP_HOST
  • APP_PORT
  • APP_SSE_HEARTBEAT_SECONDS
  • APP_COMPLETED_JOB_TTL_SECONDS
  • APP_COMPLETED_JOB_MAX_COUNT
  • APP_JOB_PRUNE_INTERVAL_SECONDS
  • APP_HTTP_ENABLED
  • APP_HTTP_HOST
  • APP_HTTP_PORT
  • APP_HTTP_SSE_HEARTBEAT_SECONDS
  • APP_MCP_ENABLED
  • APP_MCP_PATH
  • APP_MCP_SERVER_NAME
  • APP_MCP_TITLE
  • the staged release artifact under .release-artifacts/current/ for LaunchAgent-owned runs

The server loads persisted YAML through the package's Foundation URL-backed YAML provider and swift-configuration; environment variables remain a developer override surface, and persisted YAML takes precedence over bundled defaults. The normal standalone path seeds ~/Library/Application Support/SpeakSwiftlyServer/server.yaml from the bundled default-server.yaml resource when that persisted file is missing. Runtime startup choices live in the same document under app.runtime, so HTTP, MCP, embedded, and tool-managed saves no longer maintain a separate runtime JSON file.

The tool-managed LaunchAgent layout is centered on one per-user location under ~/Library/Application Support/SpeakSwiftlyServer, with logs in ~/Library/Logs/SpeakSwiftlyServer. The SpeakSwiftlyServerTool target owns that layout through AppManagedInstallLayout.swift; the library target stays focused on embedded server state, runtime hosting, HTTP, and MCP.

The default build and unit-test loop does not require secrets or a running LaunchAgent. Use server.yaml only when you want to run the executable, inspect configuration loading, or inspect LaunchAgent-owned behavior locally.

Runtime Behavior

For a fresh checkout, resolve dependencies once and then use the normal package loop:

xcrun swift package resolve
xcrun swift build
xcrun swift test

Use the unified tool surface when you want to inspect the operator path directly:

xcrun swift run SpeakSwiftlyServerTool help
xcrun swift run SpeakSwiftlyServerTool launch-agent print-plist
xcrun swift run SpeakSwiftlyServerTool healthcheck

The server-owned Application Support config is LaunchAgent-oriented and defaults to 127.0.0.1:7337. The in-memory default profiles still reserve 7338 for ad hoc standalone executable configs and 7339 for embedded app-owned configs when a config file omits an explicit port.

For Codex plugin or hook changes, keep end-user behavior plugin-managed and use the repo-local .codex/ files only as a development harness. Current Codex builds require both features.hooks = true and features.plugin_hooks = true before installed plugin lifecycle hooks run; do not add ~/.codex/hooks.json as a normal repair path. Codex 0.129.0 stores per-hook review decisions under [hooks.state] in ~/.codex/config.toml; use the hooks settings panel to approve expected Speak Swiftly hooks instead of bypassing review with a user-level hook. The hook doctor summarizes the active install, hook feature flags, hook review state, and voice-profile state:

node scripts/codex-hooks-doctor.mjs

When testing Codex plugin payload behavior, keep Gale's personal Codex scope reserved for stable production installs. Use docs/maintainers/plugin-install-testing.md to inspect this repository's manifest, hooks, skills, and MCP payload locally, and leave marketplace add, upgrade, remove, and catalog-reference tests to the socket checkout.

Before any live end-to-end run, make sure the LaunchAgent-backed live service has released resident model memory through the live-service model unload preflight. Leave the installed service in place; the E2E helper runs on its own random ports and only needs comfortable memory headroom.

System Voice Profile Resources

Create package-owned system voice profiles through the upstream SpeakSwiftly command plugin from this package checkout:

xcrun swift package plugin --allow-writing-to-package-directory upsert-system-voice-profile \
  --target SpeakSwiftlyServer \
  --name swift-signal \
  --text "A short source text for the generated system voice." \
  --vibe femme \
  --voice-description "A clear, bright, steady technical-assistant voice."

The upsert-system-voice-profile command plugin is the normal authoring surface for Sources/SpeakSwiftlyServer/Resources/SystemProfiles/profiles/<profile-name>/. Do not manually copy profiles out of a live runtime profile store or hand-edit generated manifests to make them appear system-authored. If xcrun swift package plugin --list does not show the upsert-system-voice-profile verb, treat that as a package/plugin exposure issue to fix before generating resources.

Profile generation uses MLX/Metal. If a Codex sandboxed process cannot see the default Metal GPU, rerun the command plugin from a normal user shell or an explicitly unsandboxed command. Use the lower-level SpeakSwiftlyTool --system-profile-resource-root entrypoint only for upstream debugging or plugin implementation work, not for ordinary server resource authoring.

Development Expectations

Naming Conventions

Keep user-facing lifecycle vocabulary consistent across docs, scripts, and commands. In this repo that means preferring pairs like install and uninstall, treating install as the normal all-in-one staged-artifact plus LaunchAgent refresh path, keeping promote-live as the explicit staged-to-live promotion spelling, and avoiding alternate verbs for the same operator action unless a compatibility surface already requires them.

The built-in voice names are swift-signal and swift-anchor. Treat them as package-owned seed identities, not user-owned example names. If an existing user profile already owns one of those names, default-voice installation falls back to a -builtin suffix for the package copy instead of overwriting or renaming the user's profile.

Normal voice-profile creation flows should remain user-owned. Built-in voice work should preserve a clear distinction between user-authored profiles and package-authored system profiles, and should not add public API fields unless application consumers need those fields to make a user-visible decision.

Match the current boundary language in the code:

  • EmbeddedServer is the public app-owned embedding surface
  • transport-local shaping belongs at the HTTP and MCP edges
  • internal host ownership should stay behind the package boundary instead of leaking new public helpers casually

Accessibility Expectations

This repository does not currently maintain a separate top-level ACCESSIBILITY.md. For relevant work, treat accessibility and operator clarity as part of normal change quality: keep user-facing logs, errors, and documentation explicit, readable, and unambiguous, and call out any new user-visible limitation plainly in docs or review notes when it matters.

Verification

The maintainer validation entrypoint is:

sh scripts/repo-maintenance/validate-all.sh

That validation path now includes the default package lane (xcrun swift build and xcrun swift test) before the repo-owned DocC, formatting, and lint checks, so maintainers do not need a separate "ordinary SwiftPM lane" command chain just to get the standard package signal.

It also owns the repo-specific CLI smoke checks that prove the built tool still renders help output and LaunchAgent property lists correctly, so GitHub can rely on one authoritative maintainer lane instead of duplicating package build, test, and DocC steps in a second workflow.

Remote GitHub Actions intentionally runs the lighter CI wrapper:

sh scripts/repo-maintenance/validate-ci.sh

That wrapper keeps the toolkit, plugin, workflow, package build, and package test checks on pull requests and main pushes while leaving DocC, CLI smoke, SwiftFormat, SwiftLint, and live E2E to the local maintainer and release gates.

Direct formatter and linter commands are:

swiftformat --lint --config .swiftformat .
swiftformat --config .swiftformat .
swiftlint lint --config .swiftlint.yml

The live end-to-end gate is intentionally small and should still be run in one foreground process at a time:

sh scripts/repo-maintenance/validate-local-e2e.sh

The script unloads resident models from the installed LaunchAgent-backed service, runs SPEAKSWIFTLYSERVER_E2E=1 xcrun swift test --filter ServerTransportE2ETests, and reloads resident models afterward. The standard release workflow runs this local live E2E gate by default after the normal maintainer validation gate. Use --skip-local-e2e on release.sh only when the current release intentionally cannot touch the live service on this machine and another concrete live E2E signal exists for the release candidate.

This suite is a transport-owned smoke pass, not a second copy of SpeakSwiftly's broader worker end-to-end coverage. Keep this repo's live E2E focused on proving the shipped server can boot the published runtime, answer over HTTP and MCP, deliver MCP resource updates, and retain completed request state.

Maintainer Reference

Embedding

The supported public embedding surface is EmbeddedServer, defined in Sources/SpeakSwiftlyServer/Host/EmbeddedServer.swift. App code owns that one observable object directly, calls liftoff(), binds UI to its observable properties, and uses the same object for runtime controls, playback controls, voice-profile actions, and direct live speech submission through queueLiveSpeech(...).

Embedded app callers should pass SpeakSwiftly.RequestContext directly when the app has richer caller, project, or origin metadata than the server can infer. HTTP and MCP speech surfaces add transport defaults for that context automatically.

If callers do not pass EmbeddedServer.Options(port:), the embedded host defaults to 127.0.0.1:7339. If callers pass EmbeddedServer.Options(runtimeProfileRootURL:), the server treats that as its profile-store root, resolves it to the containing state root, and passes that state root directly into SpeakSwiftly.liftoff(configuration:stateRootURL:), while keeping the server's own runtime-configuration snapshot aligned with the same on-disk state.

Codex Plugin

This repository is the canonical payload source for the Socket-managed Speak Swiftly Codex plugin. The root .codex-plugin/plugin.json points at the checked-in .mcp.json connection, the tracked skills/ bundle, and the plugin-managed hooks/hooks.json file.

This repo does not maintain Claude Code or Anthropic plugin parity. Keep plugin work focused on the Socket-managed Codex path, and remove tracked Claude-specific docs, manifests, scripts, or examples if they appear.

Default user-facing install and update examples should use the Socket marketplace entry:

codex plugin marketplace add gaelic-ghost/socket
codex plugin marketplace upgrade socket

The plugin identity is speak-swiftly, with display name Speak Swiftly; older standalone installs may still appear as speak-swiftly-server until upgraded or disabled. Do not use a repo-local marketplace file from this checkout for normal installs. Socket is the supported marketplace entrypoint for end users.

Marketplace installation gives Codex the plugin payload: skills, MCP registration for http://127.0.0.1:7337/mcp, and lifecycle hooks. It does not by itself start, install, or update the native Swift service.

End users should start with plugin-managed hook setup rather than copying repo-local .codex files into their own Codex home. User-level ~/.codex/hooks.json Speak Swiftly entries are duplicate or legacy repair targets, not the healthy default path.

Current Codex builds require both features.hooks = true and features.plugin_hooks = true before installed plugin lifecycle hooks become runnable. hooks enables the hook system generally; plugin_hooks enables hook sources loaded from installed plugins. Codex 0.129.0 also persists per-hook review decisions under [hooks.state] in ~/.codex/config.toml; missing Speak Swiftly entries there mean the operator needs to approve the plugin-managed hooks in Codex settings.

Use the hook doctor as the first audit surface for installed plugin metadata, duplicate user-level hooks, hook review state, live runtime reachability, and recent hook logs:

node scripts/codex-hooks-doctor.mjs
node scripts/codex-hooks-doctor.mjs --repair-plan

For install-surface testing, use docs/maintainers/plugin-install-testing.md. Keep personal production Codex installs untouched by running manifest and payload checks from this repository; run actual marketplace add, upgrade, and catalog-reference tests from the socket checkout.

Monorepo and Submodule Handoff

  • Treat ../../speak-to-user/monorepo/packages/SpeakSwiftlyServer as the integration submodule copy, not the primary development home.
  • Treat the local ../../speak-to-user/monorepo checkout as a clean base checkout that stays on main and stays clean.
  • Never use that clean base checkout for feature work, experiments, release bumps, or submodule-pointer edits.
  • For monorepo work, create a dedicated git worktree, do the work there, open a pull request, and then delete the merged worktree and branch afterward.
  • When speak-to-user adopts a new server version, prefer updating the submodule pointer to a tagged SpeakSwiftlyServer release instead of an arbitrary branch tip.

Release Workflow

Use the repo-maintenance release entrypoint intentionally:

scripts/repo-maintenance/release.sh --mode standard --version vX.Y.Z

Run standard mode from a feature branch or worktree. It validates the checkout, creates the annotated tag, pushes the branch and tag, opens or updates the release PR, watches CI, checks review state, merges the PR, fast-forwards local main, creates the GitHub release, and cleans up merged branches when safe.

The release flow runs scripts/repo-maintenance/version-bump.sh before tagging so version-bearing repo surfaces move with the release. After the release PR is merged, local main is fast-forwarded, the annotated tag is pushed, and the GitHub release exists, the standard flow refreshes the LaunchAgent-backed live service from the synced main checkout and runs the HTTP plus MCP healthcheck. Use --skip-live-service-update only when this machine should not be touched by that release.

Before the release branch is pushed, the standard flow also runs scripts/repo-maintenance/validate-local-e2e.sh after the normal maintainer validation gate. That makes local live transport E2E part of the default release signal instead of a separate manual checklist item.

Use deferred remote CI when full local validation has already passed and GitHub's check wait would otherwise keep a Codex shell process open just to poll:

scripts/repo-maintenance/release.sh --mode standard --version vX.Y.Z --remote-ci-mode defer

Deferred mode still performs the local release preparation, branch push, pull request creation, and initial check discovery. The release is not complete until the same thread resumes after CI settles and reruns the standard release command to finish review checks, merge, tagging, GitHub release creation, live-service update, and cleanup.

For the detailed contract and edge cases, use docs/maintainers/release-workflow.md.

Pull Request Expectations

Summarize what changed, why it changed, and what reviewers should pay attention to first. For release work, make sure the branch-side candidate preparation is complete before handing it off to main for the final publish step.

Communication

Raise uncertainty early when a task starts pushing on architecture, release semantics, or live-service behavior. If the clean path needs wider scope than the original request, say so before the change sprawls. If a behavior changed across docs, HTTP, MCP, LaunchAgent, or embedding surfaces, mention that explicitly instead of assuming reviewers will reconstruct it from the diff.

Documentation Map

License and Contribution Terms

This repository is licensed under Apache License 2.0. By contributing, you are contributing changes under that same project license.