Skip to content

Instantly share code, notes, and snippets.

@bbarker
Last active April 28, 2026 15:25
Show Gist options
  • Select an option

  • Save bbarker/61eac1666140aadcbc39d1c5d1a14564 to your computer and use it in GitHub Desktop.

Select an option

Save bbarker/61eac1666140aadcbc39d1c5d1a14564 to your computer and use it in GitHub Desktop.
Unison Work Prompt (Steering) file

Architecture and Style

Strive to write functional-style, maintainable code, while keeping an eye on performance. Avoid anything unsafe.

CRITICAL: Always force thunked function calls with (). If a function returns '{g} a, calling it without () silently discards the unevaluated thunk — causing data loss with no type error or runtime crash. E.g. _ = createAlert ... link does nothing; _ = createAlert ... link () works. See also: CLAUDE.md "Stream Double-Thunk" for the related do vs parens pitfall.

Never use bug in production code. bug is an uncatchable crash — it cannot be recovered from by any handler. In a Route context it causes "Internal Server Error". Use pattern matching to make all branches explicit instead of using bug as an "unreachable" marker. The same applies to any other construct that crashes the runtime (e.g., abort, incomplete pattern matches). In tests, prefer ensure false or test.fail over bug for assertion failures.

Rather than using full namespace paths at each use site, try to import (use) the terms at the top of the term definition to make code more readable.

For business logic, structural types should almost always be avoided; these may be more useful in certain library contexts, but should be carefully considered.

Use newtypes when wrapping primitive types that are passed (or likely to be passed) as arguments to functions with other arguments of the same primitive types to avoid confusion.

Never use bare Boolean for config flags. In Unison's content-addressed model, all false values share the same hash, so two Boolean config terms with the same value become aliases. Use a two-constructor enum type instead (e.g., unique type PipelineStartMode = StartPipelines | SkipPipelines).

Don't use comments like this:


Nobody reads this as they aren't stored in the codebase, and it makes folds ("---") hard to find in large scratch files.

When creating or significantly updating a term, especially a function, add a .doc (Doc) term for it, succinctly explaining its purpose. But don't create doc terms for transient things like dead code review, temporary plans that will soon be implemented, etc; use markdown files instead for that.

Avoid single-character variable names; 2 characters is ok if the meaning is obvious.

HTML Templates

When creating or modifying HTML templates, use the @hojberg/html library for structured HTML generation instead of raw HTML text strings. Use the helper functions in anyxchange.html (e.g., checkbox, radioGroup, selectField, badge, formGroupLabel) for common UI patterns. Keep configurable options (like categories, radius values, etc.) in anyxchange.config as typed lists rather than hardcoding them in templates. For functions that need to return Text for backward compatibility, create an internal *Html variant that returns Html and a wrapper that calls Html.toText on it.

Use existing CSS classes (.card, .form-grid, .form-actions, etc.) from anyxchange.ui.styles rather than inventing new ones (see CLAUDE.md for details).

Environment Configuration

Prefer Environment.Config.lookup over expect for optional config like external APIs to avoid crashes when not configured (see CLAUDE.md for details).

General Reminders and Instructions

Don't use git generally; use UCM (Unison) via MCP. Don't edit or ideally even read files in .old_do_not_edit as they are likely stale. Don't be surprised in a scratch file is empty; i often reset them after they grow large; you should refer to code in the codebase via Unison MCP typically.

Be sure to add tests as you go to confirm functionality. When adding new functionality, we should usually edit existing functions so it is wired into the rest of the app rather than creating a modified copy.

If doing new work, add more granular lists of tasks as checkboxes under each feature and check them off once completed (and tested if testing). Be sure to add test> watch expressions to confirm (non-IO) tests are working: https://www.unison-lang.org/docs/usage-topics/testing/

Logs (Log.info or Log.debug) should be used prudently but not overused.

Any new pages should use the page template anyxchange.ui.templates.page; look up dependents of anyxchange.ui.templates.page in Unison MCP to find example usages.

Use the Unison and Playwright MCP servers as needed to confirm functionality; the app should be served locally at http://localhost:8080/s/anyxchange/ if you run anyxchange.app.deployLocal. Before passing code off to me, check the relevant scratch file with MCP, e.g.:

Called unison.typecheck-code({"projectContext":{"projectName":"some-project","branchName":"some-branch"},"code":{"filePath":"/home/bbarker/workspace/unison-scratch/scratch-codex-some-project.u"}})

Note that local deployments use a temporary database that is reset on each deploy; any persistent data for testing should be added to populateTestData (if warranted).

Please use a single scratch file per project to avoid issues with UCM's update workflow, especially when you're switching between multiple Unison projects.

Preferred naming convention:

  • scratch-{ai-agent}-{project}--{branch}.u (e.g. scratch-codex-anyxchange--main.u)
  • Omit --{branch} only when working exclusively on main

CRITICAL: The branch in the scratch file name must match the branchName used in all Unison MCP calls (e.g. typecheck-code, view-definitions, etc.). At the start of a session, verify the active branch with get-current-project-context and confirm your scratch file name matches. Only deviate if explicitly directed (e.g. comparing definitions across branches).

Also: avoid deleting scratch-file code as a cleanup step. If older scratch content is in the way, fold it under a --- line. See "Important notes" for the exception during update.

Finally, don't treat scratch files as the source of truth for "what is in the codebase". Use the Unison MCP server to look up current definitions; scratch files (including scratch.u) may be outdated or below folds.

Instead of asking me to run ucm commands, or doing it yourself, try first to use the UCM MCP server.

Once code changes are completed and typechecked, tell the user the changes are ready. The user should run edit.dependents on the modified terms to pull any downstream definitions into the scratch file for re-typechecking, repeating as needed until everything is clean. Then the user runs update in UCM to apply all changes to the codebase. After that, the user can run anyxchange.app.deployLocalTest to test locally (though this may not always work depending on environment setup).

After making changes, please add a concise summary to the top of the anyxchange.CHANGES term (a unison Doc); obtain the date from the Unix date command and Format it as YYYY-MM-DD. Don't run 'update' yourself for MCP; but feel free to use load <some_scratch.u> and then run anyxchange.app.deployLocal in Unison MCP to do any testing and verification; for reference, run behaves as follows:

anyxchange/main> ? run

  run
  `run mymain args...`  Runs `!mymain`, where `mymain` is searched for in the most recent
                        typechecked file, or in the codebase. Any provided arguments will be passed
                        as program arguments as though they were provided at the command line when
                        running mymain as an executable.

Report to the user a shortened version of the additions to changes, hopefully a short line, suitable for adding as a comment to the commit history using UCM's history.comment

When renaming terms (or types) use term.rename or similar UCM functionality rather than creating additional aliases, which just makes the code more confusing and complex. Then use the corresponding UCM delete functionality to remove the old alias.

Important notes

Don't delete code we've been working on from scratch files; I'll handle backing that up. The exception is that if it is clearly conflicting with something else you are working on, but even then probably better to just use a fold (---).

Scratch is not the source of truth; the codebase is. Removing a definition from a scratch file normally does nothing to the codebase — you'd need explicit delete.term / delete.type (or delete-definitions via MCP) to drop it. The one exception: during an update into a temporary update branch (e.g. update-main), a term absent from the scratch is treated as a deletion. Before running update, check what's being deleted; accidental drops here have cost us real code more than once.

Type Changes Require BOTH Migration AND Schema Update

When adding/removing fields from a type used in a database table, you MUST include BOTH in the same scratch file before running update:

  1. Migration - converts data and records new type hash
  2. Schema update - changes db.schema to use typeLink NewType

If you forget the schema update, enforceTypeConsistency will fail with a type mismatch error even though the migration ran correctly. This is because it compares the recorded hash against the schema's typeLink.

For schema-library upgrades, preserved old-schema aliases, and the checklist for historical migrations, see CLAUDE-AnyXchange.md "Database Migrations" and the AnyXchange README migration section.

Other notes

Project-level README Doc terms should live at the top-level in a project, not under a namespace, so they show up correctly on Unison Share.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment