directa is a personal tool first; issues and patches are welcome all the same. Read AGENTS.md for the codebase map, invariants, and commands (CLAUDE.md is a one-line pointer to it), and docs/cli-contract.md for the JSON surface. make test and scripts/smoke.sh must pass locally, and make dead-code (after brew install periphery) must report no unused code; scripts/smoke-launchd.sh exercises the real launchd lifecycle if your change touches daemon management. GitHub Actions (.github/workflows/ci.yml) runs swift build + make test on macos-26 only: no smoke, no large runners.
The Swift compiler driver leaves a small temporary folder in your user temp directory on every build and never removes it (swiftlang/swift-driver#1720), so thousands pile up on a busy machine. scripts/sweep-swift-temp.sh removes only those leftovers (its header lists exactly which folders qualify), and make build and make test run it first. To also run it on every commit, enable the versioned hooks once per clone:
git config core.hooksPath .githooksThe hook never blocks a commit: a failed sweep is printed and the commit goes ahead. git config --unset core.hooksPath turns it off again.
Commit subjects and PR titles are Conventional Commits. The type/scope list and subject grammar live in AGENTS.md (Engineering rules / Git). GitHub squash uses the PR title as the main subject, so the PR title is a conventional commit subject, not a headline sentence.
Never git stash (use temp commits). Agents never bump versions or publish.
Good:
feat(cli): add Grok Build session hook so sessions rediscover servers
fix(app): keep the menu bar extra from quitting after memory pressure
fix(supervisor): spawn agent-managed servers outside the daemon's jetsam coalition
feat(cli)!: leave lock holders running by default
docs: adopt conventional commits for PR and commit titles
Bad:
lock: hold the resource without stopping servers by default
Lock leaves servers up; doctor reports jetsam leftovers; DMG installer can replace the app
Update SetupPerformer.swift
WIP
fix stuff
The first is missing a type. The second is a semicolon laundry list: title the primary change (! then feat then fix) and name the rest in the body. The others are a file list or a placeholder.
User-facing: what changed for a person running directa, migration if any, how it was verified. Not a restatement of the subject, not a file list. Version Packages PRs keep the title the Changesets bot writes.
User-facing changes get a Changeset (npm run changeset); see .changeset/README.md. Internal-only work (CI, agent docs, no-behavior refactors) never gets a changeset. A breaking subject (type(scope)!:) still needs a major changeset: the ! is for git log, Changesets own the version.
The body is changelog text for someone using directa. Lead with what changed for them. No function names, internal file paths, or launchd labels. A term of art (jetsam, lock resource) gets a short plain-English gloss on first use.
Product version lives in package.json. npm run version (used by the Release workflow) syncs DirectaVersion.version in Sources/DirectaKit/Model/Models.swift.
GitHub releases are driven by Changesets. After changesets land on main, a Version Packages PR appears; merging it tags vX.Y.Z and opens the GitHub release. Nothing is published to npm: the root package.json is private and only tracks the product version.
A separate macOS workflow (.github/workflows/release-dmg.yml) builds a Developer ID-signed, notarized DMG and attaches it to that release. The Release job dispatches it after a successful publish (workflow_dispatch): a release created with GITHUB_TOKEN does not fire release:published on other workflows. Required repository secrets:
APPLE_DEVELOPER_ID_P12_BASE64/APPLE_DEVELOPER_ID_P12_PASSWORD: exported Developer ID Application certificateAPPLE_SIGN_IDENTITY: exact codesign identity string (for exampleDeveloper ID Application: Name (TEAMID))APPLE_API_KEY_BASE64/APPLE_API_KEY_ID/APPLE_API_ISSUER: App Store Connect API key fornotarytool
The release DMG build runs with DIRECTA_REQUIRE_SIGNING=1, so a runner missing the certificate fails the build rather than shipping an ad-hoc image Gatekeeper would disable.
Local path: make release-dmg (Developer ID signed, notarized, and stapled; fails loudly if signing or notary credentials are missing), then gh release upload vX.Y.Z dist/directa-X.Y.Z.dmg.
directa is distributed as a cask through the self-owned tap quantizor/homebrew-tap (installed as brew install --cask quantizor/tap/directa). A tap is required rather than optional: the official homebrew/cask needs 225 stars and a 30-day-old repo, and a tapless cask can never be upgraded (brew re-reads the definition saved at install time, so the version always compares equal).
The release workflow bumps the tap automatically on publish. The cask's structure (packaging/homebrew/directa.rb), the bump-homebrew-cask workflow that injects each release's version and sha256, and the scripts/smoke-cask.sh gate are all documented in docs/releasing.md. One additional repository secret drives the bump:
HOMEBREW_TAP_TOKEN: a fine-grained PAT withcontents: writeonquantizor/homebrew-tap(the defaultGITHUB_TOKENcannot push across repos). A GitHub App token viaactions/create-github-app-tokenis the equivalent alternative.
Anything in the cask's uninstall runs on every brew upgrade, not only on uninstall, so it is limited to unregistering the background agent (the app re-registers it when brew relaunches). It must never remove hooks or data: nothing restores those automatically. Full removal is directa uninstall.
The session-context payload is harness-agnostic: directa context prints a fenced plain-text block describing the current project's servers, and directa statusline prints a one-line presence summary from statusline stdin JSON. Wiring those into a harness is the only per-harness work.
- Conform to
HarnessAdapterinSources/directa/HookSupport.swift: aname(the--harnessvalue), thesettingsURLof the file the harness reads, and an idempotentinstall(cliPath:)that merges a session-start hook into that file without clobbering what is already there. Read and write it with the protocol's ownloadSettings()andwriteSettings(_:)rather than reaching forData(contentsOf:):installwrites back everything it reads, so a read that answers "empty" for a file that exists turns the merge into a replacement of settings directa does not own.loadSettingsrefuses a file it cannot parse for that reason, and returns an empty dictionary only when there is genuinely nothing there to lose. - If the harness wants a structured payload (as Claude Code does with
hookSpecificOutput.additionalContext, or Cursor with{additional_context}), add a hidden subcommand likeHookClaudeSessionStart/HookCursorSessionStartthat adapts theHookContext.renderoutput (a thin socket fetch over the pureAgentContext.renderrenderer in DirectaKit) to that shape. Keep the guarantees: exit 0 always, fast, silent when there is nothing to say, never auto-starting the daemon, and never emitting raw log lines or command strings (child output and committed configs are attacker-influenceable). Resolve the session directory viaHookSessionCwd(Cursor:workspace_roots/CURSOR_PROJECT_DIR; Claude:cwd; Grok:cwd/workspaceRoot/GROK_WORKSPACE_ROOT). When the event fires more than once per session, or another harness also runs this hook (Cursor runs the hooks in~/.claude/settings.jsonby default), decide whether this invocation speaks inHookPayloadGatefrom a field the vendor documents in the payload, emitting whenever the payload is unreadable. - Register the adapter in
harnessAdaptersand document the harness indocs/cli-contract.mdunderdirecta hook install.
A harness that only supports plain-text injection needs no adapter code at all: point its hook at directa context. Shipped adapters today: antigravity, claude, cursor, grok, opencode. Grok discards hook stdout on SessionStart and UserPromptSubmit, and delivers PreToolUse additionalContext after the tool result, so that adapter registers PreToolUse and UserPromptSubmit (UserPromptSubmit marks the turn; PreToolUse emits once per turn), removes this command from any other event on install, and also writes a managed ~/.grok/rules/directa.md (a standing instruction to run directa context, not a live snapshot, because Grok home rules apply to every project and cover the first tool of a turn; the text lives in HarnessStandingInstruction, shared with OpenCode). OpenCode has no session-start injection point at all, so its adapter wires that same standing instruction through the instructions array of the winning global config (opencode.jsonc preferred over opencode.json) and never writes ~/.config/opencode/AGENTS.md, which would shadow the ~/.claude/CLAUDE.md fallback OpenCode reads. Do not register a Stop hook to smuggle additionalContext: Stop additionalContext is injected as a user message and continues the turn.